Add data request pages to compliance portal
Let trust-portal data subjects submit and track GDPR/CCPA rights requests. The new Data Requests page lists the viewer's own requests and a dialog submits new ones, scoped server-side to the verified viewer email so former or inactive users can still exercise their rights. Submission requires magic-link sign-in (reusing the existing gate) but not the NDA gate. Extend the shared rights_request enums with RECTIFICATION, OBJECTION and COMPLAINT types plus a REJECTED state, and keep the console GraphQL, @probo/helpers and the MCP specification in sync. Expose a trust GraphQL surface (myRightsRequests query, createRightsRequest mutation) backed by a trust service and contact-scoped coredata loaders. Add the missing v2 UI kit primitives the dialog needs on top of Base UI: a SegmentedControl radio-cards group, a form Textarea, and a Field wrapper. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -20,43 +20,49 @@
|
||||
|
||||
type Translator = (s: string) => string;
|
||||
|
||||
export type RightsRequestType = "ACCESS" | "DELETION" | "PORTABILITY";
|
||||
export type RightsRequestType =
|
||||
| "ACCESS"
|
||||
| "DELETION"
|
||||
| "RECTIFICATION"
|
||||
| "PORTABILITY"
|
||||
| "OBJECTION"
|
||||
| "COMPLAINT";
|
||||
|
||||
export const rightsRequestTypes = [
|
||||
"ACCESS",
|
||||
"DELETION",
|
||||
"RECTIFICATION",
|
||||
"PORTABILITY",
|
||||
"OBJECTION",
|
||||
"COMPLAINT",
|
||||
] as const;
|
||||
|
||||
export type RightsRequestState = "TODO" | "IN_PROGRESS" | "DONE";
|
||||
export type RightsRequestState = "TODO" | "IN_PROGRESS" | "DONE" | "REJECTED";
|
||||
|
||||
export const rightsRequestStates = [
|
||||
"TODO",
|
||||
"IN_PROGRESS",
|
||||
"DONE",
|
||||
"REJECTED",
|
||||
] as const;
|
||||
|
||||
const rightsRequestTypeLabels: Record<RightsRequestType, string> = {
|
||||
"ACCESS": "Access",
|
||||
"DELETION": "Deletion",
|
||||
"RECTIFICATION": "Rectification",
|
||||
"PORTABILITY": "Portability",
|
||||
"OBJECTION": "Objection",
|
||||
"COMPLAINT": "Complaint",
|
||||
};
|
||||
|
||||
export function getRightsRequestTypeLabel(__: Translator, type: RightsRequestType) {
|
||||
switch (type) {
|
||||
case "ACCESS":
|
||||
return __("Access");
|
||||
case "DELETION":
|
||||
return __("Deletion");
|
||||
case "PORTABILITY":
|
||||
return __("Portability");
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
return __(rightsRequestTypeLabels[type] ?? type);
|
||||
}
|
||||
|
||||
export function getRightsRequestTypeOptions(__: Translator) {
|
||||
return rightsRequestTypes.map((type) => ({
|
||||
value: type,
|
||||
label: __({
|
||||
"ACCESS": "Access",
|
||||
"DELETION": "Deletion",
|
||||
"PORTABILITY": "Portability",
|
||||
}[type]),
|
||||
label: __(rightsRequestTypeLabels[type]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -70,31 +76,27 @@ export const getRightsRequestStateVariant = (
|
||||
return "info" as const;
|
||||
case "DONE":
|
||||
return "success" as const;
|
||||
case "REJECTED":
|
||||
return "danger" as const;
|
||||
default:
|
||||
return "neutral" as const;
|
||||
}
|
||||
};
|
||||
|
||||
const rightsRequestStateLabels: Record<RightsRequestState, string> = {
|
||||
"TODO": "To Do",
|
||||
"IN_PROGRESS": "In Progress",
|
||||
"DONE": "Done",
|
||||
"REJECTED": "Rejected",
|
||||
};
|
||||
|
||||
export function getRightsRequestStateLabel(__: Translator, state: RightsRequestState) {
|
||||
switch (state) {
|
||||
case "TODO":
|
||||
return __("To Do");
|
||||
case "IN_PROGRESS":
|
||||
return __("In Progress");
|
||||
case "DONE":
|
||||
return __("Done");
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return __(rightsRequestStateLabels[state] ?? state);
|
||||
}
|
||||
|
||||
export function getRightsRequestStateOptions(__: Translator) {
|
||||
return rightsRequestStates.map((state) => ({
|
||||
value: state,
|
||||
label: __({
|
||||
"TODO": "To Do",
|
||||
"IN_PROGRESS": "In Progress",
|
||||
"DONE": "Done",
|
||||
}[state]),
|
||||
label: __(rightsRequestStateLabels[state]),
|
||||
}));
|
||||
}
|
||||
|
||||
61
packages/ui/src/v2/SegmentedControl/SegmentedControl.tsx
Normal file
61
packages/ui/src/v2/SegmentedControl/SegmentedControl.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { segmentedControl } from "./variants";
|
||||
|
||||
export type SegmentedControlProps = {
|
||||
// Single selected value (controlled).
|
||||
value?: string;
|
||||
// Single selected value (uncontrolled).
|
||||
defaultValue?: string;
|
||||
// Fired with the newly selected value. Never fired with an empty selection,
|
||||
// so a value always stays selected (clicking the active item is a no-op).
|
||||
onValueChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
// Single-select pill group. Wraps Base UI's array-based ToggleGroup with a
|
||||
// friendlier single-value API.
|
||||
export function SegmentedControl(props: SegmentedControlProps) {
|
||||
const { value, defaultValue, onValueChange, disabled, className, children } = props;
|
||||
const { root } = segmentedControl();
|
||||
|
||||
return (
|
||||
<BaseToggleGroup
|
||||
className={root({ className })}
|
||||
disabled={disabled}
|
||||
value={value != null ? [value] : undefined}
|
||||
defaultValue={defaultValue != null ? [defaultValue] : undefined}
|
||||
onValueChange={(groupValue) => {
|
||||
const next = groupValue[0];
|
||||
if (next != null) {
|
||||
onValueChange?.(next);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BaseToggleGroup>
|
||||
);
|
||||
}
|
||||
40
packages/ui/src/v2/SegmentedControl/SegmentedControlItem.tsx
Normal file
40
packages/ui/src/v2/SegmentedControl/SegmentedControlItem.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Toggle as BaseToggle } from "@base-ui/react/toggle";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { segmentedControl } from "./variants";
|
||||
|
||||
export type SegmentedControlItemProps
|
||||
= & Omit<ComponentProps<typeof BaseToggle>, "className">
|
||||
& {
|
||||
className?: string;
|
||||
// Identifies this item within the group; matched against the group value.
|
||||
value: string;
|
||||
};
|
||||
|
||||
// A single segment. Pressed state is driven by Base UI (`data-pressed`).
|
||||
export function SegmentedControlItem(props: SegmentedControlItemProps) {
|
||||
const { className, ...rest } = props;
|
||||
const { item } = segmentedControl();
|
||||
|
||||
return <BaseToggle className={item({ className })} {...rest} />;
|
||||
}
|
||||
41
packages/ui/src/v2/SegmentedControl/variants.ts
Normal file
41
packages/ui/src/v2/SegmentedControl/variants.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Segmented control (single-select radio-cards over Base UI's ToggleGroup).
|
||||
// There is no surrounding track: the items are standalone bordered cards laid
|
||||
// out on an equal-width grid that wraps to new rows. Using a grid (rather than
|
||||
// flex-wrap) keeps every card the same width and prevents a lone wrapped item
|
||||
// from stretching across its row. Each card keeps a 1px border at all sizes
|
||||
// (the pressed state only darkens the border, so selection never shifts layout).
|
||||
export const segmentedControl = tv({
|
||||
slots: {
|
||||
root: "grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-1",
|
||||
item: [
|
||||
"min-w-0 cursor-pointer select-none rounded-3 border border-sand-a6 bg-sand-1 px-4 py-3.5",
|
||||
"text-center text-2 font-medium text-sand-12 outline-none transition-colors",
|
||||
"hover:border-sand-a8",
|
||||
"focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1",
|
||||
"data-pressed:border-sand-a12",
|
||||
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
],
|
||||
},
|
||||
});
|
||||
49
packages/ui/src/v2/form/Field.tsx
Normal file
49
packages/ui/src/v2/form/Field.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { field } from "./variants";
|
||||
|
||||
export type FieldProps = {
|
||||
// Text shown above the control. The control is nested inside the <label> so
|
||||
// the association is implicit (no htmlFor / id threading required).
|
||||
label?: ReactNode;
|
||||
// Validation / server error shown below the control.
|
||||
error?: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// Vertical label + control + error grouping for form dialogs.
|
||||
export function Field(props: FieldProps) {
|
||||
const { label, error, className, children } = props;
|
||||
const { root, label: labelSlot, labelText, error: errorSlot } = field();
|
||||
|
||||
return (
|
||||
<div className={root({ className })}>
|
||||
<label className={labelSlot()}>
|
||||
{label != null && <span className={labelText()}>{label}</span>}
|
||||
{children}
|
||||
</label>
|
||||
{error != null && <p className={errorSlot()}>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
packages/ui/src/v2/form/Textarea.tsx
Normal file
45
packages/ui/src/v2/form/Textarea.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { textArea } from "./variants";
|
||||
|
||||
export type TextareaProps
|
||||
= & Omit<ComponentProps<"textarea">, "className">
|
||||
& {
|
||||
// Applied to the bordered container (the top-level element).
|
||||
className?: string;
|
||||
// Surface treatment (defaults to "surface").
|
||||
variant?: "classic" | "surface" | "soft";
|
||||
};
|
||||
|
||||
// Multi-line text input on a bordered surface mirroring TextField. The container
|
||||
// is the top-level node; all native textarea props spread onto the inner control.
|
||||
export function Textarea(props: TextareaProps) {
|
||||
const { className, variant, rows = 4, ...textareaProps } = props;
|
||||
const { root, textarea } = textArea({ variant });
|
||||
|
||||
return (
|
||||
<div className={root({ className })}>
|
||||
<textarea className={textarea()} rows={rows} {...textareaProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -51,6 +51,42 @@ export const textField = tv({
|
||||
},
|
||||
});
|
||||
|
||||
// Multi-line text input, mirroring TextField's bordered surface. Base UI has no
|
||||
// textarea primitive, so this styles a native <textarea>.
|
||||
export const textArea = tv({
|
||||
slots: {
|
||||
root: [
|
||||
"flex rounded-2 text-2 text-sand-12 transition-colors",
|
||||
"focus-within:ring-2 focus-within:ring-sand-8 focus-within:ring-offset-1 focus-within:ring-offset-sand-1",
|
||||
"has-[textarea:disabled]:pointer-events-none has-[textarea:disabled]:opacity-50",
|
||||
],
|
||||
textarea: [
|
||||
"min-h-16 w-full resize-y bg-transparent px-2 py-1.5 text-sand-12 outline-none",
|
||||
"placeholder:text-sand-a9",
|
||||
],
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
classic: { root: "border border-sand-a5 bg-sand-1 inset-shadow-2" },
|
||||
surface: { root: "border border-sand-a5 bg-sand-1" },
|
||||
soft: { root: "bg-gold-3" },
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "surface",
|
||||
},
|
||||
});
|
||||
|
||||
// Vertical label + control + error grouping used by form dialogs.
|
||||
export const field = tv({
|
||||
slots: {
|
||||
root: "flex flex-col gap-1.5",
|
||||
label: "flex flex-col gap-1.5",
|
||||
labelText: "text-2 font-medium text-sand-12",
|
||||
error: "text-1 text-red-a11",
|
||||
},
|
||||
});
|
||||
|
||||
export const textFieldSkeleton = tv({
|
||||
base: "inline-block animate-pulse rounded-2 bg-sand-3 align-middle",
|
||||
variants: {
|
||||
|
||||
Reference in New Issue
Block a user