diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CategoryDialog.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CategoryDialog.tsx index b3f2aa6a0..c5cd3bc5a 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CategoryDialog.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CategoryDialog.tsx @@ -43,11 +43,6 @@ const createMutation = graphql` description kind rank - cookies { - name - duration - description - } createdAt updatedAt } diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CookieDialog.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CookieDialog.tsx index 8af67e360..b4f425ad7 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CookieDialog.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/_components/CookieDialog.tsx @@ -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; } interface CookieDialogProps { @@ -82,7 +71,7 @@ export function CookieDialog({ categories, onOpenChange }: CookieDialogProps) { const { toast } = useToast(); const dialogRef = useDialogRef(); - const [updateCategory, isUpdating] = useMutation(updateCategoryMutation); + const [createCookie, isCreating] = useMutation(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) { - diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/CategorySection.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/CategorySection.tsx index 56ca4ee1d..4ade1414f 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/CategorySection.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/CategorySection.tsx @@ -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(updateCategoryMutation); + const [createCookie, isCreating] + = useMutation(createCookieMutation); + const [updateCookie, isUpdatingCookie] + = useMutation(updateCookieMutation); + const [deleteCookie] + = useMutation(deleteCookieMutation); const [moveCookie] = useMutation(moveCookieMutation); const [isEditingCategory, setIsEditingCategory] = useState(false); - const [editingCookieIndex, setEditingCookieIndex] = useState(null); + const [editingCookieId, setEditingCookieId] = useState(null); const [isAddingCookie, setIsAddingCookie] = useState(false); - const doUpdate = ( - input: Record, - 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) - {category.cookies.map((cookie, index) => - editingCookieIndex === index + {cookies.map(cookie => + editingCookieId === cookie.id ? ( handleSaveEditCookie(index, updated)} - onCancel={() => setEditingCookieIndex(null)} + key={cookie.id} + cookieKey={cookie} + isUpdating={isMutating} + onSave={updated => handleSaveEditCookie(cookie.id, updated)} + onCancel={() => setEditingCookieId(null)} /> ) : ( - + {cookie.name} @@ -384,7 +521,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps)