Add uncategorised cookie category

Replace the `required` boolean column on cookie_categories with a `kind`
enum (NORMAL, NECESSARY, UNCATEGORISED). The Necessary category remains
undeletable and always-on for consent; the new Uncategorised category is
also undeletable but users can opt out of it.

When a category is deleted, its cookies are merged into the Uncategorised
category (lazy-created for legacy banners that don't have one yet).

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-20 19:00:00 +04:00
parent 2147cded9f
commit 7cd8c516b9
18 changed files with 207 additions and 70 deletions

View File

@@ -41,7 +41,7 @@ const createMutation = graphql`
id
name
description
required
kind
rank
cookies {
name
@@ -94,7 +94,6 @@ export function CategoryDialog({
cookieBannerId,
name,
description,
required: false,
rank: nextRank,
},
connections: [connectionId],

View File

@@ -28,14 +28,16 @@ import { CategoryDialog } from "./CategoryDialog";
const categoryListFragment = graphql`
fragment CategoryList_cookieBanner on CookieBanner {
id
categories(first: 50, orderBy: { field: RANK, direction: ASC }) @required(action: THROW) {
categories(first: 50, orderBy: { field: RANK, direction: ASC })
@connection(key: "CategoryList_categories")
@required(action: THROW) {
__id
edges {
node {
id
name
description
required
kind
rank
}
}
@@ -152,7 +154,7 @@ export function CategoryList({ cookieBannerKey }: CategoryListProps) {
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="font-medium">{category.name}</span>
{category.required && (
{category.kind === "NECESSARY" && (
<Badge variant="neutral">{__("Required")}</Badge>
)}
</div>
@@ -175,7 +177,7 @@ export function CategoryList({ cookieBannerKey }: CategoryListProps) {
<IconArrowDown size={14} />
</button>
</div>
{!category.required && (
{category.kind === "NORMAL" && (
<Button
variant="danger"
className="h-6 px-2 text-xs"

View File

@@ -30,7 +30,9 @@ export const cookieBannerCookiesPageQuery = graphql`
__typename
... on CookieBanner {
id
categories(first: 50, orderBy: { field: RANK, direction: ASC }) @required(action: THROW) {
categories(first: 50, orderBy: { field: RANK, direction: ASC })
@connection(key: "CookieBannerCookiesPage_categories")
@required(action: THROW) {
__id
edges {
node {

View File

@@ -42,7 +42,7 @@ export const categorySectionFragment = graphql`
id
name
description
required
kind
cookies {
name
duration
@@ -269,7 +269,7 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-medium">{category.name}</span>
{category.required && (
{category.kind === "NECESSARY" && (
<Badge variant="neutral">{__("Required")}</Badge>
)}
</div>
@@ -314,7 +314,7 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
editingCookieIndex === index
? (
<Tr key={index}>
<Td>
<Td className="pr-3">
<Input
value={cookieForm.name}
onChange={e =>
@@ -322,7 +322,7 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
placeholder={__("Cookie name")}
/>
</Td>
<Td>
<Td className="pr-3">
<Input
value={cookieForm.duration}
onChange={e =>
@@ -333,7 +333,7 @@ export function CategorySection({ categoryKey }: CategorySectionProps) {
placeholder={__("e.g. 1 year")}
/>
</Td>
<Td>
<Td className="pr-3">
<Input
value={cookieForm.description}
onChange={e =>