Fix HalRepresentationWithEmbedded type (#1793)

Fix HalRepresentationWithEmbedded type since _embedded can be null.

Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-09-02 15:47:15 +02:00
committed by GitHub
parent 0ba8300051
commit 43e1ea06c8
16 changed files with 108 additions and 106 deletions

View File

@@ -0,0 +1,2 @@
- type: Fixed
description: Fix HalRepresentationWithEmbedded type ([#1793](https://github.com/scm-manager/scm-manager/pull/1793))

View File

@@ -51,7 +51,7 @@ const CloneInformation: FC<Props> = ({ url, repository }) => {
const error = changesetsError || defaultBranchError;
const branch = defaultBranchData?.defaultBranch;
const emptyRepository = changesets?._embedded.changesets.length === 0;
const emptyRepository = (changesets?._embedded?.changesets.length || 0) === 0;
return (
<div className="content">

View File

@@ -44,10 +44,10 @@ const ProtocolInformation: FC<Props> = ({ repository }) => {
return <Loading />;
}
const emptyRepository = data?._embedded.changesets.length === 0;
const emptyRepository = (data?._embedded?.changesets.length || 0) === 0;
return (
<div>
<div className="content">
<ErrorNotification error={error} />
<h4>{t("scm-hg-plugin.information.clone")}</h4>
<pre>

View File

@@ -38,7 +38,7 @@ class ProtocolInformation extends React.Component<Props> {
return null;
}
return (
<div>
<div className="content">
<h4>{t("scm-svn-plugin.information.checkout")}</h4>
<pre>
<code>svn checkout {href}</code>

View File

@@ -35,9 +35,9 @@ describe("Test changeset hooks", () => {
type: "hg",
_links: {
changesets: {
href: "/r/c"
}
}
href: "/r/c",
},
},
};
const develop: Branch = {
@@ -45,9 +45,9 @@ describe("Test changeset hooks", () => {
revision: "42",
_links: {
history: {
href: "/r/b/c"
}
}
href: "/r/b/c",
},
},
};
const changeset: Changeset = {
@@ -55,23 +55,23 @@ describe("Test changeset hooks", () => {
description: "Awesome change",
date: new Date(),
author: {
name: "Arthur Dent"
name: "Arthur Dent",
},
_embedded: {},
_links: {}
_links: {},
};
const changesets: ChangesetCollection = {
page: 1,
pageTotal: 1,
_embedded: {
changesets: [changeset]
changesets: [changeset],
},
_links: {}
_links: {},
};
const expectChangesetCollection = (result?: ChangesetCollection) => {
expect(result?._embedded.changesets[0].id).toBe(changesets._embedded.changesets[0].id);
expect(result?._embedded?.changesets[0].id).toBe(changesets._embedded?.changesets[0].id);
};
afterEach(() => {
@@ -85,7 +85,7 @@ describe("Test changeset hooks", () => {
const queryClient = createInfiniteCachingClient();
const { result, waitFor } = renderHook(() => useChangesets(repository), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {
@@ -98,14 +98,14 @@ describe("Test changeset hooks", () => {
it("should return changesets for page", async () => {
fetchMock.getOnce("/api/v2/r/c", changesets, {
query: {
page: 42
}
page: 42,
},
});
const queryClient = createInfiniteCachingClient();
const { result, waitFor } = renderHook(() => useChangesets(repository, { page: 42 }), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {
@@ -121,7 +121,7 @@ describe("Test changeset hooks", () => {
const queryClient = createInfiniteCachingClient();
const { result, waitFor } = renderHook(() => useChangesets(repository, { branch: develop }), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {
@@ -137,7 +137,7 @@ describe("Test changeset hooks", () => {
const queryClient = createInfiniteCachingClient();
const { result, waitFor } = renderHook(() => useChangesets(repository), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {
@@ -149,7 +149,7 @@ describe("Test changeset hooks", () => {
"hitchhiker",
"heart-of-gold",
"changeset",
"42"
"42",
]);
expect(changeset?.id).toBe("42");
@@ -163,7 +163,7 @@ describe("Test changeset hooks", () => {
const queryClient = createInfiniteCachingClient();
const { result, waitFor } = renderHook(() => useChangeset(repository, "42"), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {

View File

@@ -67,7 +67,7 @@ export const useChangesets = (
const key = branchQueryKey(repository, branch, "changesets", request?.page || 0);
return useQuery<ChangesetCollection, Error>(key, () => apiClient.get(link).then((response) => response.json()), {
onSuccess: (changesetCollection) => {
changesetCollection._embedded.changesets.forEach((changeset) => {
changesetCollection._embedded?.changesets.forEach((changeset) => {
queryClient.setQueryData(changesetQueryKey(repository, changeset.id), changeset);
});
},

View File

@@ -53,7 +53,7 @@ export const useHistory = (
{
keepPreviousData: true,
onSuccess: (changesets: ChangesetCollection) => {
changesets._embedded.changesets.forEach((changeset: Changeset) =>
changesets._embedded?.changesets.forEach((changeset: Changeset) =>
queryClient.setQueryData(changesetQueryKey(repository, changeset.id), changeset)
);
},

View File

@@ -34,21 +34,21 @@ export const useNotifications = () => {
const link = (me?._links["notifications"] as Link)?.href;
const { data, error, isLoading, refetch } = useQuery<NotificationCollection, Error>(
"notifications",
() => apiClient.get(link).then(response => response.json()),
() => apiClient.get(link).then((response) => response.json()),
{
enabled: !!link
enabled: !!link,
}
);
const memoizedRefetch = useCallback(() => {
return refetch().then(r => r.data);
return refetch().then((r) => r.data);
}, [refetch]);
return {
data,
error,
isLoading,
refetch: memoizedRefetch
refetch: memoizedRefetch,
};
};
@@ -58,13 +58,13 @@ export const useDismissNotification = (notification: Notification) => {
const { data, isLoading, error, mutate } = useMutation<Response, Error>(() => apiClient.delete(link), {
onSuccess: () => {
queryClient.invalidateQueries("notifications");
}
},
});
return {
isLoading,
error,
dismiss: () => mutate(),
isCleared: !!data
isCleared: !!data,
};
};
@@ -74,13 +74,13 @@ export const useClearNotifications = (notificationCollection: NotificationCollec
const { data, isLoading, error, mutate } = useMutation<Response, Error>(() => apiClient.delete(link), {
onSuccess: () => {
queryClient.invalidateQueries("notifications");
}
},
});
return {
isLoading,
error,
clear: () => mutate(),
isCleared: !!data
isCleared: !!data,
};
};
@@ -99,13 +99,13 @@ export const useNotificationSubscription = (
const onVisible = useCallback(() => {
// we don't need to catch the error,
// because if the refetch throws an error the parent useNotifications should catch it
refetch().then(collection => {
refetch().then((collection) => {
if (collection) {
const newNotifications = collection._embedded.notifications.filter(n => {
const newNotifications = collection._embedded?.notifications.filter((n) => {
return disconnectedAt && disconnectedAt < new Date(n.createdAt);
});
if (newNotifications.length > 0) {
setNotifications(previous => [...previous, ...newNotifications]);
if (newNotifications && newNotifications.length > 0) {
setNotifications((previous) => [...previous, ...newNotifications]);
}
setDisconnectedAt(undefined);
}
@@ -118,7 +118,7 @@ export const useNotificationSubscription = (
const received = useCallback(
(notification: Notification) => {
setNotifications(previous => [...previous, notification]);
setNotifications((previous) => [...previous, notification]);
refetch();
},
[refetch]
@@ -137,9 +137,9 @@ export const useNotificationSubscription = (
const connect = () => {
disconnect();
cancel = apiClient.subscribe(link, {
notification: event => {
notification: (event) => {
received(JSON.parse(event.data));
}
},
});
};
@@ -166,7 +166,7 @@ export const useNotificationSubscription = (
const remove = useCallback(
(notification: Notification) => {
setNotifications(oldNotifications => [...oldNotifications.filter(n => !isEqual(n, notification))]);
setNotifications((oldNotifications) => [...oldNotifications.filter((n) => !isEqual(n, notification))]);
},
[setNotifications]
);
@@ -178,6 +178,6 @@ export const useNotificationSubscription = (
return {
notifications,
remove,
clear
clear,
};
};

View File

@@ -30,7 +30,7 @@ import {
Repository,
RepositoryRole,
RepositoryRoleCollection,
RepositoryVerbs
RepositoryVerbs,
} from "@scm-manager/ui-types";
import fetchMock from "fetch-mock-jest";
import { renderHook } from "@testing-library/react-hooks";
@@ -41,7 +41,7 @@ import {
useDeletePermission,
usePermissions,
useRepositoryVerbs,
useUpdatePermission
useUpdatePermission,
} from "./permissions";
import { act } from "react-test-renderer";
@@ -49,21 +49,21 @@ describe("permission hooks test", () => {
const readRole: RepositoryRole = {
name: "READ",
verbs: ["read", "pull"],
_links: {}
_links: {},
};
const roleCollection: RepositoryRoleCollection = {
_embedded: {
repositoryRoles: [readRole]
repositoryRoles: [readRole],
},
_links: {},
page: 1,
pageTotal: 1
pageTotal: 1,
};
const verbCollection: RepositoryVerbs = {
verbs: ["read", "pull"],
_links: {}
_links: {},
};
const readPermission: Permission = {
@@ -73,9 +73,9 @@ describe("permission hooks test", () => {
groupPermission: false,
_links: {
update: {
href: "/p/trillian"
}
}
href: "/p/trillian",
},
},
};
const writePermission: Permission = {
@@ -85,32 +85,32 @@ describe("permission hooks test", () => {
groupPermission: false,
_links: {
delete: {
href: "/p/dent"
}
}
href: "/p/dent",
},
},
};
const permissionsRead: PermissionCollection = {
_embedded: {
permissions: [readPermission]
permissions: [readPermission],
},
_links: {}
_links: {},
};
const permissionsWrite: PermissionCollection = {
_embedded: {
permissions: [writePermission]
permissions: [writePermission],
},
_links: {}
_links: {},
};
const namespace: Namespace = {
namespace: "spaceships",
_links: {
permissions: {
href: "/ns/spaceships/permissions"
}
}
href: "/ns/spaceships/permissions",
},
},
};
const repository: Repository = {
@@ -119,9 +119,9 @@ describe("permission hooks test", () => {
type: "git",
_links: {
permissions: {
href: "/r/heart-of-gold/permissions"
}
}
href: "/r/heart-of-gold/permissions",
},
},
};
const queryClient = createInfiniteCachingClient();
@@ -137,7 +137,7 @@ describe("permission hooks test", () => {
fetchMock.get("/api/v2/verbs", verbCollection);
const { result, waitFor } = renderHook(() => useRepositoryVerbs(), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {
return !!result.current.data;
@@ -152,23 +152,23 @@ describe("permission hooks test", () => {
version: "x.y.z",
_links: {
repositoryRoles: {
href: "/roles"
href: "/roles",
},
repositoryVerbs: {
href: "/verbs"
}
}
href: "/verbs",
},
},
});
fetchMock.get("/api/v2/roles", roleCollection);
fetchMock.get("/api/v2/verbs", verbCollection);
const { result, waitFor } = renderHook(() => useAvailablePermissions(), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {
return !!result.current.data;
});
expect(result.current.data?.repositoryRoles).toEqual(roleCollection._embedded.repositoryRoles);
expect(result.current.data?.repositoryRoles).toEqual(roleCollection._embedded?.repositoryRoles);
expect(result.current.data?.repositoryVerbs).toEqual(verbCollection.verbs);
});
});
@@ -176,7 +176,7 @@ describe("permission hooks test", () => {
describe("usePermissions tests", () => {
const fetchPermissions = async (namespaceOrRepository: Namespace | Repository) => {
const { result, waitFor } = renderHook(() => usePermissions(namespaceOrRepository), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await waitFor(() => {
return !!result.current.data;
@@ -216,14 +216,14 @@ describe("permission hooks test", () => {
fetchMock.postOnce("/api/v2/ns/spaceships/permissions", {
status: 201,
headers: {
Location: "/ns/spaceships/permissions/42"
}
Location: "/ns/spaceships/permissions/42",
},
});
fetchMock.getOnce("/api/v2/ns/spaceships/permissions/42", readPermission);
const { result, waitForNextUpdate } = renderHook(() => useCreatePermission(namespace), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await act(() => {
@@ -241,11 +241,11 @@ describe("permission hooks test", () => {
it("should fail without location header", async () => {
fetchMock.postOnce("/api/v2/ns/spaceships/permissions", {
status: 201
status: 201,
});
const { result, waitForNextUpdate } = renderHook(() => useCreatePermission(namespace), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await act(() => {
@@ -270,11 +270,11 @@ describe("permission hooks test", () => {
describe("useDeletePermission tests", () => {
const deletePermission = async () => {
fetchMock.deleteOnce("/api/v2/p/dent", {
status: 204
status: 204,
});
const { result, waitForNextUpdate } = renderHook(() => useDeletePermission(repository), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await act(() => {
@@ -308,11 +308,11 @@ describe("permission hooks test", () => {
describe("useUpdatePermission tests", () => {
const updatePermission = async () => {
fetchMock.putOnce("/api/v2/p/trillian", {
status: 204
status: 204,
});
const { result, waitForNextUpdate } = renderHook(() => useUpdatePermission(repository), {
wrapper: createWrapper(undefined, queryClient)
wrapper: createWrapper(undefined, queryClient),
});
await act(() => {

View File

@@ -53,7 +53,7 @@ export const useAvailablePermissions = () => {
if (roles.data && verbs.data) {
data = {
repositoryVerbs: verbs.data.verbs,
repositoryRoles: roles.data._embedded.repositoryRoles,
repositoryRoles: roles.data._embedded?.repositoryRoles || [],
};
}

View File

@@ -308,7 +308,7 @@ describe("Test repository hooks", () => {
});
expect(result.current.data).toBeDefined();
if (result.current?.data) {
expect(result.current?.data._embedded.repositoryTypes[0].name).toEqual("git");
expect(result.current?.data._embedded?.repositoryTypes[0].name).toEqual("git");
}
});
});

View File

@@ -71,7 +71,7 @@ export const useRepositories = (request?: UseRepositoriesRequest): ApiResult<Rep
enabled: !request?.disabled,
onSuccess: (repositories: RepositoryCollection) => {
// prepare single repository cache
repositories._embedded.repositories.forEach((repository: Repository) => {
repositories._embedded?.repositories.forEach((repository: Repository) => {
queryClient.setQueryData(["repository", repository.namespace, repository.name], repository);
});
},
@@ -363,7 +363,7 @@ export const useRenameRepository = (repository: Repository) => {
const { mutate, isLoading, error, data } = useMutation<unknown, Error, RenameRepositoryRequest>(
({ name, namespace }) => apiClient.post(url, { namespace, name }, "application/vnd.scmm-repository+json;v=2"),
{
onSuccess: () => queryClient.removeQueries(repoQueryKey(repository))
onSuccess: () => queryClient.removeQueries(repoQueryKey(repository)),
}
);

View File

@@ -43,13 +43,13 @@ export const useRepositoryRoles = (request?: UseRepositoryRolesRequest): ApiResu
return useQuery<RepositoryRoleCollection, Error>(
["repositoryRoles", request?.page || 0],
() => apiClient.get(`${indexLink}?${createQueryString(queryParams)}`).then(response => response.json()),
() => apiClient.get(`${indexLink}?${createQueryString(queryParams)}`).then((response) => response.json()),
{
onSuccess: (repositoryRoles: RepositoryRoleCollection) => {
repositoryRoles._embedded.repositoryRoles.forEach((repositoryRole: RepositoryRole) =>
repositoryRoles._embedded?.repositoryRoles.forEach((repositoryRole: RepositoryRole) =>
queryClient.setQueryData(["repositoryRole", repositoryRole.name], repositoryRole)
);
}
},
}
);
};
@@ -57,7 +57,7 @@ export const useRepositoryRoles = (request?: UseRepositoryRolesRequest): ApiResu
export const useRepositoryRole = (name: string): ApiResult<RepositoryRole> => {
const indexLink = useRequiredIndexLink("repositoryRoles");
return useQuery<RepositoryRole, Error>(["repositoryRole", name], () =>
apiClient.get(urls.concat(indexLink, name)).then(response => response.json())
apiClient.get(urls.concat(indexLink, name)).then((response) => response.json())
);
};
@@ -65,14 +65,14 @@ const createRepositoryRole = (link: string) => {
return (repositoryRole: RepositoryRoleCreation) => {
return apiClient
.post(link, repositoryRole, "application/vnd.scmm-repositoryRole+json;v=2")
.then(response => {
.then((response) => {
const location = response.headers.get("Location");
if (!location) {
throw new Error("Server does not return required Location header");
}
return apiClient.get(location);
})
.then(response => response.json());
.then((response) => response.json());
};
};
@@ -82,24 +82,24 @@ export const useCreateRepositoryRole = () => {
const { mutate, data, isLoading, error } = useMutation<RepositoryRole, Error, RepositoryRoleCreation>(
createRepositoryRole(link),
{
onSuccess: repositoryRole => {
onSuccess: (repositoryRole) => {
queryClient.setQueryData(["repositoryRole", repositoryRole.name], repositoryRole);
return queryClient.invalidateQueries(["repositoryRoles"]);
}
},
}
);
return {
create: (repositoryRole: RepositoryRoleCreation) => mutate(repositoryRole),
isLoading,
error,
repositoryRole: data
repositoryRole: data,
};
};
export const useUpdateRepositoryRole = () => {
const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, RepositoryRole>(
repositoryRole => {
(repositoryRole) => {
const updateUrl = requiredLink(repositoryRole, "update");
return apiClient.put(updateUrl, repositoryRole, "application/vnd.scmm-repositoryRole+json;v=2");
},
@@ -107,21 +107,21 @@ export const useUpdateRepositoryRole = () => {
onSuccess: async (_, repositoryRole) => {
await queryClient.invalidateQueries(["repositoryRole", repositoryRole.name]);
await queryClient.invalidateQueries(["repositoryRoles"]);
}
},
}
);
return {
update: (repositoryRole: RepositoryRole) => mutate(repositoryRole),
isLoading,
error,
isUpdated: !!data
isUpdated: !!data,
};
};
export const useDeleteRepositoryRole = () => {
const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, RepositoryRole>(
repositoryRole => {
(repositoryRole) => {
const deleteUrl = requiredLink(repositoryRole, "delete");
return apiClient.delete(deleteUrl);
},
@@ -129,13 +129,13 @@ export const useDeleteRepositoryRole = () => {
onSuccess: async (_, name) => {
await queryClient.invalidateQueries(["repositoryRole", name]);
await queryClient.invalidateQueries(["repositoryRoles"]);
}
},
}
);
return {
remove: (repositoryRole: RepositoryRole) => mutate(repositoryRole),
isLoading,
error,
isDeleted: !!data
isDeleted: !!data,
};
};

View File

@@ -24,7 +24,7 @@
import { ApiResult, useRequiredIndexLink } from "./base";
import { useMutation, useQuery, useQueryClient } from "react-query";
import {Link, Me, User, UserCollection, UserCreation} from "@scm-manager/ui-types";
import { Link, Me, User, UserCollection, UserCreation } from "@scm-manager/ui-types";
import { apiClient } from "./apiclient";
import { createQueryString } from "./utils";
import { concat } from "./urls";
@@ -52,7 +52,7 @@ export const useUsers = (request?: UseUsersRequest): ApiResult<UserCollection> =
() => apiClient.get(`${indexLink}?${createQueryString(queryParams)}`).then((response) => response.json()),
{
onSuccess: (users: UserCollection) => {
users._embedded.users.forEach((user: User) => queryClient.setQueryData(["user", user.name], user));
users._embedded?.users.forEach((user: User) => queryClient.setQueryData(["user", user.name], user));
},
}
);
@@ -215,7 +215,7 @@ export const useSetUserPassword = (user: User) => {
passwordOverwritten: !!data,
isLoading,
error,
reset
reset,
};
};
@@ -235,6 +235,6 @@ export const useChangeUserPassword = (user: User | Me) => {
passwordChanged: !!data,
isLoading,
error,
reset
reset,
};
};

View File

@@ -33,7 +33,7 @@ type Props = {
class ChangesetTags extends React.Component<Props> {
getTags = () => {
const { changeset } = this.props;
return changeset._embedded.tags || [];
return changeset._embedded?.tags || [];
};
render() {

View File

@@ -44,7 +44,7 @@ export type HalRepresentation = {
};
export type HalRepresentationWithEmbedded<T extends Embedded> = HalRepresentation & {
_embedded: T;
_embedded?: T;
};
export type PagedCollection<T extends Embedded = Embedded> = HalRepresentationWithEmbedded<T> & {