From 165b9ad9d391d527ad7c52edd8d36f4a11f9d6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Thu, 23 Apr 2026 19:07:11 +0400 Subject: [PATCH] Add slug to cookie categories for stable consent identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The category slug provides a stable, URL-safe key used as the data-cookie-consent attribute value and consent data key, replacing the fragile category name. This prevents breakage when categories are renamed. - Add slug column with unique-per-banner constraint and backfill migration - Add Slug validator (lowercase alphanumeric + hyphens) - Propagate slug through GraphQL schema, service layer, and snapshot - Update console UI with slug field in create/edit forms - Switch cookie-banner widget to use slug as consent data keys Signed-off-by: Émile Ré --- .../_components/CategoryDialog.tsx | 27 +++++++++++- .../cookies/_components/CategorySection.tsx | 8 +++- .../cookies/_components/EditCategoryForm.tsx | 13 +++++- packages/cookie-banner/src/client.ts | 11 ++--- .../src/components/category-list.ts | 1 + .../src/components/category-toggle.ts | 7 +-- .../cookie-banner/src/components/category.ts | 4 ++ .../src/components/cookie-banner-root.ts | 8 ++-- pkg/cookiebanner/defaults.go | 11 ++--- pkg/cookiebanner/errors.go | 1 + pkg/cookiebanner/service.go | 16 +++++++ pkg/coredata/cookie_banner_version.go | 1 + pkg/coredata/cookie_category.go | 21 +++++++++ pkg/coredata/migrations/20260423T144457Z.sql | 24 +++++++++++ .../api/console/v1/cookie_banner_resolvers.go | 8 ++++ .../console/v1/graphql/cookie_banner.graphql | 3 ++ .../api/console/v1/types/cookie_category.go | 1 + pkg/validator/validator_format.go | 32 ++++++++++++++ pkg/validator/validator_format_test.go | 43 +++++++++++++++++++ 19 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 pkg/coredata/migrations/20260423T144457Z.sql 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 c5cd3bc5a..b9d73c68c 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 @@ -40,6 +40,7 @@ const createMutation = graphql` node { id name + slug description kind rank @@ -78,8 +79,27 @@ export function CategoryDialog({ const [create, isCreating] = useMutation(createMutation); const [name, setName] = useState(""); + const [slug, setSlug] = useState(""); + const [slugTouched, setSlugTouched] = useState(false); const [description, setDescription] = useState(""); + const handleNameChange = (value: string) => { + setName(value); + if (!slugTouched) { + setSlug( + value + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, ""), + ); + } + }; + + const handleSlugChange = (value: string) => { + setSlugTouched(true); + setSlug(value); + }; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -88,6 +108,7 @@ export function CategoryDialog({ input: { cookieBannerId, name, + slug, description, rank: nextRank, }, @@ -113,7 +134,11 @@ export function CategoryDialog({
- setName(e.target.value)} required /> + handleNameChange(e.target.value)} required /> + + + + handleSlugChange(e.target.value)} required pattern="[a-z0-9]+(-[a-z0-9]+)*" /> 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 2078f50e3..b8596d79b 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 @@ -56,6 +56,7 @@ export const categorySectionFragment = graphql` fragment CategorySectionFragment on CookieCategory { id name + slug description kind cookies(first: 100, orderBy: { field: CREATED_AT, direction: ASC }) @@ -93,6 +94,7 @@ const updateCategoryMutation = graphql` cookieCategory { id name + slug description rank updatedAt @@ -235,12 +237,13 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps) const cookies = category.cookies.edges.map(e => e.node); const isMutating = isUpdating || isCreating || isUpdatingCookie; - const handleSaveCategory = (name: string, description: string) => { + const handleSaveCategory = (name: string, slug: string, description: string) => { updateCategory({ variables: { input: { cookieCategoryId: category.id, name, + slug, description, }, }, @@ -476,6 +479,7 @@ export function CategorySection({ categoryKey, onDelete }: CategorySectionProps) ? ( data-cookie-consent=" - {category.name.toLowerCase()} + {category.slug} "

diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/EditCategoryForm.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/EditCategoryForm.tsx index bc8f35c84..0b35de7f9 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/EditCategoryForm.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/cookies/_components/EditCategoryForm.tsx @@ -18,14 +18,16 @@ import { useState } from "react"; interface EditCategoryFormProps { name: string; + slug: string; description: string; isUpdating: boolean; - onSave: (name: string, description: string) => void; + onSave: (name: string, slug: string, description: string) => void; onCancel: () => void; } export function EditCategoryForm({ name, + slug, description, isUpdating, onSave, @@ -33,6 +35,7 @@ export function EditCategoryForm({ }: EditCategoryFormProps) { const { __ } = useTranslate(); const [editName, setEditName] = useState(name); + const [editSlug, setEditSlug] = useState(slug); const [editDescription, setEditDescription] = useState(description); return ( @@ -42,6 +45,12 @@ export function EditCategoryForm({ onChange={e => setEditName(e.target.value)} placeholder={__("Category name")} /> + setEditSlug(e.target.value)} + placeholder={__("Category slug")} + pattern="[a-z0-9]+(-[a-z0-9]+)*" + />