From 1b8bd1895eb2d4dda32bbe687a0e66a37617250a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 29 May 2026 15:49:21 +0200 Subject: [PATCH] Rework tracker and resource row actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn the category column into an inline Select so a tracker pattern or resource can be recategorised in place. On the pattern row the move is gated by a confirm that surfaces the existing behaviour of promoting or linking a third party. The remaining row actions (include/exclude and delete) move into a single action dropdown, leaving only Edit inline. Add a Max Age column to the pattern table and a Category column to the resource table, and lay the edit forms out with the name above the inputs so the description can use the full row width. Shrink the action column to its content so the freed space goes to the data columns. Signed-off-by: Émile Ré --- .../resources/CookieBannerResourcesPage.tsx | 3 +- .../_components/TrackerResourceRow.tsx | 89 ++++++--------- .../_components/TrackerResourceRowEdit.tsx | 2 +- .../trackers/CookieBannerTrackersPage.tsx | 3 +- .../_components/MoveToCategorySelect.tsx | 105 +++++++++++++++++ .../_components/TrackerPatternRow.tsx | 108 ++++++++---------- .../_components/TrackerPatternRowEdit.tsx | 77 +++++++------ 7 files changed, 228 insertions(+), 159 deletions(-) create mode 100644 apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/MoveToCategorySelect.tsx diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx index 3fdfb9e49..ace5afc6d 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/CookieBannerResourcesPage.tsx @@ -187,8 +187,9 @@ export default function CookieBannerResourcesPage({ {__("Type")} {__("Origin")} {__("Path")} + {__("Category")} {__("Last Detected")} - + diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx index f1d8afcac..35b1aa79a 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRow.tsx @@ -16,9 +16,9 @@ import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react"; import { formatError, type GraphQLError } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; import { + ActionDropdown, Badge, - Dropdown, - IconArrowBoxLeft, + DropdownItem, IconPencil, IconTrashCan, Td, @@ -26,21 +26,16 @@ import { useConfirm, useToast, } from "@probo/ui"; -import { Suspense, useCallback, useState } from "react"; -import { graphql, useFragment, useMutation, useQueryLoader } from "react-relay"; -import { useParams } from "react-router"; +import { useState } from "react"; +import { graphql, useFragment, useMutation } from "react-relay"; import { ConnectionHandler } from "relay-runtime"; -import type { MoveToCategoryDropdownQuery } from "#/__generated__/core/MoveToCategoryDropdownQuery.graphql"; import type { TrackerResourceRowDeleteMutation } from "#/__generated__/core/TrackerResourceRowDeleteMutation.graphql"; import type { TrackerResourceRowFragment$key } from "#/__generated__/core/TrackerResourceRowFragment.graphql"; import type { TrackerResourceRowMoveMutation } from "#/__generated__/core/TrackerResourceRowMoveMutation.graphql"; import type { TrackerResourceRowUpdateMutation } from "#/__generated__/core/TrackerResourceRowUpdateMutation.graphql"; -import { - MoveToCategoryDropdown, - moveToCategoryDropdownQuery, -} from "../../trackers/_components/MoveToCategoryDropdown"; +import { MoveToCategorySelect } from "../../trackers/_components/MoveToCategorySelect"; import { TrackerResourceRowEdit } from "./TrackerResourceRowEdit"; @@ -54,6 +49,10 @@ const trackerResourceFragment = graphql` description excluded lastDetectedAt + cookieCategory { + id + name + } } `; @@ -147,22 +146,10 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc const { __ } = useTranslate(); const { toast } = useToast(); const confirm = useConfirm(); - const { cookieBannerId } = useParams<{ cookieBannerId: string }>(); const resource = useFragment(trackerResourceFragment, resourceKey); const typeBadge = resourceTypeBadge(resource.type, __); const [isEditing, setIsEditing] = useState(false); - const [categoryQueryRef, loadCategoryQuery] - = useQueryLoader(moveToCategoryDropdownQuery); - - const handleCategoryDropdownOpen = useCallback( - (open: boolean) => { - if (open && cookieBannerId) { - loadCategoryQuery({ cookieBannerId }); - } - }, - [loadCategoryQuery, cookieBannerId], - ); const [deleteResource] = useMutation(deleteResourceMutation); @@ -309,6 +296,13 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc {resource.path} + + + {resource.lastDetectedAt ? ( @@ -318,7 +312,7 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc ) : -} - +
- - - - )} - > - {categoryQueryRef && ( - - - - )} - - - + + + {resource.excluded ? __("Include") : __("Exclude")} + + + {__("Delete")} + +
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRowEdit.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRowEdit.tsx index ad5c4bb8d..84a8bb39c 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRowEdit.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/resources/_components/TrackerResourceRowEdit.tsx @@ -61,7 +61,7 @@ export function TrackerResourceRowEdit({ placeholder={__("Display name")} /> - +
{__("Third party")} {__("Source")} {__("Category")} + {__("Max Age")} {__("Last Matched")} - + diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/MoveToCategorySelect.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/MoveToCategorySelect.tsx new file mode 100644 index 000000000..1e8adc150 --- /dev/null +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/MoveToCategorySelect.tsx @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { useTranslate } from "@probo/i18n"; +import { Option, Select } from "@probo/ui"; +import { Suspense, useCallback } from "react"; +import { type PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay"; +import { useParams } from "react-router"; + +import type { MoveToCategoryDropdownQuery } from "#/__generated__/core/MoveToCategoryDropdownQuery.graphql"; + +import { moveToCategoryDropdownQuery } from "./MoveToCategoryDropdown"; + +interface MoveToCategorySelectProps { + currentCategoryId?: string; + currentCategoryName?: string; + onSelect: (categoryId: string) => void; +} + +export function MoveToCategorySelect({ + currentCategoryId, + currentCategoryName, + onSelect, +}: MoveToCategorySelectProps) { + const { cookieBannerId } = useParams<{ cookieBannerId: string }>(); + const [categoryQueryRef, loadCategoryQuery] + = useQueryLoader(moveToCategoryDropdownQuery); + + const handleOpenChange = useCallback( + (open: boolean) => { + if (open && cookieBannerId) { + loadCategoryQuery({ cookieBannerId }); + } + }, + [loadCategoryQuery, cookieBannerId], + ); + + const handleValueChange = useCallback( + (categoryId: string) => { + if (categoryId !== currentCategoryId) { + onSelect(categoryId); + } + }, + [currentCategoryId, onSelect], + ); + + return ( + + ); +} + +interface MoveToCategoryOptionsProps { + queryRef: PreloadedQuery; +} + +function MoveToCategoryOptions({ queryRef }: MoveToCategoryOptionsProps) { + const { __ } = useTranslate(); + const data = usePreloadedQuery(moveToCategoryDropdownQuery, queryRef); + + if (data.node.__typename !== "CookieBanner") { + return null; + } + + const categories = data.node.categories.edges.map(e => e.node); + + if (categories.length === 0) { + return ( + + ); + } + + return ( + <> + {categories.map(cat => ( + + ))} + + ); +} diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx index 379d2fc92..fa01a0c0c 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRow.tsx @@ -13,12 +13,12 @@ // PERFORMANCE OF THIS SOFTWARE. import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react"; -import { formatError, getTrackerSourceBadge, getTrackerTypeBadge, type GraphQLError } from "@probo/helpers"; +import { formatError, getTrackerSourceBadge, getTrackerTypeBadge, type GraphQLError, humanizeSeconds } from "@probo/helpers"; import { useTranslate } from "@probo/i18n"; import { + ActionDropdown, Badge, - Dropdown, - IconArrowBoxLeft, + DropdownItem, IconPencil, IconTrashCan, Td, @@ -26,21 +26,16 @@ import { useConfirm, useToast, } from "@probo/ui"; -import { Suspense, useCallback, useState } from "react"; -import { graphql, useFragment, useMutation, useQueryLoader } from "react-relay"; -import { useParams } from "react-router"; +import { useState } from "react"; +import { graphql, useFragment, useMutation } from "react-relay"; import { ConnectionHandler } from "relay-runtime"; -import type { MoveToCategoryDropdownQuery } from "#/__generated__/core/MoveToCategoryDropdownQuery.graphql"; import type { TrackerPatternRowDeleteMutation } from "#/__generated__/core/TrackerPatternRowDeleteMutation.graphql"; import type { TrackerPatternRowFragment$key } from "#/__generated__/core/TrackerPatternRowFragment.graphql"; import type { TrackerPatternRowMoveMutation } from "#/__generated__/core/TrackerPatternRowMoveMutation.graphql"; import type { TrackerPatternRowUpdateMutation } from "#/__generated__/core/TrackerPatternRowUpdateMutation.graphql"; -import { - MoveToCategoryDropdown, - moveToCategoryDropdownQuery, -} from "./MoveToCategoryDropdown"; +import { MoveToCategorySelect } from "./MoveToCategorySelect"; import { TrackerPatternRowEdit } from "./TrackerPatternRowEdit"; const trackerPatternFragment = graphql` @@ -54,6 +49,7 @@ const trackerPatternFragment = graphql` excluded lastMatchedAt cookieCategory { + id name } thirdParty { @@ -143,21 +139,9 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo const { __ } = useTranslate(); const { toast } = useToast(); const confirm = useConfirm(); - const { cookieBannerId } = useParams<{ cookieBannerId: string }>(); const pattern = useFragment(trackerPatternFragment, patternKey); const [isEditing, setIsEditing] = useState(false); - const [categoryQueryRef, loadCategoryQuery] - = useQueryLoader(moveToCategoryDropdownQuery); - - const handleCategoryDropdownOpen = useCallback( - (open: boolean) => { - if (open && cookieBannerId) { - loadCategoryQuery({ cookieBannerId }); - } - }, - [loadCategoryQuery, cookieBannerId], - ); const [deletePattern] = useMutation(deletePatternMutation); @@ -229,6 +213,22 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo }); }; + const handleMoveWithConfirm = (targetCategoryId: string) => { + if (targetCategoryId === pattern.cookieCategory?.id) { + return; + } + confirm( + () => { + handleMove(targetCategoryId); + }, + { + message: __("Moving this tracker to a category will create a third party for it (or link an existing one) if it doesn't have one yet. Continue?"), + variant: "primary", + label: __("Move"), + }, + ); + }; + const handleToggleExcluded = () => { updatePattern({ variables: { @@ -323,10 +323,15 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo ? {srcBadge.label} : -} + + + - {pattern.cookieCategory - ? {pattern.cookieCategory.name} - : -} + {humanizeSeconds(pattern.maxAgeSeconds ?? null)} {pattern.lastMatchedAt @@ -337,7 +342,7 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo ) : -} - +
- - - - )} - > - {categoryQueryRef && ( - - - - )} - - - + + + {pattern.excluded ? __("Include") : __("Exclude")} + + + {__("Delete")} + +
diff --git a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRowEdit.tsx b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRowEdit.tsx index cf7a49c58..20c2a4472 100644 --- a/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRowEdit.tsx +++ b/apps/console/src/pages/organizations/cookie-banners/configuration/trackers/_components/TrackerPatternRowEdit.tsx @@ -58,44 +58,45 @@ export function TrackerPatternRowEdit({ return ( - - {pattern} - - - - - ( - field.onChange({ ...field.value, value: v })} - onUnitChange={u => field.onChange({ ...field.value, unit: u })} - /> - )} - /> - - -
- - - + +
+ {pattern} +
+
+ + +
+
+ + ( + field.onChange({ ...field.value, value: v })} + onUnitChange={u => field.onChange({ ...field.value, unit: u })} + /> + )} + /> +
+ + +