Add cookie banner deletion from overview page

Wire the existing deleteCookieBanner GraphQL mutation to the
cookie banners overview list. Inactive banners with delete
permission show an action dropdown with a confirmed delete flow.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-24 18:23:41 +04:00
parent a1d2eebf2b
commit d9db93911b

View File

@@ -12,13 +12,25 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
import { formatError, type GraphQLError, sprintf } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks"; import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { Badge, Button, Card, PageHeader } from "@probo/ui"; import {
import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; ActionDropdown,
Badge,
Button,
Card,
DropdownItem,
IconTrashCan,
PageHeader,
useConfirm,
useToast,
} from "@probo/ui";
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
import { Link } from "react-router"; import { Link } from "react-router";
import { graphql } from "relay-runtime"; import { graphql } from "relay-runtime";
import type { CookieBannersOverviewPageDeleteMutation } from "#/__generated__/core/CookieBannersOverviewPageDeleteMutation.graphql";
import type { CookieBannersOverviewPageQuery } from "#/__generated__/core/CookieBannersOverviewPageQuery.graphql"; import type { CookieBannersOverviewPageQuery } from "#/__generated__/core/CookieBannersOverviewPageQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
@@ -29,7 +41,10 @@ export const cookieBannersOverviewPageQuery = graphql`
organization: node(id: $organizationId) { organization: node(id: $organizationId) {
__typename __typename
... on Organization { ... on Organization {
cookieBanners(first: 50, orderBy: { field: CREATED_AT, direction: DESC }) @required(action: THROW) { cookieBanners(first: 50, orderBy: { field: CREATED_AT, direction: DESC })
@connection(key: "CookieBannersOverviewPage_cookieBanners", filters: [])
@required(action: THROW) {
__id
edges { edges {
node { node {
id id
@@ -37,6 +52,7 @@ export const cookieBannersOverviewPageQuery = graphql`
origin origin
state state
createdAt createdAt
canDelete: permission(action: "core:cookie-banner:delete")
} }
} }
} }
@@ -45,6 +61,17 @@ export const cookieBannersOverviewPageQuery = graphql`
} }
`; `;
const deleteCookieBannerMutation = graphql`
mutation CookieBannersOverviewPageDeleteMutation(
$input: DeleteCookieBannerInput!
$connections: [ID!]!
) {
deleteCookieBanner(input: $input) {
deletedCookieBannerId @deleteEdge(connections: $connections)
}
}
`;
interface CookieBannersOverviewPageProps { interface CookieBannersOverviewPageProps {
queryRef: PreloadedQuery<CookieBannersOverviewPageQuery>; queryRef: PreloadedQuery<CookieBannersOverviewPageQuery>;
} }
@@ -52,6 +79,8 @@ interface CookieBannersOverviewPageProps {
export function CookieBannersOverviewPage({ queryRef }: CookieBannersOverviewPageProps) { export function CookieBannersOverviewPage({ queryRef }: CookieBannersOverviewPageProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { toast } = useToast();
const confirm = useConfirm();
usePageTitle(__("Cookie Banners")); usePageTitle(__("Cookie Banners"));
@@ -60,9 +89,61 @@ export function CookieBannersOverviewPage({ queryRef }: CookieBannersOverviewPag
throw new Error("invalid type for node"); throw new Error("invalid type for node");
} }
const connectionId = organization.cookieBanners.__id;
const banners = organization.cookieBanners.edges.map(e => e.node); const banners = organization.cookieBanners.edges.map(e => e.node);
const newBannerHref = `/organizations/${organizationId}/cookie-banners/new`; const newBannerHref = `/organizations/${organizationId}/cookie-banners/new`;
const [deleteCookieBanner] = useMutation<CookieBannersOverviewPageDeleteMutation>(deleteCookieBannerMutation);
const handleDelete = (bannerId: string, bannerName: string) => {
confirm(
() =>
new Promise<void>((resolve) => {
deleteCookieBanner({
variables: {
input: { cookieBannerId: bannerId },
connections: [connectionId],
},
onCompleted(_, errors) {
if (errors?.length) {
toast({
title: __("Error"),
description: errors[0].message,
variant: "error",
});
} else {
toast({
title: __("Success"),
description: __("Cookie banner deleted successfully"),
variant: "success",
});
}
resolve();
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to delete cookie banner"),
error as GraphQLError,
),
variant: "error",
});
resolve();
},
});
}),
{
message: sprintf(
__("This will permanently delete the cookie banner \"%s\". This action cannot be undone."),
bannerName,
),
variant: "danger",
label: __("Delete"),
},
);
};
if (banners.length === 0) { if (banners.length === 0) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -111,6 +192,19 @@ export function CookieBannersOverviewPage({ queryRef }: CookieBannersOverviewPag
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{new Date(banner.createdAt).toLocaleDateString()} {new Date(banner.createdAt).toLocaleDateString()}
</span> </span>
{banner.canDelete && banner.state !== "ACTIVE" && (
<div onClick={e => e.preventDefault()}>
<ActionDropdown>
<DropdownItem
onClick={() => handleDelete(banner.id, banner.name)}
variant="danger"
icon={IconTrashCan}
>
{__("Delete")}
</DropdownItem>
</ActionDropdown>
</div>
)}
</div> </div>
</Link> </Link>
))} ))}