Create a db table for cookies for easiest management

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-21 13:25:43 +04:00
parent f743c743c1
commit 29c0d55e75
18 changed files with 1485 additions and 346 deletions

View File

@@ -43,11 +43,6 @@ const createMutation = graphql`
description
kind
rank
cookies {
name
duration
description
}
createdAt
updatedAt
}

View File

@@ -31,22 +31,18 @@ import { useState } from "react";
import { useMutation } from "react-relay";
import { graphql } from "relay-runtime";
import type { CookieDialogUpdateMutation } from "#/__generated__/core/CookieDialogUpdateMutation.graphql";
import type { CookieDialogCreateMutation } from "#/__generated__/core/CookieDialogCreateMutation.graphql";
const updateCategoryMutation = graphql`
mutation CookieDialogUpdateMutation($input: UpdateCookieCategoryInput!) {
updateCookieCategory(input: $input) {
cookieCategory {
id
name
description
rank
cookies {
const createCookieMutation = graphql`
mutation CookieDialogCreateMutation($input: CreateCookieInput!) {
createCookie(input: $input) {
cookieEdge {
node {
id
name
duration
description
}
updatedAt
}
cookieBanner {
id
@@ -60,16 +56,9 @@ const updateCategoryMutation = graphql`
}
`;
interface CookieEntry {
name: string;
duration: string;
description: string;
}
interface Category {
id: string;
name: string;
cookies: ReadonlyArray<CookieEntry>;
}
interface CookieDialogProps {
@@ -82,7 +71,7 @@ export function CookieDialog({ categories, onOpenChange }: CookieDialogProps) {
const { toast } = useToast();
const dialogRef = useDialogRef();
const [updateCategory, isUpdating] = useMutation<CookieDialogUpdateMutation>(updateCategoryMutation);
const [createCookie, isCreating] = useMutation<CookieDialogCreateMutation>(createCookieMutation);
const [categoryId, setCategoryId] = useState(categories[0]?.id ?? "");
const [name, setName] = useState("");
@@ -92,30 +81,29 @@ export function CookieDialog({ categories, onOpenChange }: CookieDialogProps) {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const category = categories.find(c => c.id === categoryId);
if (!category) return;
const existingCookies = category.cookies.map(c => ({
name: c.name,
duration: c.duration,
description: c.description,
}));
updateCategory({
createCookie({
variables: {
input: {
cookieCategoryId: categoryId,
cookies: [
...existingCookies,
{
name: name.trim(),
duration: duration.trim(),
description: description.trim(),
},
],
name: name.trim(),
duration: duration.trim(),
description: description.trim(),
},
},
onCompleted() {
onCompleted(_response, errors) {
if (errors?.length) {
const isConflict = errors.some(
e => (e as unknown as GraphQLError).extensions?.code === "CONFLICT",
);
toast({
title: __("Error"),
description: isConflict
? __("A cookie with this name already exists in this banner")
: errors[0].message,
variant: "error",
});
return;
}
toast({ title: __("Success"), description: __("Cookie added"), variant: "success" });
dialogRef.current?.close();
},
@@ -163,8 +151,8 @@ export function CookieDialog({ categories, onOpenChange }: CookieDialogProps) {
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={isUpdating}>
{isUpdating ? __("Adding...") : __("Add Cookie")}
<Button type="submit" disabled={isCreating}>
{isCreating ? __("Adding...") : __("Add Cookie")}
</Button>
</DialogFooter>
</form>

View File

@@ -35,8 +35,11 @@ import { useState } from "react";
import { useFragment, useMutation } from "react-relay";
import { graphql } from "relay-runtime";
import type { CategorySectionCreateCookieMutation } from "#/__generated__/core/CategorySectionCreateCookieMutation.graphql";
import type { CategorySectionDeleteCookieMutation } from "#/__generated__/core/CategorySectionDeleteCookieMutation.graphql";
import type { CategorySectionFragment$key } from "#/__generated__/core/CategorySectionFragment.graphql";
import type { CategorySectionMoveCookieMutation } from "#/__generated__/core/CategorySectionMoveCookieMutation.graphql";
import type { CategorySectionUpdateCookieMutation } from "#/__generated__/core/CategorySectionUpdateCookieMutation.graphql";
import type { CategorySectionUpdateMutation } from "#/__generated__/core/CategorySectionUpdateMutation.graphql";
import { AddCookieRow } from "./AddCookieRow";
@@ -55,10 +58,16 @@ export const categorySectionFragment = graphql`
name
description
kind
cookies {
name
duration
description
cookies(first: 100, orderBy: { field: CREATED_AT, direction: ASC }) @required(action: THROW) {
edges {
node {
id
name
duration
description
...EditCookieRowFragment
}
}
}
cookieBanner @required(action: THROW) {
categories(first: 50, orderBy: { field: RANK, direction: ASC }) @required(action: THROW) {
@@ -66,11 +75,6 @@ export const categorySectionFragment = graphql`
node {
id
name
cookies {
name
duration
description
}
}
}
}
@@ -88,11 +92,55 @@ const updateCategoryMutation = graphql`
name
description
rank
cookies {
updatedAt
}
cookieBanner {
id
latestVersion {
id
version
state
}
}
}
}
`;
const createCookieMutation = graphql`
mutation CategorySectionCreateCookieMutation(
$input: CreateCookieInput!
) {
createCookie(input: $input) {
cookieEdge {
node {
id
name
duration
description
}
}
cookieBanner {
id
latestVersion {
id
version
state
}
}
}
}
`;
const updateCookieMutation = graphql`
mutation CategorySectionUpdateCookieMutation(
$input: UpdateCookieInput!
) {
updateCookie(input: $input) {
cookie {
id
name
duration
description
updatedAt
}
cookieBanner {
@@ -107,26 +155,36 @@ const updateCategoryMutation = graphql`
}
`;
const deleteCookieMutation = graphql`
mutation CategorySectionDeleteCookieMutation(
$input: DeleteCookieInput!
) {
deleteCookie(input: $input) {
deletedCookieId
cookieBanner {
id
latestVersion {
id
version
state
}
}
}
}
`;
const moveCookieMutation = graphql`
mutation CategorySectionMoveCookieMutation(
$input: MoveCookieToCategoryInput!
) {
moveCookieToCategory(input: $input) {
sourceCookieCategory {
cookie {
id
cookies {
name
duration
description
}
updatedAt
}
targetCookieCategory {
id
cookies {
name
duration
description
name
duration
description
cookieCategory {
id
}
updatedAt
}
@@ -154,22 +212,29 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
const [updateCategory, isUpdating]
= useMutation<CategorySectionUpdateMutation>(updateCategoryMutation);
const [createCookie, isCreating]
= useMutation<CategorySectionCreateCookieMutation>(createCookieMutation);
const [updateCookie, isUpdatingCookie]
= useMutation<CategorySectionUpdateCookieMutation>(updateCookieMutation);
const [deleteCookie]
= useMutation<CategorySectionDeleteCookieMutation>(deleteCookieMutation);
const [moveCookie]
= useMutation<CategorySectionMoveCookieMutation>(moveCookieMutation);
const [isEditingCategory, setIsEditingCategory] = useState(false);
const [editingCookieIndex, setEditingCookieIndex] = useState<number | null>(null);
const [editingCookieId, setEditingCookieId] = useState<string | null>(null);
const [isAddingCookie, setIsAddingCookie] = useState(false);
const doUpdate = (
input: Record<string, unknown>,
onSuccess?: () => void,
) => {
const cookies = category.cookies.edges.map(e => e.node);
const isMutating = isUpdating || isCreating || isUpdatingCookie;
const handleSaveCategory = (name: string, description: string) => {
updateCategory({
variables: {
input: {
cookieCategoryId: category.id,
...input,
name,
description,
},
},
onCompleted(_response, errors) {
@@ -186,7 +251,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
description: __("Category updated"),
variant: "success",
});
onSuccess?.();
setIsEditingCategory(false);
},
onError(error) {
toast({
@@ -201,62 +266,138 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
});
};
const handleSaveCategory = (name: string, description: string) => {
doUpdate({ name, description }, () => {
setIsEditingCategory(false);
});
};
const handleSaveEditCookie = (index: number, cookie: CookieEntry) => {
if (!cookie.name.trim()) return;
const newCookies = category.cookies.map((c, i) =>
i === index
? { ...cookie }
: { name: c.name, duration: c.duration, description: c.description },
);
doUpdate({ cookies: newCookies }, () => {
setEditingCookieIndex(null);
});
};
const handleDeleteCookie = (index: number) => {
const newCookies = category.cookies
.filter((_, i) => i !== index)
.map(c => ({
name: c.name,
duration: c.duration,
description: c.description,
}));
doUpdate({ cookies: newCookies });
};
const handleSaveNewCookie = (cookie: CookieEntry) => {
if (!cookie.name.trim()) return;
const newCookies = [
...category.cookies.map(c => ({
name: c.name,
duration: c.duration,
description: c.description,
})),
{ ...cookie },
];
doUpdate({ cookies: newCookies }, () => {
setIsAddingCookie(false);
createCookie({
variables: {
input: {
cookieCategoryId: category.id,
name: cookie.name,
duration: cookie.duration,
description: cookie.description,
},
},
onCompleted(_response, errors) {
if (errors?.length) {
const isConflict = errors.some(
e => (e as unknown as GraphQLError).extensions?.code === "CONFLICT",
);
toast({
title: __("Error"),
description: isConflict
? __("A cookie with this name already exists in this banner")
: errors[0].message,
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Cookie added"),
variant: "success",
});
setIsAddingCookie(false);
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to add cookie"),
error as GraphQLError,
),
variant: "error",
});
},
});
};
const handleSaveEditCookie = (cookieId: string, cookie: CookieEntry) => {
if (!cookie.name.trim()) return;
updateCookie({
variables: {
input: {
cookieId,
name: cookie.name,
duration: cookie.duration,
description: cookie.description,
},
},
onCompleted(_response, errors) {
if (errors?.length) {
const isConflict = errors.some(
e => (e as unknown as GraphQLError).extensions?.code === "CONFLICT",
);
toast({
title: __("Error"),
description: isConflict
? __("A cookie with this name already exists in this banner")
: errors[0].message,
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Cookie updated"),
variant: "success",
});
setEditingCookieId(null);
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to update cookie"),
error as GraphQLError,
),
variant: "error",
});
},
});
};
const handleDeleteCookie = (cookieId: string) => {
deleteCookie({
variables: {
input: { cookieId },
},
onCompleted(_response, errors) {
if (errors?.length) {
toast({
title: __("Error"),
description: errors[0].message,
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Cookie deleted"),
variant: "success",
});
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to delete cookie"),
error as GraphQLError,
),
variant: "error",
});
},
});
};
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 handleMoveCookie = (cookieId: string, targetCategoryId: string) => {
moveCookie({
variables: {
input: {
sourceCookieCategoryId: category.id,
cookieId,
targetCookieCategoryId: targetCategoryId,
cookieName: cookie.name,
},
},
onCompleted(_response, errors) {
@@ -353,23 +494,19 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
</Tr>
</Thead>
<Tbody>
{category.cookies.map((cookie, index) =>
editingCookieIndex === index
{cookies.map(cookie =>
editingCookieId === cookie.id
? (
<EditCookieRow
key={cookie.name}
cookie={{
name: cookie.name,
duration: cookie.duration,
description: cookie.description,
}}
isUpdating={isUpdating}
onSave={updated => handleSaveEditCookie(index, updated)}
onCancel={() => setEditingCookieIndex(null)}
key={cookie.id}
cookieKey={cookie}
isUpdating={isMutating}
onSave={updated => handleSaveEditCookie(cookie.id, updated)}
onCancel={() => setEditingCookieId(null)}
/>
)
: (
<Tr key={cookie.name}>
<Tr key={cookie.id}>
<Td>
<code className="text-sm font-mono">{cookie.name}</code>
</Td>
@@ -384,7 +521,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
<button
type="button"
onClick={() => {
setEditingCookieIndex(index);
setEditingCookieId(cookie.id);
setIsAddingCookie(false);
}}
className="p-1 rounded cursor-pointer"
@@ -406,7 +543,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
<DropdownItem
className="text-sm"
key={cat.id}
onSelect={() => handleMoveCookie(index, cat.id)}
onSelect={() => handleMoveCookie(cookie.id, cat.id)}
>
{cat.name}
</DropdownItem>
@@ -415,7 +552,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
)}
<button
type="button"
onClick={() => handleDeleteCookie(index)}
onClick={() => handleDeleteCookie(cookie.id)}
className="p-1 rounded cursor-pointer text-danger-dark"
>
<IconTrashCan size={14} />
@@ -427,7 +564,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
)}
{isAddingCookie && (
<AddCookieRow
isUpdating={isUpdating}
isUpdating={isMutating}
onSave={handleSaveNewCookie}
onCancel={() => setIsAddingCookie(false)}
/>
@@ -441,7 +578,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)
variant="secondary"
onClick={() => {
setIsAddingCookie(true);
setEditingCookieIndex(null);
setEditingCookieId(null);
}}
>
<IconPlusSmall size={14} />

View File

@@ -15,24 +15,41 @@
import { useTranslate } from "@probo/i18n";
import { Button, Input, Td, Tr } from "@probo/ui";
import { useState } from "react";
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { EditCookieRowFragment$key } from "#/__generated__/core/EditCookieRowFragment.graphql";
import type { CookieEntry } from "./CategorySection";
export const editCookieRowFragment = graphql`
fragment EditCookieRowFragment on Cookie {
name
duration
description
}
`;
interface EditCookieRowProps {
cookie: CookieEntry;
cookieKey: EditCookieRowFragment$key;
isUpdating: boolean;
onSave: (cookie: CookieEntry) => void;
onCancel: () => void;
}
export function EditCookieRow({
cookie,
cookieKey,
isUpdating,
onSave,
onCancel,
}: EditCookieRowProps) {
const { __ } = useTranslate();
const [form, setForm] = useState<CookieEntry>(cookie);
const cookie = useFragment(editCookieRowFragment, cookieKey);
const [form, setForm] = useState<CookieEntry>({
name: cookie.name,
duration: cookie.duration,
description: cookie.description,
});
return (
<Tr>