Use fragment and add confirm
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Badge, Button, Card, IconArrowDown, IconArrowUp, useToast } from "@probo/ui";
|
||||
import { Badge, Button, Card, IconArrowDown, IconArrowUp, useConfirm, useToast } from "@probo/ui";
|
||||
import { useState } from "react";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
@@ -87,6 +87,7 @@ interface CategoryListProps {
|
||||
export function CategoryList({ cookieBannerKey }: CategoryListProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const confirm = useConfirm();
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
||||
const banner = useFragment(categoryListFragment, cookieBannerKey);
|
||||
@@ -98,19 +99,35 @@ export function CategoryList({ cookieBannerKey }: CategoryListProps) {
|
||||
|
||||
const sorted = [...categories].sort((a, b) => a.rank - b.rank);
|
||||
|
||||
const handleDelete = (categoryId: string) => {
|
||||
deleteCategory({
|
||||
variables: {
|
||||
input: { cookieCategoryId: categoryId },
|
||||
connections: [connectionId],
|
||||
const handleDelete = (categoryId: string, categoryName: string) => {
|
||||
confirm(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
deleteCategory({
|
||||
variables: {
|
||||
input: { cookieCategoryId: categoryId },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted(_, errors) {
|
||||
if (errors?.length) {
|
||||
toast({ title: __("Error"), description: errors[0].message, variant: "error" });
|
||||
} else {
|
||||
toast({ title: __("Success"), description: __("Category deleted"), variant: "success" });
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
onError(error) {
|
||||
toast({ title: __("Error"), description: formatError(__("Failed to delete category"), error as GraphQLError), variant: "error" });
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
}),
|
||||
{
|
||||
message: __("Are you sure you want to delete the category \"%s\"? Any cookies in this category will be moved to Uncategorised.").replace("%s", categoryName),
|
||||
variant: "danger",
|
||||
label: __("Delete"),
|
||||
},
|
||||
onCompleted() {
|
||||
toast({ title: __("Success"), description: __("Category deleted"), variant: "success" });
|
||||
},
|
||||
onError(error) {
|
||||
toast({ title: __("Error"), description: formatError(__("Failed to delete category"), error as GraphQLError), variant: "error" });
|
||||
},
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const handleMoveUp = (index: number) => {
|
||||
@@ -181,7 +198,7 @@ export function CategoryList({ cookieBannerKey }: CategoryListProps) {
|
||||
<Button
|
||||
variant="danger"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={() => handleDelete(category.id)}
|
||||
onClick={() => handleDelete(category.id, category.name)}
|
||||
>
|
||||
{__("Delete")}
|
||||
</Button>
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, Card, IconPlusSmall } from "@probo/ui";
|
||||
import { Button, Card, IconPlusSmall, useConfirm, useToast } from "@probo/ui";
|
||||
import { useState } from "react";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { CookieBannerCookiesPageDeleteMutation } from "#/__generated__/core/CookieBannerCookiesPageDeleteMutation.graphql";
|
||||
import type { CookieBannerCookiesPageQuery } from "#/__generated__/core/CookieBannerCookiesPageQuery.graphql";
|
||||
|
||||
import { CategoryDialog } from "../_components/CategoryDialog";
|
||||
@@ -38,6 +40,8 @@ export const cookieBannerCookiesPageQuery = graphql`
|
||||
node {
|
||||
id
|
||||
rank
|
||||
name
|
||||
kind
|
||||
...CategorySectionFragment
|
||||
}
|
||||
}
|
||||
@@ -47,6 +51,25 @@ export const cookieBannerCookiesPageQuery = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const deleteCategoryMutation = graphql`
|
||||
mutation CookieBannerCookiesPageDeleteMutation(
|
||||
$input: DeleteCookieCategoryInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
deleteCookieCategory(input: $input) {
|
||||
deletedCookieCategoryId @deleteEdge(connections: $connections)
|
||||
cookieBanner {
|
||||
id
|
||||
latestVersion {
|
||||
id
|
||||
version
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface CookieBannerCookiesPageProps {
|
||||
queryRef: PreloadedQuery<CookieBannerCookiesPageQuery>;
|
||||
}
|
||||
@@ -55,6 +78,8 @@ export default function CookieBannerCookiesPage({
|
||||
queryRef,
|
||||
}: CookieBannerCookiesPageProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const confirm = useConfirm();
|
||||
const data = usePreloadedQuery(cookieBannerCookiesPageQuery, queryRef);
|
||||
|
||||
if (data.node.__typename !== "CookieBanner") {
|
||||
@@ -66,8 +91,57 @@ export default function CookieBannerCookiesPage({
|
||||
const categories = banner.categories.edges.map(e => e.node);
|
||||
const sorted = [...categories].sort((a, b) => a.rank - b.rank);
|
||||
|
||||
const [deleteCategory]
|
||||
= useMutation<CookieBannerCookiesPageDeleteMutation>(deleteCategoryMutation);
|
||||
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
|
||||
const handleDeleteCategory = (categoryId: string, categoryName: string) => {
|
||||
confirm(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
deleteCategory({
|
||||
variables: {
|
||||
input: { cookieCategoryId: categoryId },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted(_, errors) {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: errors[0].message,
|
||||
variant: "error",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Category deleted"),
|
||||
variant: "success",
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to delete category"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
}),
|
||||
{
|
||||
message: __("Are you sure you want to delete the category \"%s\"? Any cookies in this category will be moved to Uncategorised.").replace("%s", categoryName),
|
||||
variant: "danger",
|
||||
label: __("Delete"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -85,7 +159,15 @@ export default function CookieBannerCookiesPage({
|
||||
)}
|
||||
|
||||
{sorted.map(category => (
|
||||
<CategorySection key={category.id} categoryKey={category} />
|
||||
<CategorySection
|
||||
key={category.id}
|
||||
categoryKey={category}
|
||||
onDelete={
|
||||
category.kind === "NORMAL"
|
||||
? () => handleDeleteCategory(category.id, category.name)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{showCreateDialog && (
|
||||
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
IconArrowBoxLeft,
|
||||
IconPencil,
|
||||
IconPlusSmall,
|
||||
IconTrashCan,
|
||||
@@ -48,6 +51,21 @@ export const categorySectionFragment = graphql`
|
||||
duration
|
||||
description
|
||||
}
|
||||
cookieBanner @required(action: THROW) {
|
||||
categories(first: 50, orderBy: { field: RANK, direction: ASC }) @required(action: THROW) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
cookies {
|
||||
name
|
||||
duration
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -88,9 +106,10 @@ interface CookieEntry {
|
||||
|
||||
interface CategorySectionProps {
|
||||
categoryKey: CategorySectionFragment$key;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export function CategorySection({ categoryKey }: CategorySectionProps) {
|
||||
export function CategorySection({ categoryKey, onDelete }: CategorySectionProps) {
|
||||
const category = useFragment(categorySectionFragment, categoryKey);
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
@@ -232,6 +251,95 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
|
||||
setCookieForm({ name: "", duration: "", description: "" });
|
||||
};
|
||||
|
||||
const allCategories = category.cookieBanner.categories.edges.map(e => e.node) ?? [];
|
||||
const siblingCategories = allCategories.filter(c => c.id !== category.id);
|
||||
|
||||
const handleMoveCookie = (cookieIndex: number, targetCategoryId: string) => {
|
||||
const cookie = category.cookies[cookieIndex];
|
||||
const targetCategory = siblingCategories.find(c => c.id === targetCategoryId);
|
||||
if (!targetCategory) return;
|
||||
|
||||
const targetCookies = [
|
||||
...targetCategory.cookies.map(c => ({
|
||||
name: c.name,
|
||||
duration: c.duration,
|
||||
description: c.description,
|
||||
})),
|
||||
{ name: cookie.name, duration: cookie.duration, description: cookie.description },
|
||||
];
|
||||
|
||||
updateCategory({
|
||||
variables: {
|
||||
input: {
|
||||
cookieCategoryId: targetCategoryId,
|
||||
cookies: targetCookies,
|
||||
},
|
||||
},
|
||||
onCompleted(_response, errors) {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: errors[0].message,
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCookies = category.cookies
|
||||
.filter((_, i) => i !== cookieIndex)
|
||||
.map(c => ({
|
||||
name: c.name,
|
||||
duration: c.duration,
|
||||
description: c.description,
|
||||
}));
|
||||
|
||||
updateCategory({
|
||||
variables: {
|
||||
input: {
|
||||
cookieCategoryId: category.id,
|
||||
cookies: sourceCookies,
|
||||
},
|
||||
},
|
||||
onCompleted(_response, errors) {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: errors[0].message,
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Cookie moved"),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to move cookie"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(
|
||||
__("Failed to move cookie"),
|
||||
error as GraphQLError,
|
||||
),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border overflow-hidden">
|
||||
<div className="p-4">
|
||||
@@ -273,13 +381,21 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
|
||||
<Badge variant="neutral">{__("Required")}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsEditingCategory(true)}
|
||||
>
|
||||
<IconPencil size={14} />
|
||||
{__("Edit")}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsEditingCategory(true)}
|
||||
>
|
||||
<IconPencil size={14} />
|
||||
{__("Edit")}
|
||||
</Button>
|
||||
{onDelete && (
|
||||
<Button variant="danger" onClick={onDelete}>
|
||||
<IconTrashCan size={14} />
|
||||
{__("Delete")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isEditingCategory && (
|
||||
@@ -378,14 +494,36 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleStartEditCookie(index)}
|
||||
className="p-1 rounded cursor-pointer text-muted-foreground hover:text-foreground"
|
||||
className="p-1 rounded cursor-pointer"
|
||||
>
|
||||
<IconPencil size={14} />
|
||||
</button>
|
||||
{siblingCategories.length > 0 && (
|
||||
<Dropdown
|
||||
toggle={(
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded cursor-pointer"
|
||||
>
|
||||
<IconArrowBoxLeft size={14} />
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
{siblingCategories.map(cat => (
|
||||
<DropdownItem
|
||||
className="text-sm"
|
||||
key={cat.id}
|
||||
onSelect={() => handleMoveCookie(index, cat.id)}
|
||||
>
|
||||
{cat.name}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDeleteCookie(index)}
|
||||
className="p-1 rounded cursor-pointer text-muted-foreground hover:text-red-500"
|
||||
className="p-1 rounded cursor-pointer text-danger-dark"
|
||||
>
|
||||
<IconTrashCan size={14} />
|
||||
</button>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { tv, type VariantProps } from "tailwind-variants";
|
||||
import { Slot } from "../Slot";
|
||||
|
||||
export const button = tv({
|
||||
base: "flex items-center justify-center gap-[6px] px-3 py-2 rounded-full cursor-pointer text-sm font-medium h-8 focus:outline-none whitespace-nowrap w-max",
|
||||
base: "flex items-center justify-center gap-[6px] px-3 py-2 rounded-lg cursor-pointer text-sm font-medium h-8 focus:outline-none whitespace-nowrap w-max",
|
||||
variants: {
|
||||
variant: {
|
||||
primary:
|
||||
|
||||
Reference in New Issue
Block a user