From 622f1ba67d3da15e324a754c94841af1e5e5b9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 20 Jul 2026 09:47:55 +0200 Subject: [PATCH] Address PR review on data request pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Require a verified viewer email before creating a rights request and validate the free-text fields with the same SafeText bounds the console uses, so this public portal mutation stays safe and bounded. Move myRightsRequests onto the base Query, drop the now-dead count loaders, and order the RECTIFICATION enum value before PORTABILITY so the Postgres sort order matches RightsRequestTypes(). Harden the v2 kit primitives: SegmentedControl keeps equal-width cards (auto-fill), preserves its selection when the active card is toggled, and forwards an accessible name; Field associates its label and error by id/aria instead of wrapping the control in a label. Give the type group an accessible name, require the name field for non-complaint types, use a timezone-stable reference year, drop the underreporting header count, and neutralize the response-deadline copy. Signed-off-by: Émile Ré --- .../src/pages/requests/RequestsPage.tsx | 2 +- .../requests/_components/NewRequestDialog.tsx | 7 ++- .../src/pages/requests/_lib/rightsRequest.ts | 3 +- .../src/pages/requests/_locales/en-US.json | 2 +- .../src/pages/requests/_locales/fr-FR.json | 2 +- .../v2/SegmentedControl/SegmentedControl.tsx | 52 ++++++++++++++----- .../ui/src/v2/SegmentedControl/variants.ts | 2 +- packages/ui/src/v2/form/Field.tsx | 50 ++++++++++++++---- packages/ui/src/v2/form/variants.ts | 1 - pkg/coredata/migrations/20260717T133746Z.sql | 4 +- pkg/coredata/rights_requests.go | 38 -------------- pkg/server/api/trust/v1/base_resolvers.go | 33 ++++++++++++ pkg/server/api/trust/v1/graphql/base.graphql | 10 ++++ .../trust/v1/graphql/rights_request.graphql | 12 ----- .../api/trust/v1/rights_request_resolvers.go | 39 ++------------ pkg/trust/rights_request_service.go | 45 ++++++---------- 16 files changed, 156 insertions(+), 146 deletions(-) diff --git a/apps/compliance-portal/src/pages/requests/RequestsPage.tsx b/apps/compliance-portal/src/pages/requests/RequestsPage.tsx index 2bed3fcbb..cc4232afa 100644 --- a/apps/compliance-portal/src/pages/requests/RequestsPage.tsx +++ b/apps/compliance-portal/src/pages/requests/RequestsPage.tsx @@ -132,7 +132,7 @@ export function RequestsPage({ queryRef }: RequestsPageProps) { return ( <> - +
("ACCESS"); const [name, setName] = useState(viewerName); @@ -148,10 +149,11 @@ function NewRequestForm({ onClose, connectionId, viewerEmail, viewerName }: NewR
- + {t("dialog.typeLabel")} setType(value as SubmittableRightsRequestType)} > @@ -173,6 +175,7 @@ function NewRequestForm({ onClose, connectionId, viewerEmail, viewerName }: NewR setName(e.target.value)} /> diff --git a/apps/compliance-portal/src/pages/requests/_lib/rightsRequest.ts b/apps/compliance-portal/src/pages/requests/_lib/rightsRequest.ts index 7ab96b76a..0894362c2 100644 --- a/apps/compliance-portal/src/pages/requests/_lib/rightsRequest.ts +++ b/apps/compliance-portal/src/pages/requests/_lib/rightsRequest.ts @@ -98,7 +98,8 @@ export const rightsRequestFormConfig: Record< // Human-facing reference derived from the created year and a short suffix of the // opaque id (display-only; not a stored sequential number). export function formatRightsRequestReference(id: string, createdAt: string): string { - const year = new Date(createdAt).getFullYear(); + // Use UTC so the reference year is stable regardless of the viewer's timezone. + const year = new Date(createdAt).getUTCFullYear(); const suffix = id.replace(/[^a-zA-Z0-9]/g, "").slice(-6).toUpperCase(); return `REQ-${year}-${suffix}`; } diff --git a/apps/compliance-portal/src/pages/requests/_locales/en-US.json b/apps/compliance-portal/src/pages/requests/_locales/en-US.json index 7bfda2b61..1dabd1722 100644 --- a/apps/compliance-portal/src/pages/requests/_locales/en-US.json +++ b/apps/compliance-portal/src/pages/requests/_locales/en-US.json @@ -35,7 +35,7 @@ "submit": "Submit Request", "success": { "title": "Request submitted", - "description": "We'll process your request and respond within 30 days.", + "description": "We'll process your request and respond within the timeframe required by applicable law.", "close": "Close" } }, diff --git a/apps/compliance-portal/src/pages/requests/_locales/fr-FR.json b/apps/compliance-portal/src/pages/requests/_locales/fr-FR.json index d30617379..ca9fc1cd7 100644 --- a/apps/compliance-portal/src/pages/requests/_locales/fr-FR.json +++ b/apps/compliance-portal/src/pages/requests/_locales/fr-FR.json @@ -35,7 +35,7 @@ "submit": "Envoyer la demande", "success": { "title": "Demande envoyée", - "description": "Nous traiterons votre demande et vous répondrons sous 30 jours.", + "description": "Nous traiterons votre demande et vous répondrons dans le délai prévu par la loi applicable.", "close": "Fermer" } }, diff --git a/packages/ui/src/v2/SegmentedControl/SegmentedControl.tsx b/packages/ui/src/v2/SegmentedControl/SegmentedControl.tsx index 796441686..c0ddb8783 100644 --- a/packages/ui/src/v2/SegmentedControl/SegmentedControl.tsx +++ b/packages/ui/src/v2/SegmentedControl/SegmentedControl.tsx @@ -19,40 +19,66 @@ // SOFTWARE. import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group"; -import type { ReactNode } from "react"; +import { type ReactNode, useState } from "react"; import { segmentedControl } from "./variants"; export type SegmentedControlProps = { // Single selected value (controlled). - value?: string; + "value"?: string; // Single selected value (uncontrolled). - defaultValue?: string; + "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; + "onValueChange"?: (value: string) => void; + "disabled"?: boolean; + "className"?: string; + // Accessible name for the group (or reference a visible label via + // `aria-labelledby`), since the control has no intrinsic label. + "aria-label"?: string; + "aria-labelledby"?: string; + "children"?: ReactNode; }; // Single-select pill group. Wraps Base UI's array-based ToggleGroup with a -// friendlier single-value API. +// friendlier single-value API. Selection is tracked internally (seeded from +// `value`/`defaultValue`) so toggling the active item off — which Base UI +// reports as an empty group value — never clears the selection. export function SegmentedControl(props: SegmentedControlProps) { - const { value, defaultValue, onValueChange, disabled, className, children } = props; + const { + value, + defaultValue, + onValueChange, + disabled, + className, + children, + "aria-label": ariaLabel, + "aria-labelledby": ariaLabelledby, + } = props; const { root } = segmentedControl(); + const isControlled = value !== undefined; + const [internalValue, setInternalValue] = useState(defaultValue); + const currentValue = isControlled ? value : internalValue; + return ( { const next = groupValue[0]; - if (next != null) { - onValueChange?.(next); + // Ignore the empty value emitted when the active item is toggled off, + // preserving the single-selection contract. + if (next == null) { + return; } + if (!isControlled) { + setInternalValue(next); + } + onValueChange?.(next); }} > {children} diff --git a/packages/ui/src/v2/SegmentedControl/variants.ts b/packages/ui/src/v2/SegmentedControl/variants.ts index 2a1d4c905..8d1a20fa5 100644 --- a/packages/ui/src/v2/SegmentedControl/variants.ts +++ b/packages/ui/src/v2/SegmentedControl/variants.ts @@ -28,7 +28,7 @@ import { tv } from "tailwind-variants/lite"; // (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", + root: "grid grid-cols-[repeat(auto-fill,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", diff --git a/packages/ui/src/v2/form/Field.tsx b/packages/ui/src/v2/form/Field.tsx index 8580e567e..a4aaa63d8 100644 --- a/packages/ui/src/v2/form/Field.tsx +++ b/packages/ui/src/v2/form/Field.tsx @@ -18,32 +18,60 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import type { ReactNode } from "react"; +import { cloneElement, isValidElement, type ReactElement, type ReactNode, useId } from "react"; import { field } from "./variants"; export type FieldProps = { - // Text shown above the control. The control is nested inside the