mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-11 07:55:47 +01:00
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:
2
gradle/changelog/delete_tag_with_redirect.yaml
Normal file
2
gradle/changelog/delete_tag_with_redirect.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: fixed
|
||||||
|
description: Redirect after single tag was deleted ([#1700](https://github.com/scm-manager/scm-manager/pull/1700))
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user