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 { formatError, type GraphQLError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
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 { useState } from "react";
|
||||||
import { useFragment, useMutation } from "react-relay";
|
import { useFragment, useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -87,6 +87,7 @@ interface CategoryListProps {
|
|||||||
export function CategoryList({ cookieBannerKey }: CategoryListProps) {
|
export function CategoryList({ cookieBannerKey }: CategoryListProps) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const confirm = useConfirm();
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
|
|
||||||
const banner = useFragment(categoryListFragment, cookieBannerKey);
|
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 sorted = [...categories].sort((a, b) => a.rank - b.rank);
|
||||||
|
|
||||||
const handleDelete = (categoryId: string) => {
|
const handleDelete = (categoryId: string, categoryName: string) => {
|
||||||
|
confirm(
|
||||||
|
() =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
deleteCategory({
|
deleteCategory({
|
||||||
variables: {
|
variables: {
|
||||||
input: { cookieCategoryId: categoryId },
|
input: { cookieCategoryId: categoryId },
|
||||||
connections: [connectionId],
|
connections: [connectionId],
|
||||||
},
|
},
|
||||||
onCompleted() {
|
onCompleted(_, errors) {
|
||||||
|
if (errors?.length) {
|
||||||
|
toast({ title: __("Error"), description: errors[0].message, variant: "error" });
|
||||||
|
} else {
|
||||||
toast({ title: __("Success"), description: __("Category deleted"), variant: "success" });
|
toast({ title: __("Success"), description: __("Category deleted"), variant: "success" });
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({ title: __("Error"), description: formatError(__("Failed to delete category"), error as GraphQLError), variant: "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"),
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoveUp = (index: number) => {
|
const handleMoveUp = (index: number) => {
|
||||||
@@ -181,7 +198,7 @@ export function CategoryList({ cookieBannerKey }: CategoryListProps) {
|
|||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
className="h-6 px-2 text-xs"
|
className="h-6 px-2 text-xs"
|
||||||
onClick={() => handleDelete(category.id)}
|
onClick={() => handleDelete(category.id, category.name)}
|
||||||
>
|
>
|
||||||
{__("Delete")}
|
{__("Delete")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -12,12 +12,14 @@
|
|||||||
// 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 } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
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 { useState } from "react";
|
||||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
|
import type { CookieBannerCookiesPageDeleteMutation } from "#/__generated__/core/CookieBannerCookiesPageDeleteMutation.graphql";
|
||||||
import type { CookieBannerCookiesPageQuery } from "#/__generated__/core/CookieBannerCookiesPageQuery.graphql";
|
import type { CookieBannerCookiesPageQuery } from "#/__generated__/core/CookieBannerCookiesPageQuery.graphql";
|
||||||
|
|
||||||
import { CategoryDialog } from "../_components/CategoryDialog";
|
import { CategoryDialog } from "../_components/CategoryDialog";
|
||||||
@@ -38,6 +40,8 @@ export const cookieBannerCookiesPageQuery = graphql`
|
|||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
rank
|
rank
|
||||||
|
name
|
||||||
|
kind
|
||||||
...CategorySectionFragment
|
...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 {
|
interface CookieBannerCookiesPageProps {
|
||||||
queryRef: PreloadedQuery<CookieBannerCookiesPageQuery>;
|
queryRef: PreloadedQuery<CookieBannerCookiesPageQuery>;
|
||||||
}
|
}
|
||||||
@@ -55,6 +78,8 @@ export default function CookieBannerCookiesPage({
|
|||||||
queryRef,
|
queryRef,
|
||||||
}: CookieBannerCookiesPageProps) {
|
}: CookieBannerCookiesPageProps) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const confirm = useConfirm();
|
||||||
const data = usePreloadedQuery(cookieBannerCookiesPageQuery, queryRef);
|
const data = usePreloadedQuery(cookieBannerCookiesPageQuery, queryRef);
|
||||||
|
|
||||||
if (data.node.__typename !== "CookieBanner") {
|
if (data.node.__typename !== "CookieBanner") {
|
||||||
@@ -66,8 +91,57 @@ export default function CookieBannerCookiesPage({
|
|||||||
const categories = banner.categories.edges.map(e => e.node);
|
const categories = banner.categories.edges.map(e => e.node);
|
||||||
const sorted = [...categories].sort((a, b) => a.rank - b.rank);
|
const sorted = [...categories].sort((a, b) => a.rank - b.rank);
|
||||||
|
|
||||||
|
const [deleteCategory]
|
||||||
|
= useMutation<CookieBannerCookiesPageDeleteMutation>(deleteCategoryMutation);
|
||||||
|
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -85,7 +159,15 @@ export default function CookieBannerCookiesPage({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{sorted.map(category => (
|
{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 && (
|
{showCreateDialog && (
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
IconArrowBoxLeft,
|
||||||
IconPencil,
|
IconPencil,
|
||||||
IconPlusSmall,
|
IconPlusSmall,
|
||||||
IconTrashCan,
|
IconTrashCan,
|
||||||
@@ -48,6 +51,21 @@ export const categorySectionFragment = graphql`
|
|||||||
duration
|
duration
|
||||||
description
|
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 {
|
interface CategorySectionProps {
|
||||||
categoryKey: CategorySectionFragment$key;
|
categoryKey: CategorySectionFragment$key;
|
||||||
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CategorySection({ categoryKey }: CategorySectionProps) {
|
export function CategorySection({ categoryKey, onDelete }: CategorySectionProps) {
|
||||||
const category = useFragment(categorySectionFragment, categoryKey);
|
const category = useFragment(categorySectionFragment, categoryKey);
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -232,6 +251,95 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
|
|||||||
setCookieForm({ name: "", duration: "", description: "" });
|
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 (
|
return (
|
||||||
<Card className="border overflow-hidden">
|
<Card className="border overflow-hidden">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
@@ -273,6 +381,7 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
|
|||||||
<Badge variant="neutral">{__("Required")}</Badge>
|
<Badge variant="neutral">{__("Required")}</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => setIsEditingCategory(true)}
|
onClick={() => setIsEditingCategory(true)}
|
||||||
@@ -280,6 +389,13 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
|
|||||||
<IconPencil size={14} />
|
<IconPencil size={14} />
|
||||||
{__("Edit")}
|
{__("Edit")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{onDelete && (
|
||||||
|
<Button variant="danger" onClick={onDelete}>
|
||||||
|
<IconTrashCan size={14} />
|
||||||
|
{__("Delete")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isEditingCategory && (
|
{!isEditingCategory && (
|
||||||
@@ -378,14 +494,36 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleStartEditCookie(index)}
|
onClick={() => handleStartEditCookie(index)}
|
||||||
className="p-1 rounded cursor-pointer text-muted-foreground hover:text-foreground"
|
className="p-1 rounded cursor-pointer"
|
||||||
>
|
>
|
||||||
<IconPencil size={14} />
|
<IconPencil size={14} />
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDeleteCookie(index)}
|
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} />
|
<IconTrashCan size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { tv, type VariantProps } from "tailwind-variants";
|
|||||||
import { Slot } from "../Slot";
|
import { Slot } from "../Slot";
|
||||||
|
|
||||||
export const button = tv({
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
primary:
|
primary:
|
||||||
|
|||||||
Reference in New Issue
Block a user