Fix single tag deletion (#1700)

Redirect to tags overview after a single tag was deleted to prevent error page.

Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-06-16 14:45:40 +02:00
committed by GitHub
parent b9d5c3aa8d
commit ab2bd6d679
4 changed files with 32 additions and 21 deletions

View File

@@ -0,0 +1,2 @@
- type: fixed
description: Redirect after single tag was deleted ([#1700](https://github.com/scm-manager/scm-manager/pull/1700))

View File

@@ -239,14 +239,22 @@ describe("Test Tag hooks", () => {
expect(queryState!.isInvalidated).toBe(true); expect(queryState!.isInvalidated).toBe(true);
}; };
const shouldRemoveQuery = async (queryKey: string[], data: unknown) => {
queryClient.setQueryData(queryKey, data);
await deleteTag();
const queryState = queryClient.getQueryState(queryKey);
expect(queryState).toBeUndefined();
};
it("should delete tag", async () => { it("should delete tag", async () => {
const { isDeleted } = await deleteTag(); const { isDeleted } = await deleteTag();
expect(isDeleted).toBe(true); expect(isDeleted).toBe(true);
}); });
it("should invalidate tag cache", async () => { it("should delete tag cache", async () => {
await shouldInvalidateQuery(["repository", "hitchhiker", "heart-of-gold", "tag", "1.0"], tagOneDotZero); await shouldRemoveQuery(["repository", "hitchhiker", "heart-of-gold", "tag", "1.0"], tagOneDotZero);
}); });
it("should invalidate tag collection cache", async () => { it("should invalidate tag collection cache", async () => {

View File

@@ -37,7 +37,7 @@ export const useTags = (repository: Repository): ApiResult<TagCollection> => {
const link = requiredLink(repository, "tags"); const link = requiredLink(repository, "tags");
return useQuery<TagCollection, Error>( return useQuery<TagCollection, Error>(
repoQueryKey(repository, "tags"), repoQueryKey(repository, "tags"),
() => apiClient.get(link).then(response => response.json()) () => apiClient.get(link).then((response) => response.json())
// we do not populate the cache for a single tag, // we do not populate the cache for a single tag,
// because we have no pagination for tags and if we have a lot of them // because we have no pagination for tags and if we have a lot of them
// the population slows us down // the population slows us down
@@ -47,16 +47,15 @@ export const useTags = (repository: Repository): ApiResult<TagCollection> => {
export const useTag = (repository: Repository, name: string): ApiResult<Tag> => { export const useTag = (repository: Repository, name: string): ApiResult<Tag> => {
const link = requiredLink(repository, "tags"); const link = requiredLink(repository, "tags");
return useQuery<Tag, Error>(tagQueryKey(repository, name), () => return useQuery<Tag, Error>(tagQueryKey(repository, name), () =>
apiClient.get(concat(link, name)).then(response => response.json()) apiClient.get(concat(link, name)).then((response) => response.json())
); );
}; };
const invalidateCacheForTag = (queryClient: QueryClient, repository: NamespaceAndName, tag: Tag) => { const invalidateCacheForTag = (queryClient: QueryClient, repository: NamespaceAndName, tag: Tag) => {
return Promise.all([ return Promise.all([
queryClient.invalidateQueries(repoQueryKey(repository, "tags")), queryClient.invalidateQueries(repoQueryKey(repository, "tags")),
queryClient.invalidateQueries(tagQueryKey(repository, tag.name)),
queryClient.invalidateQueries(repoQueryKey(repository, "changesets")), queryClient.invalidateQueries(repoQueryKey(repository, "changesets")),
queryClient.invalidateQueries(repoQueryKey(repository, "changeset", tag.revision)) queryClient.invalidateQueries(repoQueryKey(repository, "changeset", tag.revision)),
]); ]);
}; };
@@ -65,16 +64,16 @@ const createTag = (changeset: Changeset, link: string) => {
return apiClient return apiClient
.post(link, { .post(link, {
name, name,
revision: changeset.id revision: changeset.id,
}) })
.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());
}; };
}; };
@@ -82,36 +81,38 @@ export const useCreateTag = (repository: Repository, changeset: Changeset) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const link = requiredLink(changeset, "tag"); const link = requiredLink(changeset, "tag");
const { isLoading, error, mutate, data } = useMutation<Tag, Error, string>(createTag(changeset, link), { const { isLoading, error, mutate, data } = useMutation<Tag, Error, string>(createTag(changeset, link), {
onSuccess: async tag => { onSuccess: async (tag) => {
queryClient.setQueryData(tagQueryKey(repository, tag.name), tag); queryClient.setQueryData(tagQueryKey(repository, tag.name), tag);
await queryClient.invalidateQueries(tagQueryKey(repository, tag.name));
await invalidateCacheForTag(queryClient, repository, tag); await invalidateCacheForTag(queryClient, repository, tag);
} },
}); });
return { return {
isLoading, isLoading,
error, error,
create: (name: string) => mutate(name), create: (name: string) => mutate(name),
tag: data tag: data,
}; };
}; };
export const useDeleteTag = (repository: Repository) => { export const useDeleteTag = (repository: Repository) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutate, isLoading, error, data } = useMutation<unknown, Error, Tag>( const { mutate, isLoading, error, data } = useMutation<unknown, Error, Tag>(
tag => { (tag) => {
const deleteUrl = (tag._links.delete as Link).href; const deleteUrl = (tag._links.delete as Link).href;
return apiClient.delete(deleteUrl); return apiClient.delete(deleteUrl);
}, },
{ {
onSuccess: async (_, tag) => { onSuccess: async (_, tag) => {
queryClient.removeQueries(tagQueryKey(repository, tag.name));
await invalidateCacheForTag(queryClient, repository, tag); await invalidateCacheForTag(queryClient, repository, tag);
} },
} }
); );
return { return {
remove: (tag: Tag) => mutate(tag), remove: (tag: Tag) => mutate(tag),
isLoading, isLoading,
error, error,
isDeleted: !!data isDeleted: !!data,
}; };
}; };

View File

@@ -23,10 +23,10 @@
*/ */
import React, { FC, useEffect, useState } from "react"; import React, { FC, useEffect, useState } from "react";
import { Link, Repository, Tag } from "@scm-manager/ui-types"; import { Repository, Tag } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import TagRow from "./TagRow"; import TagRow from "./TagRow";
import { apiClient, ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components"; import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
import { useDeleteTag } from "@scm-manager/ui-api"; import { useDeleteTag } from "@scm-manager/ui-api";
type Props = { type Props = {
@@ -77,12 +77,12 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
className: "is-outlined", className: "is-outlined",
label: t("tag.delete.confirmAlert.submit"), label: t("tag.delete.confirmAlert.submit"),
isLoading, isLoading,
onClick: () => deleteTag() onClick: () => deleteTag(),
}, },
{ {
label: t("tag.delete.confirmAlert.cancel"), label: t("tag.delete.confirmAlert.cancel"),
onClick: () => abortDelete() onClick: () => abortDelete(),
} },
]} ]}
close={() => abortDelete()} close={() => abortDelete()}
/> />
@@ -95,7 +95,7 @@ const TagTable: FC<Props> = ({ repository, baseUrl, tags }) => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{tags.map(tag => ( {tags.map((tag) => (
<TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} /> <TagRow key={tag.name} baseUrl={baseUrl} tag={tag} onDelete={onDelete} />
))} ))}
</tbody> </tbody>