Link cookie policy doc and revamp tracker rows
Expose the generated cookie policy Document on the CookieBanner GraphQL type through a nullable policyDocument field and resolver, and surface a link to it from the banner configuration header next to the origin and ID. The link is hidden until a banner version is published and the document exists. Rework the tracker table rows: drop the Source column in favour of a tracker Type badge, and move each tracker's description inline beneath its name (and into the add/edit row inputs) instead of a separate Description column. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -29,7 +29,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
||||||
import { Outlet, useParams } from "react-router";
|
import { Link, Outlet, useParams } from "react-router";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
|
|
||||||
import type { CookieBannerConfigLayoutActivateMutation } from "#/__generated__/core/CookieBannerConfigLayoutActivateMutation.graphql";
|
import type { CookieBannerConfigLayoutActivateMutation } from "#/__generated__/core/CookieBannerConfigLayoutActivateMutation.graphql";
|
||||||
@@ -52,6 +52,9 @@ export const cookieBannerConfigLayoutQuery = graphql`
|
|||||||
version
|
version
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
|
policyDocument {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,6 +217,17 @@ export default function CookieBannerConfigLayout({ queryRef }: CookieBannerConfi
|
|||||||
<IconSquareBehindSquare2 size={16} />
|
<IconSquareBehindSquare2 size={16} />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
{banner.policyDocument && (
|
||||||
|
<>
|
||||||
|
<span className="text-border-primary">·</span>
|
||||||
|
<Link
|
||||||
|
to={`/organizations/${organizationId}/documents/${banner.policyDocument.id}`}
|
||||||
|
className="font-medium text-txt-primary underline"
|
||||||
|
>
|
||||||
|
{__("Cookie Policy")}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -58,10 +58,16 @@ export function AddCookieRow({
|
|||||||
return (
|
return (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td className="pr-3">
|
<Td className="pr-3">
|
||||||
|
<div className="flex flex-col gap-2 min-w-0 max-w-xs">
|
||||||
<Input
|
<Input
|
||||||
{...register("name")}
|
{...register("name")}
|
||||||
placeholder={__("Cookie name")}
|
placeholder={__("Cookie name")}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
{...register("description")}
|
||||||
|
placeholder={__("Description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td />
|
<Td />
|
||||||
<Td className="pr-3">
|
<Td className="pr-3">
|
||||||
@@ -78,12 +84,6 @@ export function AddCookieRow({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="pr-3">
|
|
||||||
<Input
|
|
||||||
{...register("description")}
|
|
||||||
placeholder={__("Description")}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||||
import { formatError, type GraphQLError, humanizeSeconds } from "@probo/helpers";
|
import { formatError, getTrackerTypeBadge, type GraphQLError, humanizeSeconds } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -76,10 +76,10 @@ export const categorySectionFragment = graphql`
|
|||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
|
trackerType
|
||||||
maxAgeSeconds
|
maxAgeSeconds
|
||||||
description
|
description
|
||||||
excluded
|
excluded
|
||||||
source
|
|
||||||
...EditCookieRowFragment
|
...EditCookieRowFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,10 +136,10 @@ const createPatternMutation = graphql`
|
|||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
displayName
|
displayName
|
||||||
|
trackerType
|
||||||
maxAgeSeconds
|
maxAgeSeconds
|
||||||
description
|
description
|
||||||
excluded
|
excluded
|
||||||
source
|
|
||||||
...EditCookieRowFragment
|
...EditCookieRowFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -759,9 +759,8 @@ export function CategorySection({ categoryKey, connectionId }: CategorySectionPr
|
|||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{__("Name")}</Th>
|
||||||
<Th>{__("Source")}</Th>
|
<Th>{__("Type")}</Th>
|
||||||
<Th>{__("Duration")}</Th>
|
<Th>{__("Duration")}</Th>
|
||||||
<Th>{__("Description")}</Th>
|
|
||||||
<Th className="w-20" />
|
<Th className="w-20" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -780,26 +779,24 @@ export function CategorySection({ categoryKey, connectionId }: CategorySectionPr
|
|||||||
: (
|
: (
|
||||||
<Tr key={pattern.id} className={pattern.excluded ? "opacity-80" : undefined}>
|
<Tr key={pattern.id} className={pattern.excluded ? "opacity-80" : undefined}>
|
||||||
<Td>
|
<Td>
|
||||||
|
<div className="flex flex-col min-w-0 max-w-xs">
|
||||||
<code className="text-sm font-mono">{pattern.displayName}</code>
|
<code className="text-sm font-mono">{pattern.displayName}</code>
|
||||||
|
{pattern.description && (
|
||||||
|
<span className="text-xs text-txt-tertiary wrap-break-word line-clamp-1">
|
||||||
|
{pattern.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Badge
|
{(() => {
|
||||||
variant={pattern.source === "SCRIPT" ? "info" : "neutral"}
|
const typeBadge = getTrackerTypeBadge(pattern.trackerType, __);
|
||||||
title={
|
return <Badge variant={typeBadge.variant}>{typeBadge.label}</Badge>;
|
||||||
pattern.source === "SCRIPT"
|
})()}
|
||||||
? __("Set by a script at runtime")
|
|
||||||
: __("Already present when the page was loaded")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{pattern.source === "SCRIPT" ? __("Script") : __("Pre-existing")}
|
|
||||||
</Badge>
|
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="text-sm text-muted-foreground">
|
<Td className="text-sm text-muted-foreground">
|
||||||
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
|
{humanizeSeconds(pattern.maxAgeSeconds ?? null)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="text-sm text-muted-foreground">
|
|
||||||
{pattern.description}
|
|
||||||
</Td>
|
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
import { fromMaxAgeSeconds, toMaxAgeSeconds } from "@probo/helpers";
|
import { fromMaxAgeSeconds, getTrackerTypeBadge, toMaxAgeSeconds } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { Button, DurationInput, Input, Td, Toggle, Tr } from "@probo/ui";
|
import { Badge, Button, DurationInput, Input, Td, Toggle, Tr } from "@probo/ui";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useFragment } from "react-relay";
|
import { useFragment } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
@@ -26,6 +26,7 @@ import type { CookieEntry } from "./CategorySection";
|
|||||||
export const editCookieRowFragment = graphql`
|
export const editCookieRowFragment = graphql`
|
||||||
fragment EditCookieRowFragment on TrackerPattern {
|
fragment EditCookieRowFragment on TrackerPattern {
|
||||||
displayName
|
displayName
|
||||||
|
trackerType
|
||||||
maxAgeSeconds
|
maxAgeSeconds
|
||||||
description
|
description
|
||||||
excluded
|
excluded
|
||||||
@@ -72,9 +73,12 @@ export function EditCookieRow({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const typeBadge = getTrackerTypeBadge(cookie.trackerType, __);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td className="pr-3">
|
<Td className="pr-3">
|
||||||
|
<div className="flex flex-col gap-2 min-w-0 max-w-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Controller
|
<Controller
|
||||||
name="excluded"
|
name="excluded"
|
||||||
@@ -90,8 +94,15 @@ export function EditCookieRow({
|
|||||||
/>
|
/>
|
||||||
<code className="text-sm font-mono">{cookie.displayName}</code>
|
<code className="text-sm font-mono">{cookie.displayName}</code>
|
||||||
</div>
|
</div>
|
||||||
|
<Input
|
||||||
|
{...register("description")}
|
||||||
|
placeholder={__("Description")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={typeBadge.variant}>{typeBadge.label}</Badge>
|
||||||
</Td>
|
</Td>
|
||||||
<Td />
|
|
||||||
<Td className="pr-3">
|
<Td className="pr-3">
|
||||||
<Controller
|
<Controller
|
||||||
name="duration"
|
name="duration"
|
||||||
@@ -106,12 +117,6 @@ export function EditCookieRow({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="pr-3">
|
|
||||||
<Input
|
|
||||||
{...register("description")}
|
|
||||||
placeholder={__("Description")}
|
|
||||||
/>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -143,6 +143,32 @@ func (r *cookieBannerResolver) LatestVersion(ctx context.Context, obj *types.Coo
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PolicyDocument is the resolver for the policyDocument field.
|
||||||
|
func (r *cookieBannerResolver) PolicyDocument(ctx context.Context, obj *types.CookieBanner) (*types.Document, error) {
|
||||||
|
if obj.PolicyDocument == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
loaders := dataloader.FromContext(ctx)
|
||||||
|
|
||||||
|
document, err := loaders.Document.Load(ctx, obj.PolicyDocument.ID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) || errors.Is(err, dataloadgen.ErrNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot get policy document", log.Error(err))
|
||||||
|
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewDocument(document), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ConsentRecords is the resolver for the consentRecords field.
|
// ConsentRecords is the resolver for the consentRecords field.
|
||||||
func (r *cookieBannerResolver) ConsentRecords(ctx context.Context, obj *types.CookieBanner, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.CookieConsentRecordOrderBy, filter *types.CookieConsentRecordFilter) (*types.CookieConsentRecordConnection, error) {
|
func (r *cookieBannerResolver) ConsentRecords(ctx context.Context, obj *types.CookieBanner, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.CookieConsentRecordOrderBy, filter *types.CookieConsentRecordFilter) (*types.CookieConsentRecordConnection, error) {
|
||||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionCookieConsentRecordList)
|
scope, err := r.authorize(ctx, obj.ID, probo.ActionCookieConsentRecordList)
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ type CookieBanner implements Node {
|
|||||||
|
|
||||||
latestVersion: CookieBannerVersion @goField(forceResolver: true)
|
latestVersion: CookieBannerVersion @goField(forceResolver: true)
|
||||||
|
|
||||||
|
policyDocument: Document @goField(forceResolver: true)
|
||||||
|
|
||||||
consentRecords(
|
consentRecords(
|
||||||
first: Int
|
first: Int
|
||||||
after: CursorKey
|
after: CursorKey
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func NewCookieBannerEdge(b *coredata.CookieBanner, orderBy coredata.CookieBanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewCookieBanner(b *coredata.CookieBanner) *CookieBanner {
|
func NewCookieBanner(b *coredata.CookieBanner) *CookieBanner {
|
||||||
return &CookieBanner{
|
banner := &CookieBanner{
|
||||||
ID: b.ID,
|
ID: b.ID,
|
||||||
Organization: &Organization{
|
Organization: &Organization{
|
||||||
ID: b.OrganizationID,
|
ID: b.OrganizationID,
|
||||||
@@ -77,6 +77,12 @@ func NewCookieBanner(b *coredata.CookieBanner) *CookieBanner {
|
|||||||
CreatedAt: b.CreatedAt,
|
CreatedAt: b.CreatedAt,
|
||||||
UpdatedAt: b.UpdatedAt,
|
UpdatedAt: b.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.PolicyDocumentID != nil {
|
||||||
|
banner.PolicyDocument = &Document{ID: *b.PolicyDocumentID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return banner
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCookieBannerTranslation(t *coredata.CookieBannerTranslation) *CookieBannerTranslation {
|
func NewCookieBannerTranslation(t *coredata.CookieBannerTranslation) *CookieBannerTranslation {
|
||||||
|
|||||||
Reference in New Issue
Block a user