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:
Eduard Heimbuch
2023-08-22 19:59:53 +02:00
parent 9c0d064491
commit 2efcbfa759
11 changed files with 109 additions and 80 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Make namespaces collapsible and save the collapsed state in local storage

View File

@@ -0,0 +1,2 @@
- type: changed
description: Sort repositories alphanumerically per namespace

View File

@@ -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

View File

@@ -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";

View File

@@ -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] {

View File

@@ -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,
}; };
}; };

View File

@@ -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" />
</> </>
); );

View File

@@ -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";

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);
} }