mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 19:15:52 +01:00
Improve repository overview
- Sort repositories alphanumerically case insensitive per namespace - Make the namespaces collapsible and store the collapsed state in local storage Committed-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
2
gradle/changelog/collapsible_namespaces.yaml
Normal file
2
gradle/changelog/collapsible_namespaces.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: added
|
||||||
|
description: Make namespaces collapsible and save the collapsed state in local storage
|
||||||
2
gradle/changelog/sort-repos.yaml
Normal file
2
gradle/changelog/sort-repos.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: changed
|
||||||
|
description: Sort repositories alphanumerically per namespace
|
||||||
@@ -41,6 +41,7 @@ import javax.inject.Inject;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
@@ -48,6 +49,7 @@ import java.util.concurrent.locks.Lock;
|
|||||||
import java.util.concurrent.locks.ReadWriteLock;
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
@@ -146,7 +148,7 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Repository> getAll() {
|
public Collection<Repository> getAll() {
|
||||||
return withReadLockedMaps(() -> ImmutableList.copyOf(byNamespaceAndName.values()));
|
return withReadLockedMaps(() -> ImmutableList.copyOf(byNamespaceAndName.values().stream().sorted(Comparator.comparing(v -> v.getNamespaceAndName().toString().toLowerCase())).collect(Collectors.toList())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export * from "./usePluginCenterAuthInfo";
|
|||||||
export * from "./compare";
|
export * from "./compare";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./links";
|
export * from "./links";
|
||||||
|
export * from "./localStorage";
|
||||||
export { useNamespaceOptions, useGroupOptions, useUserOptions } from "./useAutocompleteOptions";
|
export { useNamespaceOptions, useGroupOptions, useUserOptions } from "./useAutocompleteOptions";
|
||||||
|
|
||||||
export { default as ApiProvider } from "./ApiProvider";
|
export { default as ApiProvider } from "./ApiProvider";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function useLocalStorage<T>(
|
export function useLocalStorage<T>(
|
||||||
key: string,
|
key: string,
|
||||||
initialValue: T
|
initialValue: T
|
||||||
): [value: T, setValue: (value: T | ((previousConfig: T) => T)) => void] {
|
): [value: T, setValue: (value: T | ((previousConfig: T) => T)) => void] {
|
||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
Repository,
|
Repository,
|
||||||
RepositoryCollection,
|
RepositoryCollection,
|
||||||
RepositoryCreation,
|
RepositoryCreation,
|
||||||
RepositoryTypeCollection
|
RepositoryTypeCollection,
|
||||||
} from "@scm-manager/ui-types";
|
} from "@scm-manager/ui-types";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import { apiClient } from "./apiclient";
|
import { apiClient } from "./apiclient";
|
||||||
@@ -72,7 +72,7 @@ export const useRepositories = (request?: UseRepositoriesRequest): ApiResult<Rep
|
|||||||
}
|
}
|
||||||
return useQuery<RepositoryCollection, Error>(
|
return useQuery<RepositoryCollection, Error>(
|
||||||
["repositories", request?.namespace?.namespace, request?.search || "", request?.page || 0],
|
["repositories", request?.namespace?.namespace, request?.search || "", request?.page || 0],
|
||||||
() => apiClient.get(`${link}?${createQueryString(queryParams)}`).then(response => response.json()),
|
() => apiClient.get(`${link}?${createQueryString(queryParams)}`).then((response) => response.json()),
|
||||||
{
|
{
|
||||||
enabled: !request?.disabled,
|
enabled: !request?.disabled,
|
||||||
onSuccess: (repositories: RepositoryCollection) => {
|
onSuccess: (repositories: RepositoryCollection) => {
|
||||||
@@ -80,7 +80,7 @@ export const useRepositories = (request?: UseRepositoriesRequest): ApiResult<Rep
|
|||||||
repositories._embedded?.repositories.forEach((repository: Repository) => {
|
repositories._embedded?.repositories.forEach((repository: Repository) => {
|
||||||
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
|
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -98,14 +98,14 @@ const createRepository = (link: string) => {
|
|||||||
}
|
}
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(createLink, request.repository, "application/vnd.scmm-repository+json;v=2")
|
.post(createLink, request.repository, "application/vnd.scmm-repository+json;v=2")
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
const location = response.headers.get("Location");
|
const location = response.headers.get("Location");
|
||||||
if (!location) {
|
if (!location) {
|
||||||
throw new Error("Server does not return required Location header");
|
throw new Error("Server does not return required Location header");
|
||||||
}
|
}
|
||||||
return apiClient.get(location);
|
return apiClient.get(location);
|
||||||
})
|
})
|
||||||
.then(response => response.json());
|
.then((response) => response.json());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,10 +117,10 @@ export const useCreateRepository = () => {
|
|||||||
const { mutate, data, isLoading, error } = useMutation<Repository, Error, CreateRepositoryRequest>(
|
const { mutate, data, isLoading, error } = useMutation<Repository, Error, CreateRepositoryRequest>(
|
||||||
createRepository(link),
|
createRepository(link),
|
||||||
{
|
{
|
||||||
onSuccess: repository => {
|
onSuccess: (repository) => {
|
||||||
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
|
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
|
||||||
return queryClient.invalidateQueries(["repositories"]);
|
return queryClient.invalidateQueries(["repositories"]);
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -129,7 +129,7 @@ export const useCreateRepository = () => {
|
|||||||
},
|
},
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
repository: data
|
repository: data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ export const useRepositoryTypes = () => useIndexJsonResource<RepositoryTypeColle
|
|||||||
export const useRepository = (namespace: string, name: string): ApiResult<Repository> => {
|
export const useRepository = (namespace: string, name: string): ApiResult<Repository> => {
|
||||||
const link = useRequiredIndexLink("repositories");
|
const link = useRequiredIndexLink("repositories");
|
||||||
return useQuery<Repository, Error>(["repository", namespace, name], () =>
|
return useQuery<Repository, Error>(["repository", namespace, name], () =>
|
||||||
apiClient.get(concat(link, namespace, name)).then(response => response.json())
|
apiClient.get(concat(link, namespace, name)).then((response) => response.json())
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ export type UseDeleteRepositoryOptions = {
|
|||||||
export const useDeleteRepository = (options?: UseDeleteRepositoryOptions) => {
|
export const useDeleteRepository = (options?: UseDeleteRepositoryOptions) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
||||||
repository => {
|
(repository) => {
|
||||||
const link = requiredLink(repository, "delete");
|
const link = requiredLink(repository, "delete");
|
||||||
return apiClient.delete(link);
|
return apiClient.delete(link);
|
||||||
},
|
},
|
||||||
@@ -161,21 +161,21 @@ export const useDeleteRepository = (options?: UseDeleteRepositoryOptions) => {
|
|||||||
}
|
}
|
||||||
queryClient.removeQueries(repoQueryKey(repository));
|
queryClient.removeQueries(repoQueryKey(repository));
|
||||||
await queryClient.invalidateQueries(["repositories"]);
|
await queryClient.invalidateQueries(["repositories"]);
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
remove: (repository: Repository) => mutate(repository),
|
remove: (repository: Repository) => mutate(repository),
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isDeleted: !!data
|
isDeleted: !!data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateRepository = () => {
|
export const useUpdateRepository = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
||||||
repository => {
|
(repository) => {
|
||||||
const link = requiredLink(repository, "update");
|
const link = requiredLink(repository, "update");
|
||||||
return apiClient.put(link, repository, "application/vnd.scmm-repository+json;v=2");
|
return apiClient.put(link, repository, "application/vnd.scmm-repository+json;v=2");
|
||||||
},
|
},
|
||||||
@@ -183,21 +183,21 @@ export const useUpdateRepository = () => {
|
|||||||
onSuccess: async (_, repository) => {
|
onSuccess: async (_, repository) => {
|
||||||
await queryClient.invalidateQueries(repoQueryKey(repository));
|
await queryClient.invalidateQueries(repoQueryKey(repository));
|
||||||
await queryClient.invalidateQueries(["repositories"]);
|
await queryClient.invalidateQueries(["repositories"]);
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
update: (repository: Repository) => mutate(repository),
|
update: (repository: Repository) => mutate(repository),
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isUpdated: !!data
|
isUpdated: !!data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useArchiveRepository = () => {
|
export const useArchiveRepository = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
||||||
repository => {
|
(repository) => {
|
||||||
const link = requiredLink(repository, "archive");
|
const link = requiredLink(repository, "archive");
|
||||||
return apiClient.post(link);
|
return apiClient.post(link);
|
||||||
},
|
},
|
||||||
@@ -205,21 +205,21 @@ export const useArchiveRepository = () => {
|
|||||||
onSuccess: async (_, repository) => {
|
onSuccess: async (_, repository) => {
|
||||||
await queryClient.invalidateQueries(repoQueryKey(repository));
|
await queryClient.invalidateQueries(repoQueryKey(repository));
|
||||||
await queryClient.invalidateQueries(["repositories"]);
|
await queryClient.invalidateQueries(["repositories"]);
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
archive: (repository: Repository) => mutate(repository),
|
archive: (repository: Repository) => mutate(repository),
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isArchived: !!data
|
isArchived: !!data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUnarchiveRepository = () => {
|
export const useUnarchiveRepository = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
||||||
repository => {
|
(repository) => {
|
||||||
const link = requiredLink(repository, "unarchive");
|
const link = requiredLink(repository, "unarchive");
|
||||||
return apiClient.post(link);
|
return apiClient.post(link);
|
||||||
},
|
},
|
||||||
@@ -227,35 +227,35 @@ export const useUnarchiveRepository = () => {
|
|||||||
onSuccess: async (_, repository) => {
|
onSuccess: async (_, repository) => {
|
||||||
await queryClient.invalidateQueries(repoQueryKey(repository));
|
await queryClient.invalidateQueries(repoQueryKey(repository));
|
||||||
await queryClient.invalidateQueries(["repositories"]);
|
await queryClient.invalidateQueries(["repositories"]);
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
unarchive: (repository: Repository) => mutate(repository),
|
unarchive: (repository: Repository) => mutate(repository),
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isUnarchived: !!data
|
isUnarchived: !!data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRunHealthCheck = () => {
|
export const useRunHealthCheck = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Repository>(
|
||||||
repository => {
|
(repository) => {
|
||||||
const link = requiredLink(repository, "runHealthCheck");
|
const link = requiredLink(repository, "runHealthCheck");
|
||||||
return apiClient.post(link);
|
return apiClient.post(link);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: async (_, repository) => {
|
onSuccess: async (_, repository) => {
|
||||||
await queryClient.invalidateQueries(repoQueryKey(repository));
|
await queryClient.invalidateQueries(repoQueryKey(repository));
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
runHealthCheck: (repository: Repository) => mutate(repository),
|
runHealthCheck: (repository: Repository) => mutate(repository),
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isRunning: !!data
|
isRunning: !!data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ export const useExportInfo = (repository: Repository): ApiResultWithFetching<Exp
|
|||||||
//TODO Refetch while exporting to update the page
|
//TODO Refetch while exporting to update the page
|
||||||
const { isLoading, isFetching, error, data } = useQuery<ExportInfo, Error>(
|
const { isLoading, isFetching, error, data } = useQuery<ExportInfo, Error>(
|
||||||
["repository", repository.namespace, repository.name, "exportInfo"],
|
["repository", repository.namespace, repository.name, "exportInfo"],
|
||||||
() => apiClient.get(link).then(response => response.json()),
|
() => apiClient.get(link).then((response) => response.json()),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ export const useExportInfo = (repository: Repository): ApiResultWithFetching<Exp
|
|||||||
isLoading,
|
isLoading,
|
||||||
isFetching,
|
isFetching,
|
||||||
error: error instanceof NotFoundError ? null : error,
|
error: error instanceof NotFoundError ? null : error,
|
||||||
data
|
data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -315,14 +315,14 @@ export const useExportRepository = () => {
|
|||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
apiClient
|
apiClient
|
||||||
.get(infolink)
|
.get(infolink)
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then((info: ExportInfo) => {
|
.then((info: ExportInfo) => {
|
||||||
if (info._links.download) {
|
if (info._links.download) {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
resolve(info);
|
resolve(info);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
reject(e);
|
reject(e);
|
||||||
});
|
});
|
||||||
@@ -335,21 +335,21 @@ export const useExportRepository = () => {
|
|||||||
onSuccess: async (_, { repository }) => {
|
onSuccess: async (_, { repository }) => {
|
||||||
await queryClient.invalidateQueries(repoQueryKey(repository));
|
await queryClient.invalidateQueries(repoQueryKey(repository));
|
||||||
await queryClient.invalidateQueries(["repositories"]);
|
await queryClient.invalidateQueries(["repositories"]);
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
exportRepository: (repository: Repository, options: ExportOptions) => mutate({ repository, options }),
|
exportRepository: (repository: Repository, options: ExportOptions) => mutate({ repository, options }),
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
data
|
data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePaths = (repository: Repository, revision: string): ApiResult<Paths> => {
|
export const usePaths = (repository: Repository, revision: string): ApiResult<Paths> => {
|
||||||
const link = requiredLink(repository, "paths").replace("{revision}", revision);
|
const link = requiredLink(repository, "paths").replace("{revision}", revision);
|
||||||
return useQuery<Paths, Error>(repoQueryKey(repository, "paths", revision), () =>
|
return useQuery<Paths, Error>(repoQueryKey(repository, "paths", revision), () =>
|
||||||
apiClient.get(link).then(response => response.json())
|
apiClient.get(link).then((response) => response.json())
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ export const useRenameRepository = (repository: Repository) => {
|
|||||||
const { mutate, isLoading, error, data } = useMutation<unknown, Error, RenameRepositoryRequest>(
|
const { mutate, isLoading, error, data } = useMutation<unknown, Error, RenameRepositoryRequest>(
|
||||||
({ name, namespace }) => apiClient.post(url, { namespace, name }, "application/vnd.scmm-repository+json;v=2"),
|
({ name, namespace }) => apiClient.post(url, { namespace, name }, "application/vnd.scmm-repository+json;v=2"),
|
||||||
{
|
{
|
||||||
onSuccess: () => queryClient.removeQueries(repoQueryKey(repository))
|
onSuccess: () => queryClient.removeQueries(repoQueryKey(repository)),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ export const useRenameRepository = (repository: Repository) => {
|
|||||||
renameRepository: (namespace: string, name: string) => mutate({ namespace, name }),
|
renameRepository: (namespace: string, name: string) => mutate({ namespace, name }),
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isRenamed: !!data
|
isRenamed: !!data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,17 +24,25 @@
|
|||||||
import React, { FC, ReactNode } from "react";
|
import React, { FC, ReactNode } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||||
|
import { useLocalStorage } from "@scm-manager/ui-api";
|
||||||
|
import Icon from "../Icon";
|
||||||
|
|
||||||
const Separator = styled.div`
|
const Separator = styled.div`
|
||||||
border-bottom: 1px solid rgb(219, 219, 219, 0.5);
|
border-bottom: 1px solid rgb(219, 219, 219, 0.5);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
namespaceHeader: ReactNode;
|
group: RepositoryGroup;
|
||||||
elements: ReactNode[];
|
elements: ReactNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupEntries: FC<Props> = ({ namespaceHeader, elements }) => {
|
const GroupEntries: FC<Props> = ({ group, elements }) => {
|
||||||
|
const [t] = useTranslation("namespaces");
|
||||||
|
const [collapsed, setCollapsed] = useLocalStorage<boolean | null>(`repoNamespace.${group.name}.collapsed`, null);
|
||||||
|
|
||||||
const content = elements.map((entry, index) => (
|
const content = elements.map((entry, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<div>{entry}</div>
|
<div>{entry}</div>
|
||||||
@@ -42,12 +50,45 @@ const GroupEntries: FC<Props> = ({ namespaceHeader, elements }) => {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const settingsLink = group.namespace?._links?.permissions && (
|
||||||
|
<Link to={`/namespace/${group.name}/settings`} aria-label={t("repositoryOverview.settings.tooltip")}>
|
||||||
|
<Icon
|
||||||
|
color="inherit"
|
||||||
|
name="cog"
|
||||||
|
title={t("repositoryOverview.settings.tooltip")}
|
||||||
|
className="is-size-6 ml-2 has-text-link"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classNames("is-flex", "is-align-items-center", "is-size-6", "has-text-weight-bold", "p-3")}>
|
<div
|
||||||
{namespaceHeader}
|
className={classNames(
|
||||||
|
"is-flex",
|
||||||
|
"is-align-items-center",
|
||||||
|
"is-justify-content-space-between",
|
||||||
|
"is-size-6",
|
||||||
|
"has-text-weight-bold",
|
||||||
|
"p-3",
|
||||||
|
"has-cursor-pointer"
|
||||||
|
)}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Link to={`/repos/${group.name}/`} className="has-text-inherit">
|
||||||
|
{group.name}
|
||||||
|
</Link>{" "}
|
||||||
|
{settingsLink}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
color="inherit"
|
||||||
|
name={collapsed ? "caret-left" : "caret-down"}
|
||||||
|
title={t("repositoryOverview.settings.tooltip")}
|
||||||
|
className="is-size-6 ml-2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames("box", "p-2")}>{content}</div>
|
{collapsed ? null : <div className={classNames("box", "p-2")}>{content}</div>}
|
||||||
<div className="is-clearfix" />
|
<div className="is-clearfix" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import useLocalStorage from "./useLocalStorage";
|
import { useLocalStorage } from "@scm-manager/ui-api";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "scm.accessibility";
|
const LOCAL_STORAGE_KEY = "scm.accessibility";
|
||||||
|
|||||||
@@ -22,35 +22,18 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { GroupEntries, RepositoryEntry } from "@scm-manager/ui-components";
|
||||||
import { Icon, RepositoryEntry, GroupEntries } from "@scm-manager/ui-components";
|
|
||||||
import { RepositoryGroup } from "@scm-manager/ui-types";
|
import { RepositoryGroup } from "@scm-manager/ui-types";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
group: RepositoryGroup;
|
group: RepositoryGroup;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RepositoryGroupEntry: FC<Props> = ({ group }) => {
|
const RepositoryGroupEntry: FC<Props> = ({ group }) => {
|
||||||
const [t] = useTranslation("namespaces");
|
|
||||||
|
|
||||||
const settingsLink = group.namespace?._links?.permissions && (
|
|
||||||
<Link to={`/namespace/${group.name}/settings`} aria-label={t("repositoryOverview.settings.tooltip")}>
|
|
||||||
<Icon color="inherit" name="cog" title={t("repositoryOverview.settings.tooltip")} className="is-size-6 ml-2" />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
const namespaceHeader = (
|
|
||||||
<>
|
|
||||||
<Link to={`/repos/${group.name}/`} className="has-text-inherit">
|
|
||||||
{group.name}
|
|
||||||
</Link>{" "}
|
|
||||||
{settingsLink}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
const entries = group.repositories.map((repository, index) => {
|
const entries = group.repositories.map((repository, index) => {
|
||||||
return <RepositoryEntry repository={repository} key={index} />;
|
return <RepositoryEntry repository={repository} key={index} />;
|
||||||
});
|
});
|
||||||
return <GroupEntries namespaceHeader={namespaceHeader} elements={entries} />;
|
return <GroupEntries group={group} elements={entries} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RepositoryGroupEntry;
|
export default RepositoryGroupEntry;
|
||||||
|
|||||||
@@ -26,75 +26,75 @@ import groupByNamespace from "./groupByNamespace";
|
|||||||
|
|
||||||
const base = {
|
const base = {
|
||||||
type: "git",
|
type: "git",
|
||||||
_links: {}
|
_links: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const slartiBlueprintsFjords = {
|
const slartiBlueprintsFjords = {
|
||||||
...base,
|
...base,
|
||||||
namespace: "slarti",
|
namespace: "slarti",
|
||||||
name: "fjords-blueprints"
|
name: "fjords-blueprints",
|
||||||
};
|
};
|
||||||
|
|
||||||
const slartiFjords = {
|
const slartiFjords = {
|
||||||
...base,
|
...base,
|
||||||
namespace: "slarti",
|
namespace: "slarti",
|
||||||
name: "fjords"
|
name: "fjords",
|
||||||
};
|
};
|
||||||
|
|
||||||
const hitchhikerRestand = {
|
const hitchhikerRestand = {
|
||||||
...base,
|
...base,
|
||||||
namespace: "hitchhiker",
|
namespace: "hitchhiker",
|
||||||
name: "restand"
|
name: "restand",
|
||||||
};
|
};
|
||||||
const hitchhikerPuzzle42 = {
|
const hitchhikerPuzzle42 = {
|
||||||
...base,
|
...base,
|
||||||
namespace: "hitchhiker",
|
namespace: "hitchhiker",
|
||||||
name: "puzzle42"
|
name: "puzzle42",
|
||||||
};
|
};
|
||||||
|
|
||||||
const hitchhikerHeartOfGold = {
|
const hitchhikerHeartOfGold = {
|
||||||
...base,
|
...base,
|
||||||
namespace: "hitchhiker",
|
namespace: "hitchhiker",
|
||||||
name: "heartOfGold"
|
name: "heartOfGold",
|
||||||
};
|
};
|
||||||
|
|
||||||
const zaphodMarvinFirmware = {
|
const zaphodMarvinFirmware = {
|
||||||
...base,
|
...base,
|
||||||
namespace: "zaphod",
|
namespace: "zaphod",
|
||||||
name: "marvin-firmware"
|
name: "marvin-firmware",
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should group the repositories by their namespace", () => {
|
it("should group the repositories by their namespace", () => {
|
||||||
const repositories = [
|
const repositories = [
|
||||||
zaphodMarvinFirmware,
|
zaphodMarvinFirmware,
|
||||||
slartiBlueprintsFjords,
|
|
||||||
hitchhikerRestand,
|
|
||||||
slartiFjords,
|
slartiFjords,
|
||||||
|
slartiBlueprintsFjords,
|
||||||
hitchhikerHeartOfGold,
|
hitchhikerHeartOfGold,
|
||||||
hitchhikerPuzzle42
|
hitchhikerPuzzle42,
|
||||||
|
hitchhikerRestand,
|
||||||
];
|
];
|
||||||
const namespaces = {
|
const namespaces = {
|
||||||
_embedded: {
|
_embedded: {
|
||||||
namespaces: [{ namespace: "hitchhiker" }, { namespace: "slarti" }, { namespace: "zaphod" }]
|
namespaces: [{ namespace: "hitchhiker" }, { namespace: "slarti" }, { namespace: "zaphod" }],
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{
|
||||||
name: "hitchhiker",
|
name: "hitchhiker",
|
||||||
namespace: { namespace: "hitchhiker" },
|
namespace: { namespace: "hitchhiker" },
|
||||||
repositories: [hitchhikerHeartOfGold, hitchhikerPuzzle42, hitchhikerRestand]
|
repositories: [hitchhikerHeartOfGold, hitchhikerPuzzle42, hitchhikerRestand],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "slarti",
|
name: "slarti",
|
||||||
namespace: { namespace: "slarti" },
|
namespace: { namespace: "slarti" },
|
||||||
repositories: [slartiFjords, slartiBlueprintsFjords]
|
repositories: [slartiFjords, slartiBlueprintsFjords],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "zaphod",
|
name: "zaphod",
|
||||||
namespace: { namespace: "zaphod" },
|
namespace: { namespace: "zaphod" },
|
||||||
repositories: [zaphodMarvinFirmware]
|
repositories: [zaphodMarvinFirmware],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(groupByNamespace(repositories, namespaces)).toEqual(expected);
|
expect(groupByNamespace(repositories, namespaces)).toEqual(expected);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function groupByNamespace(
|
|||||||
group = {
|
group = {
|
||||||
name: groupName,
|
name: groupName,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
repositories: []
|
repositories: [],
|
||||||
};
|
};
|
||||||
groups[groupName] = group;
|
groups[groupName] = group;
|
||||||
}
|
}
|
||||||
@@ -47,8 +47,6 @@ export default function groupByNamespace(
|
|||||||
|
|
||||||
const groupArray = [];
|
const groupArray = [];
|
||||||
for (const groupName in groups) {
|
for (const groupName in groups) {
|
||||||
const group = groups[groupName];
|
|
||||||
group.repositories.sort(sortByName);
|
|
||||||
groupArray.push(groups[groupName]);
|
groupArray.push(groups[groupName]);
|
||||||
}
|
}
|
||||||
groupArray.sort(sortByName);
|
groupArray.sort(sortByName);
|
||||||
@@ -65,5 +63,5 @@ function sortByName(a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findNamespace(namespaces: NamespaceCollection, namespaceToFind: string) {
|
function findNamespace(namespaces: NamespaceCollection, namespaceToFind: string) {
|
||||||
return namespaces._embedded.namespaces.find(namespace => namespace.namespace === namespaceToFind);
|
return namespaces._embedded.namespaces.find((namespace) => namespace.namespace === namespaceToFind);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user