Add documents page to the compliance portal
Build the Trust Center documents page: a unified list of published documents, uploaded files, and audit reports, grouped into category sections. An All/Public/Private tab bar filters the list by trust center visibility. Expose that filter over the trust v1 API by adding a TrustCenterVisibility enum and a shared TrustCenterVisibilityFilter input, wiring it through the documents, audits, and trustCenterFiles connections down to the existing coredata SQL filters. "All" keeps the default public+private slice; the other tabs pin a single visibility. Access controls are display-only for now (auth is handled separately): authorized or public entries open their exported PDF via the export mutations, requested entries show a pending state, and everything else shows an inert Get Access affordance. Add the v2 Tabs and Toaster kit components (Base UI headless) needed by the page and mount a toast provider at the app root for mutation feedback. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Toast } from "@base-ui/react/toast";
|
||||
import { Toaster } from "@probo/ui/src/v2/Toaster/Toaster";
|
||||
import { RouterProvider } from "react-router";
|
||||
|
||||
import { RelayProvider } from "#/lib/relay/RelayProvider";
|
||||
@@ -26,7 +28,10 @@ import { router } from "#/routes";
|
||||
export function App() {
|
||||
return (
|
||||
<RelayProvider>
|
||||
<RouterProvider router={router} />
|
||||
<Toast.Provider>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster />
|
||||
</Toast.Provider>
|
||||
</RelayProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"error": "Something went wrong"
|
||||
},
|
||||
"topBar": {
|
||||
"tagline": "Compliance Portal",
|
||||
"getAccess": "Get Access",
|
||||
@@ -24,9 +27,6 @@
|
||||
"viewAll": "View all"
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Documents"
|
||||
},
|
||||
"requests": {
|
||||
"title": "Data Requests",
|
||||
"newRequest": "New Request"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"common": {
|
||||
"error": "Une erreur est survenue"
|
||||
},
|
||||
"topBar": {
|
||||
"tagline": "Portail de conformité",
|
||||
"getAccess": "Obtenir l'accès",
|
||||
@@ -24,9 +27,6 @@
|
||||
"viewAll": "Voir tout"
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"title": "Documents"
|
||||
},
|
||||
"requests": {
|
||||
"title": "Demandes de données",
|
||||
"newRequest": "Nouvelle demande"
|
||||
|
||||
@@ -22,12 +22,16 @@ import type { PropsWithChildren } from "react";
|
||||
|
||||
import { headerBand } from "./variants";
|
||||
|
||||
export type HeaderBandProps = PropsWithChildren;
|
||||
export type HeaderBandProps = PropsWithChildren<{
|
||||
// Drop the band's bottom padding so trailing content (e.g. a tab bar) aligns
|
||||
// with the band's bottom border.
|
||||
flushBottomSpace?: boolean;
|
||||
}>;
|
||||
|
||||
// Pure layout shell: the white band + centered content column. Consumers (Hero,
|
||||
// PageHeader) supply a single content column and own its internal spacing.
|
||||
export function HeaderBand({ children }: HeaderBandProps) {
|
||||
const { band, inner } = headerBand();
|
||||
export function HeaderBand({ children, flushBottomSpace }: HeaderBandProps) {
|
||||
const { band, inner } = headerBand({ flushBottomSpace });
|
||||
|
||||
return (
|
||||
<header className={band()}>
|
||||
|
||||
@@ -25,7 +25,19 @@ import { tv } from "tailwind-variants/lite";
|
||||
// Hero and the page headers so the band is defined once.
|
||||
export const headerBand = tv({
|
||||
slots: {
|
||||
band: "flex w-full flex-col items-center border-b border-sand-a3 bg-sand-1 px-8 py-8",
|
||||
band: "flex w-full flex-col items-center border-b border-sand-a3 bg-sand-1 px-8",
|
||||
inner: "w-full max-w-5xl",
|
||||
},
|
||||
variants: {
|
||||
// Drops the bottom padding and pulls the content down 1px so a trailing
|
||||
// toolbar's own bottom border (e.g. an underlined tab bar) overlaps — and
|
||||
// paints over — the band's bottom border instead of stacking below it.
|
||||
flushBottomSpace: {
|
||||
true: { band: "pt-8", inner: "-mb-px" },
|
||||
false: { band: "py-8" },
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
flushBottomSpace: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,15 +33,18 @@ export interface PageHeaderProps {
|
||||
actions?: ReactNode;
|
||||
// Optional toolbar (tabs / filters / search) rendered below the title.
|
||||
children?: ReactNode;
|
||||
// Sit the toolbar flush on the band's bottom border (e.g. an underlined tab
|
||||
// bar whose divider doubles as the header divider).
|
||||
flushBottomSpace?: boolean;
|
||||
}
|
||||
|
||||
// Page header for the Trust Center nav pages: a size-7 title in the shared white
|
||||
// band, with an optional count, inline actions, and a toolbar slot below.
|
||||
export function PageHeader({ title, count, actions, children }: PageHeaderProps) {
|
||||
export function PageHeader({ title, count, actions, children, flushBottomSpace }: PageHeaderProps) {
|
||||
const { content, titleRow, count: countSlot } = pageHeader();
|
||||
|
||||
return (
|
||||
<HeaderBand>
|
||||
<HeaderBand flushBottomSpace={flushBottomSpace}>
|
||||
<div className={content()}>
|
||||
<div className={titleRow()}>
|
||||
<Heading level={1} size={7} weight="medium" highContrast>
|
||||
|
||||
179
apps/compliance-portal/src/pages/documents/DocumentsPage.tsx
Normal file
179
apps/compliance-portal/src/pages/documents/DocumentsPage.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
// 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 { useEffect, useRef, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { PreloadedQuery } from "react-relay";
|
||||
import { graphql, usePreloadedQuery, useRefetchableFragment } from "react-relay";
|
||||
|
||||
import { ListErrorBoundary } from "#/components/errors/ListErrorBoundary";
|
||||
import { PageHeader } from "#/components/PageHeader/PageHeader";
|
||||
|
||||
import type { DocumentsPage_query$key } from "./__generated__/DocumentsPage_query.graphql";
|
||||
import type { DocumentsPageQuery } from "./__generated__/DocumentsPageQuery.graphql";
|
||||
import type { DocumentsPageRefetchQuery } from "./__generated__/DocumentsPageRefetchQuery.graphql";
|
||||
import { AuditReportListItem } from "./_components/AuditReportListItem";
|
||||
import { DocumentListItem } from "./_components/DocumentListItem";
|
||||
import { DocumentSection } from "./_components/DocumentSection";
|
||||
import { DocumentsEmpty } from "./_components/DocumentsEmpty";
|
||||
import { DocumentsToolbar } from "./_components/DocumentsToolbar";
|
||||
import { TrustCenterFileListItem } from "./_components/TrustCenterFileListItem";
|
||||
import { groupByField } from "./_lib/groupByField";
|
||||
import { toQueryVariables } from "./_lib/toQueryVariables";
|
||||
import { useDocumentTab } from "./_lib/useDocumentTab";
|
||||
|
||||
export const documentsPageQuery = graphql`
|
||||
query DocumentsPageQuery($visibility: TrustCenterVisibility) {
|
||||
...DocumentsPage_query @arguments(visibility: $visibility)
|
||||
}
|
||||
`;
|
||||
|
||||
const documentsPageFragment = graphql`
|
||||
fragment DocumentsPage_query on Query
|
||||
@refetchable(queryName: "DocumentsPageRefetchQuery")
|
||||
@argumentDefinitions(visibility: { type: "TrustCenterVisibility" }) {
|
||||
currentTrustCenter @required(action: THROW) {
|
||||
documents(first: 250, filter: { visibility: $visibility }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
documentType
|
||||
...DocumentListItem_document
|
||||
}
|
||||
}
|
||||
}
|
||||
audits(first: 250, filter: { visibility: $visibility }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
reportFile {
|
||||
id
|
||||
}
|
||||
...AuditReportListItem_audit
|
||||
}
|
||||
}
|
||||
}
|
||||
trustCenterFiles(first: 250, filter: { visibility: $visibility }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
category
|
||||
...TrustCenterFileListItem_file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface DocumentsPageProps {
|
||||
queryRef: PreloadedQuery<DocumentsPageQuery>;
|
||||
}
|
||||
|
||||
// Trust Center documents page: a unified list of published documents, uploaded
|
||||
// files, and audit reports, grouped into category sections. The All/Public/
|
||||
// Private tabs are backed by a server-side visibility filter.
|
||||
export function DocumentsPage({ queryRef }: DocumentsPageProps) {
|
||||
const { t } = useTranslation("documents");
|
||||
const root = usePreloadedQuery<DocumentsPageQuery>(documentsPageQuery, queryRef);
|
||||
const [data, refetch] = useRefetchableFragment<DocumentsPageRefetchQuery, DocumentsPage_query$key>(
|
||||
documentsPageFragment,
|
||||
root,
|
||||
);
|
||||
|
||||
const { tab } = useDocumentTab();
|
||||
const [isRefetching, startTransition] = useTransition();
|
||||
|
||||
// The initial query already loaded with the URL's tab; only refetch on
|
||||
// subsequent tab changes, inside a transition so the toolbar and current
|
||||
// results stay mounted (dimmed via `isRefetching`) while the slice loads.
|
||||
const isFirstRender = useRef(true);
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
startTransition(() => {
|
||||
refetch(toQueryVariables(tab), { fetchPolicy: "store-or-network" });
|
||||
});
|
||||
}, [refetch, tab]);
|
||||
|
||||
const { currentTrustCenter } = data;
|
||||
const documentNodes = currentTrustCenter.documents.edges.map(edge => edge.node);
|
||||
const fileNodes = currentTrustCenter.trustCenterFiles.edges.map(edge => edge.node);
|
||||
const auditNodes = currentTrustCenter.audits.edges
|
||||
.map(edge => edge.node)
|
||||
.filter(node => node.reportFile != null);
|
||||
|
||||
const total = documentNodes.length + fileNodes.length + auditNodes.length;
|
||||
|
||||
const documentGroups = groupByField(documentNodes, node => node.documentType)
|
||||
.sort((a, b) => t(`types.${a.key}`).localeCompare(t(`types.${b.key}`)));
|
||||
const fileGroups = groupByField(fileNodes, node => node.category)
|
||||
.sort((a, b) => a.key.localeCompare(b.key));
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t("title")} count={total} flushBottomSpace>
|
||||
<DocumentsToolbar />
|
||||
</PageHeader>
|
||||
<div className="flex w-full flex-col items-center px-8 py-8">
|
||||
<div
|
||||
aria-busy={isRefetching}
|
||||
className={`flex w-full max-w-5xl flex-col gap-8 transition-opacity duration-150 ${isRefetching ? "opacity-60" : ""}`}
|
||||
>
|
||||
<ListErrorBoundary
|
||||
onRetry={done => startTransition(() => {
|
||||
refetch(toQueryVariables(tab), { fetchPolicy: "network-only", onComplete: done });
|
||||
})}
|
||||
>
|
||||
{total === 0
|
||||
? <DocumentsEmpty />
|
||||
: (
|
||||
<>
|
||||
{auditNodes.length > 0 && (
|
||||
<DocumentSection title={t("sections.reports")}>
|
||||
{auditNodes.map(node => (
|
||||
<AuditReportListItem key={node.id} auditKey={node} />
|
||||
))}
|
||||
</DocumentSection>
|
||||
)}
|
||||
{documentGroups.map(group => (
|
||||
<DocumentSection key={`type:${group.key}`} title={t(`types.${group.key}`)}>
|
||||
{group.nodes.map(node => (
|
||||
<DocumentListItem key={node.id} documentKey={node} />
|
||||
))}
|
||||
</DocumentSection>
|
||||
))}
|
||||
{fileGroups.map(group => (
|
||||
<DocumentSection key={`category:${group.key}`} title={group.key}>
|
||||
{group.nodes.map(node => (
|
||||
<TrustCenterFileListItem key={node.id} fileKey={node} />
|
||||
))}
|
||||
</DocumentSection>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ListErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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 { useEffect, useRef } from "react";
|
||||
import { useQueryLoader } from "react-relay";
|
||||
|
||||
import type { DocumentsPageQuery } from "./__generated__/DocumentsPageQuery.graphql";
|
||||
import { toQueryVariables } from "./_lib/toQueryVariables";
|
||||
import { useDocumentTab } from "./_lib/useDocumentTab";
|
||||
import { DocumentsPage, documentsPageQuery } from "./DocumentsPage";
|
||||
import { DocumentsPageSkeleton } from "./DocumentsPageSkeleton";
|
||||
|
||||
export default function DocumentsPageLoader() {
|
||||
const { tab } = useDocumentTab();
|
||||
const [queryRef, loadQuery] = useQueryLoader<DocumentsPageQuery>(documentsPageQuery);
|
||||
|
||||
// Seed the first fetch with the URL's tab; later changes are handled by the
|
||||
// page's refetch.
|
||||
const initialVariables = useRef(toQueryVariables(tab));
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery(initialVariables.current);
|
||||
}, [loadQuery]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <DocumentsPageSkeleton />;
|
||||
}
|
||||
|
||||
return <DocumentsPage queryRef={queryRef} />;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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 { TabsSkeleton } from "@probo/ui/src/v2/Tabs/TabsSkeleton";
|
||||
import { HeadingSkeleton } from "@probo/ui/src/v2/typography/HeadingSkeleton";
|
||||
import { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
|
||||
|
||||
import { HeaderBand } from "#/components/HeaderBand/HeaderBand";
|
||||
|
||||
const SECTION_PLACEHOLDERS = ["a", "b"];
|
||||
const ROW_PLACEHOLDERS = ["x", "y", "z"];
|
||||
|
||||
export function DocumentsPageSkeleton() {
|
||||
return (
|
||||
<>
|
||||
<HeaderBand flushBottomSpace>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<HeadingSkeleton size={7} className="w-64" />
|
||||
<TabsSkeleton />
|
||||
</div>
|
||||
</HeaderBand>
|
||||
<div className="flex w-full flex-col items-center px-8 py-8">
|
||||
<div className="flex w-full max-w-5xl flex-col gap-8">
|
||||
{SECTION_PLACEHOLDERS.map(section => (
|
||||
<div key={section} className="flex flex-col gap-3">
|
||||
<TextSkeleton size={3} className="w-40" />
|
||||
<div className="overflow-hidden rounded-4 border border-sand-a4 bg-sand-1">
|
||||
{ROW_PLACEHOLDERS.map(row => (
|
||||
<div key={row} className="h-16 animate-pulse border-b border-sand-a3 bg-sand-2 last:border-b-0" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import { useMutation } from "#/lib/relay/useMutation";
|
||||
|
||||
import { openExportedFile } from "../_lib/openExportedFile";
|
||||
|
||||
import type { AuditReportListItem_audit$key } from "./__generated__/AuditReportListItem_audit.graphql";
|
||||
import type { AuditReportListItemExportMutation } from "./__generated__/AuditReportListItemExportMutation.graphql";
|
||||
import { DocumentAccessAction } from "./DocumentAccessAction";
|
||||
import { documentListItem } from "./variants";
|
||||
|
||||
const auditReportListItemFragment = graphql`
|
||||
fragment AuditReportListItem_audit on Audit @throwOnFieldError {
|
||||
framework {
|
||||
name
|
||||
}
|
||||
reportFile {
|
||||
id
|
||||
fileName
|
||||
isUserAuthorized
|
||||
access {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportReportMutation = graphql`
|
||||
mutation AuditReportListItemExportMutation($input: ExportReportPDFInput!) {
|
||||
exportReportPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface AuditReportListItemProps {
|
||||
auditKey: AuditReportListItem_audit$key;
|
||||
}
|
||||
|
||||
// A single audit report row: the framework name, the report file name, and an
|
||||
// access action that opens the exported report when the viewer is authorized.
|
||||
// Renders nothing when the audit has no report file.
|
||||
export function AuditReportListItem({ auditKey }: AuditReportListItemProps) {
|
||||
const audit = useFragment(auditReportListItemFragment, auditKey);
|
||||
const [exportReport, isExporting] = useMutation<AuditReportListItemExportMutation>(exportReportMutation);
|
||||
const { root, content } = documentListItem();
|
||||
|
||||
const report = audit.reportFile;
|
||||
if (report == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleView = () => {
|
||||
exportReport({
|
||||
variables: { input: { reportId: report.id } },
|
||||
onCompleted: response => openExportedFile(response.exportReportPDF.data),
|
||||
}).catch(() => {
|
||||
// The mutation failure is already surfaced through a toast.
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={root()}>
|
||||
<div className={content()}>
|
||||
<Text size={2} weight="medium" color="neutral" highContrast className="truncate">
|
||||
{audit.framework.name}
|
||||
</Text>
|
||||
<Text size={1} color="gold" className="truncate">
|
||||
{report.fileName}
|
||||
</Text>
|
||||
</div>
|
||||
<DocumentAccessAction
|
||||
isAuthorized={report.isUserAuthorized}
|
||||
requested={report.access?.status === "REQUESTED"}
|
||||
onView={handleView}
|
||||
isViewing={isExporting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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 { ArrowSquareOutIcon, ClockIcon, LockSimpleIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@probo/ui/src/v2/Button/Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DocumentAccessActionProps {
|
||||
// Whether the viewer may open the document (public or granted access).
|
||||
isAuthorized: boolean;
|
||||
// Whether an access request is already pending for this document.
|
||||
requested: boolean;
|
||||
// Opens the document; only invoked when authorized.
|
||||
onView: () => void;
|
||||
// Whether the export/open is in flight.
|
||||
isViewing: boolean;
|
||||
}
|
||||
|
||||
// Trailing access control for a document entry: "View" when authorized, a
|
||||
// pending label when access was requested, otherwise a (currently inert) "Get
|
||||
// Access" call to action.
|
||||
export function DocumentAccessAction({ isAuthorized, requested, onView, isViewing }: DocumentAccessActionProps) {
|
||||
const { t } = useTranslation("documents");
|
||||
|
||||
if (isAuthorized) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
highContrast
|
||||
iconStart={<ArrowSquareOutIcon />}
|
||||
loading={isViewing}
|
||||
onClick={onView}
|
||||
>
|
||||
{t("actions.view")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (requested) {
|
||||
return (
|
||||
<Button variant="ghost" color="neutral" disabled iconStart={<ClockIcon />}>
|
||||
{t("actions.requested")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="ghost" color="neutral" highContrast iconStart={<LockSimpleIcon />}>
|
||||
{t("actions.getAccess")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import { useMutation } from "#/lib/relay/useMutation";
|
||||
|
||||
import { openExportedFile } from "../_lib/openExportedFile";
|
||||
|
||||
import type { DocumentListItem_document$key } from "./__generated__/DocumentListItem_document.graphql";
|
||||
import type { DocumentListItemExportMutation } from "./__generated__/DocumentListItemExportMutation.graphql";
|
||||
import { DocumentAccessAction } from "./DocumentAccessAction";
|
||||
import { documentListItem } from "./variants";
|
||||
|
||||
const documentListItemFragment = graphql`
|
||||
fragment DocumentListItem_document on Document @throwOnFieldError {
|
||||
id
|
||||
title
|
||||
documentType
|
||||
isUserAuthorized
|
||||
access {
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportDocumentMutation = graphql`
|
||||
mutation DocumentListItemExportMutation($input: ExportDocumentPDFInput!) {
|
||||
exportDocumentPDF(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface DocumentListItemProps {
|
||||
documentKey: DocumentListItem_document$key;
|
||||
}
|
||||
|
||||
// A single Probo document row: title, its document type, and an access action
|
||||
// that opens the exported PDF when the viewer is authorized.
|
||||
export function DocumentListItem({ documentKey }: DocumentListItemProps) {
|
||||
const { t } = useTranslation("documents");
|
||||
const document = useFragment(documentListItemFragment, documentKey);
|
||||
const [exportDocument, isExporting] = useMutation<DocumentListItemExportMutation>(exportDocumentMutation);
|
||||
const { root, content } = documentListItem();
|
||||
|
||||
const handleView = () => {
|
||||
exportDocument({
|
||||
variables: { input: { documentId: document.id } },
|
||||
onCompleted: response => openExportedFile(response.exportDocumentPDF.data),
|
||||
}).catch(() => {
|
||||
// The mutation failure is already surfaced through a toast.
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={root()}>
|
||||
<div className={content()}>
|
||||
<Text size={2} weight="medium" color="neutral" highContrast className="truncate">
|
||||
{document.title}
|
||||
</Text>
|
||||
<Text size={1} color="gold" className="truncate">
|
||||
{t(`types.${document.documentType}`)}
|
||||
</Text>
|
||||
</div>
|
||||
<DocumentAccessAction
|
||||
isAuthorized={document.isUserAuthorized}
|
||||
requested={document.access?.status === "REQUESTED"}
|
||||
onView={handleView}
|
||||
isViewing={isExporting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { documentSection } from "./variants";
|
||||
|
||||
interface DocumentSectionProps {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
// The list rows (document / file / report list items).
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// One category group: a localized header above a bordered list of rows.
|
||||
export function DocumentSection({ title, description, children }: DocumentSectionProps) {
|
||||
const { root, header, list } = documentSection();
|
||||
|
||||
return (
|
||||
<section className={root()}>
|
||||
<div className={header()}>
|
||||
<Text size={3} weight="medium" color="neutral" highContrast role="heading" aria-level={2}>
|
||||
{title}
|
||||
</Text>
|
||||
{description != null && (
|
||||
<Text size={2} color="neutral">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className={list()}>{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 { FileTextIcon } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { EmptyState } from "#/components/EmptyState/EmptyState";
|
||||
|
||||
import { useDocumentTab } from "../_lib/useDocumentTab";
|
||||
|
||||
// Empty state for the documents list. When a Public/Private tab is active it
|
||||
// notes the filter; otherwise it states the trust center publishes no documents.
|
||||
export function DocumentsEmpty() {
|
||||
const { t } = useTranslation("documents");
|
||||
const { tab } = useDocumentTab();
|
||||
const filtered = tab !== "all";
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
icon={<FileTextIcon />}
|
||||
title={filtered ? t("empty.filteredTitle") : t("empty.title")}
|
||||
description={filtered ? t("empty.filteredDescription") : t("empty.description")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 { Tabs } from "@probo/ui/src/v2/Tabs/Tabs";
|
||||
import { TabsIndicator } from "@probo/ui/src/v2/Tabs/TabsIndicator";
|
||||
import { TabsList } from "@probo/ui/src/v2/Tabs/TabsList";
|
||||
import { TabsTab } from "@probo/ui/src/v2/Tabs/TabsTab";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { DocumentTab } from "../_lib/useDocumentTab";
|
||||
import { DOCUMENT_TABS, useDocumentTab } from "../_lib/useDocumentTab";
|
||||
|
||||
// Access filter for the documents page: All / Public / Private tabs. Writes the
|
||||
// active tab to the URL; the page reacts and refetches the matching slice.
|
||||
export function DocumentsToolbar() {
|
||||
const { t } = useTranslation("documents");
|
||||
const { tab, setTab } = useDocumentTab();
|
||||
|
||||
return (
|
||||
<Tabs value={tab} onValueChange={value => setTab(value as DocumentTab)}>
|
||||
<TabsList>
|
||||
{DOCUMENT_TABS.map(value => (
|
||||
<TabsTab key={value} value={value}>
|
||||
{t(`tabs.${value}`)}
|
||||
</TabsTab>
|
||||
))}
|
||||
<TabsIndicator />
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 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 { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
import { useMutation } from "#/lib/relay/useMutation";
|
||||
|
||||
import { openExportedFile } from "../_lib/openExportedFile";
|
||||
|
||||
import type { TrustCenterFileListItem_file$key } from "./__generated__/TrustCenterFileListItem_file.graphql";
|
||||
import type { TrustCenterFileListItemExportMutation } from "./__generated__/TrustCenterFileListItemExportMutation.graphql";
|
||||
import { DocumentAccessAction } from "./DocumentAccessAction";
|
||||
import { documentListItem } from "./variants";
|
||||
|
||||
const trustCenterFileListItemFragment = graphql`
|
||||
fragment TrustCenterFileListItem_file on TrustCenterFile @throwOnFieldError {
|
||||
id
|
||||
name
|
||||
category
|
||||
isUserAuthorized
|
||||
access {
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const exportTrustCenterFileMutation = graphql`
|
||||
mutation TrustCenterFileListItemExportMutation($input: ExportTrustCenterFileInput!) {
|
||||
exportTrustCenterFile(input: $input) {
|
||||
data
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface TrustCenterFileListItemProps {
|
||||
fileKey: TrustCenterFileListItem_file$key;
|
||||
}
|
||||
|
||||
// A single uploaded trust-center file row: name, its category, and an access
|
||||
// action that opens the exported file when the viewer is authorized.
|
||||
export function TrustCenterFileListItem({ fileKey }: TrustCenterFileListItemProps) {
|
||||
const file = useFragment(trustCenterFileListItemFragment, fileKey);
|
||||
const [exportFile, isExporting] = useMutation<TrustCenterFileListItemExportMutation>(exportTrustCenterFileMutation);
|
||||
const { root, content } = documentListItem();
|
||||
|
||||
const handleView = () => {
|
||||
exportFile({
|
||||
variables: { input: { trustCenterFileId: file.id } },
|
||||
onCompleted: response => openExportedFile(response.exportTrustCenterFile.data),
|
||||
}).catch(() => {
|
||||
// The mutation failure is already surfaced through a toast.
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={root()}>
|
||||
<div className={content()}>
|
||||
<Text size={2} weight="medium" color="neutral" highContrast className="truncate">
|
||||
{file.name}
|
||||
</Text>
|
||||
<Text size={1} color="gold" className="truncate">
|
||||
{file.category}
|
||||
</Text>
|
||||
</div>
|
||||
<DocumentAccessAction
|
||||
isAuthorized={file.isUserAuthorized}
|
||||
requested={file.access?.status === "REQUESTED"}
|
||||
onView={handleView}
|
||||
isViewing={isExporting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Document list: category sections, each a titled block above a bordered list.
|
||||
// Entries share a common layout (title + accent metadata + trailing access
|
||||
// action).
|
||||
|
||||
export const documentSection = tv({
|
||||
slots: {
|
||||
root: "flex flex-col gap-3",
|
||||
header: "flex flex-col gap-0.5",
|
||||
list: "overflow-hidden rounded-4 border border-sand-a4 bg-sand-1",
|
||||
},
|
||||
});
|
||||
|
||||
export const documentListItem = tv({
|
||||
slots: {
|
||||
root: "flex items-center gap-4 border-b border-sand-a3 px-4 py-3 last:border-b-0",
|
||||
content: "flex min-w-0 flex-1 flex-col gap-0.5",
|
||||
},
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
export interface FieldGroup<T> {
|
||||
key: string;
|
||||
nodes: T[];
|
||||
}
|
||||
|
||||
// Groups nodes by a derived string key, preserving first-seen order of the
|
||||
// keys. Presentational only — the server already applied the visibility filter.
|
||||
export function groupByField<T>(
|
||||
nodes: readonly T[],
|
||||
getKey: (node: T) => string,
|
||||
): FieldGroup<T>[] {
|
||||
const groups = new Map<string, T[]>();
|
||||
|
||||
for (const node of nodes) {
|
||||
const key = getKey(node);
|
||||
const existing = groups.get(key);
|
||||
if (existing) {
|
||||
existing.push(node);
|
||||
} else {
|
||||
groups.set(key, [node]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...groups.entries()].map(([key, groupNodes]) => ({ key, nodes: groupNodes }));
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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.
|
||||
|
||||
// Opens a base64 data URI (returned by the export mutations) in a new browser
|
||||
// tab. Decodes to a Blob so the object URL carries the right MIME type and the
|
||||
// browser previews the PDF inline instead of navigating to a huge data: URL.
|
||||
export function openExportedFile(dataUri: string): void {
|
||||
const commaIndex = dataUri.indexOf(",");
|
||||
const base64 = commaIndex === -1 ? dataUri : dataUri.slice(commaIndex + 1);
|
||||
const mimeMatch = dataUri.match(/^data:([^;]+);/);
|
||||
const mimeType = mimeMatch?.[1] ?? "application/octet-stream";
|
||||
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
|
||||
const blob = new Blob([bytes], { type: mimeType });
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
window.open(objectUrl, "_blank", "noopener,noreferrer");
|
||||
|
||||
// Give the new tab time to claim the URL before releasing it.
|
||||
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 60_000);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 { DocumentsPageQuery$variables } from "../__generated__/DocumentsPageQuery.graphql";
|
||||
|
||||
import type { DocumentTab } from "./useDocumentTab";
|
||||
|
||||
// Maps the active tab to the typed GraphQL visibility variable. "All" omits the
|
||||
// filter (both public and private); the other tabs pin a single visibility so
|
||||
// the server returns just that slice.
|
||||
export function toQueryVariables(tab: DocumentTab): DocumentsPageQuery$variables {
|
||||
switch (tab) {
|
||||
case "public":
|
||||
return { visibility: "PUBLIC" };
|
||||
case "private":
|
||||
return { visibility: "PRIVATE" };
|
||||
default:
|
||||
return { visibility: null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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 { useCallback } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
export type DocumentTab = "all" | "public" | "private";
|
||||
|
||||
export const DOCUMENT_TABS: readonly DocumentTab[] = ["all", "public", "private"];
|
||||
|
||||
interface DocumentTabState {
|
||||
tab: DocumentTab;
|
||||
setTab: (value: DocumentTab) => void;
|
||||
}
|
||||
|
||||
// Active documents tab (All / Public / Private), persisted in the URL so it is
|
||||
// shareable and survives reloads. Pure URL state — no local state or effects —
|
||||
// so the loader, page, and toolbar can all read it without racing.
|
||||
export function useDocumentTab(): DocumentTabState {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const raw = searchParams.get("tab");
|
||||
const tab: DocumentTab = raw === "public" || raw === "private" ? raw : "all";
|
||||
|
||||
const setTab = useCallback((value: DocumentTab) => {
|
||||
setSearchParams((previous) => {
|
||||
const next = new URLSearchParams(previous);
|
||||
if (value === "all") {
|
||||
next.delete("tab");
|
||||
} else {
|
||||
next.set("tab", value);
|
||||
}
|
||||
return next;
|
||||
}, { replace: true });
|
||||
}, [setSearchParams]);
|
||||
|
||||
return { tab, setTab };
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "Documents",
|
||||
"tabs": {
|
||||
"all": "All",
|
||||
"public": "Public",
|
||||
"private": "Private"
|
||||
},
|
||||
"sections": {
|
||||
"reports": "Compliance reports"
|
||||
},
|
||||
"types": {
|
||||
"OTHER": "Other",
|
||||
"GOVERNANCE": "Governance",
|
||||
"POLICY": "Policies",
|
||||
"PROCEDURE": "Procedures",
|
||||
"PLAN": "Plans",
|
||||
"REGISTER": "Registers",
|
||||
"RECORD": "Records",
|
||||
"REPORT": "Reports",
|
||||
"TEMPLATE": "Templates",
|
||||
"STATEMENT_OF_APPLICABILITY": "Statement of Applicability"
|
||||
},
|
||||
"actions": {
|
||||
"view": "View",
|
||||
"getAccess": "Get Access",
|
||||
"requested": "Access requested"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No documents available",
|
||||
"description": "This trust center has not published any documents yet.",
|
||||
"filteredTitle": "No documents match this filter",
|
||||
"filteredDescription": "Try a different tab to see available documents."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "Documents",
|
||||
"tabs": {
|
||||
"all": "Tous",
|
||||
"public": "Public",
|
||||
"private": "Privé"
|
||||
},
|
||||
"sections": {
|
||||
"reports": "Rapports de conformité"
|
||||
},
|
||||
"types": {
|
||||
"OTHER": "Autres",
|
||||
"GOVERNANCE": "Gouvernance",
|
||||
"POLICY": "Politiques",
|
||||
"PROCEDURE": "Procédures",
|
||||
"PLAN": "Plans",
|
||||
"REGISTER": "Registres",
|
||||
"RECORD": "Enregistrements",
|
||||
"REPORT": "Rapports",
|
||||
"TEMPLATE": "Modèles",
|
||||
"STATEMENT_OF_APPLICABILITY": "Déclaration d'applicabilité"
|
||||
},
|
||||
"actions": {
|
||||
"view": "Consulter",
|
||||
"getAccess": "Obtenir l'accès",
|
||||
"requested": "Accès demandé"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Aucun document disponible",
|
||||
"description": "Ce trust center n'a pas encore publié de documents.",
|
||||
"filteredTitle": "Aucun document ne correspond à ce filtre",
|
||||
"filteredDescription": "Essayez un autre onglet pour voir les documents disponibles."
|
||||
}
|
||||
}
|
||||
32
apps/compliance-portal/src/pages/documents/routes.ts
Normal file
32
apps/compliance-portal/src/pages/documents/routes.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 { lazy } from "@probo/react-lazy";
|
||||
import type { AppRoute } from "@probo/routes";
|
||||
|
||||
import { DocumentsPageSkeleton } from "./DocumentsPageSkeleton";
|
||||
|
||||
export const documentRoutes = [
|
||||
{
|
||||
path: "documents",
|
||||
Fallback: DocumentsPageSkeleton,
|
||||
Component: lazy(() => import("./DocumentsPageLoader")),
|
||||
},
|
||||
] satisfies AppRoute[];
|
||||
@@ -25,6 +25,7 @@ import { createBrowserRouter } from "react-router";
|
||||
import { PageErrorBoundary } from "#/components/errors/PageErrorBoundary";
|
||||
import { RootErrorBoundary } from "#/components/errors/RootErrorBoundary";
|
||||
import { getPathPrefix } from "#/lib/http/pathPrefix";
|
||||
import { documentRoutes } from "#/pages/documents/routes";
|
||||
import { HomePageSkeleton } from "#/pages/HomePageSkeleton";
|
||||
import { MainLayoutSkeleton } from "#/pages/MainLayoutSkeleton";
|
||||
import { subprocessorRoutes } from "#/pages/subprocessors/routes";
|
||||
@@ -48,10 +49,7 @@ const routes = [
|
||||
Fallback: HomePageSkeleton,
|
||||
Component: lazy(() => import("#/pages/HomePageLoader")),
|
||||
},
|
||||
{
|
||||
path: "documents",
|
||||
Component: lazy(() => import("#/pages/DocumentsPage")),
|
||||
},
|
||||
...documentRoutes,
|
||||
...subprocessorRoutes,
|
||||
...updateRoutes,
|
||||
{
|
||||
|
||||
70
packages/ui/src/v2/Tabs/Tabs.stories.tsx
Normal file
70
packages/ui/src/v2/Tabs/Tabs.stories.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 { useState } from "react";
|
||||
|
||||
import { Tabs } from "./Tabs";
|
||||
import { TabsIndicator } from "./TabsIndicator";
|
||||
import { TabsList } from "./TabsList";
|
||||
import { TabsSkeleton } from "./TabsSkeleton";
|
||||
import { TabsTab } from "./TabsTab";
|
||||
|
||||
export default {
|
||||
title: "v2/Tabs",
|
||||
component: Tabs,
|
||||
};
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Tabs defaultValue="all">
|
||||
<TabsList>
|
||||
<TabsTab value="all">All</TabsTab>
|
||||
<TabsTab value="public">Public</TabsTab>
|
||||
<TabsTab value="private">Private</TabsTab>
|
||||
<TabsIndicator />
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
export function Controlled() {
|
||||
const [value, setValue] = useState<string>("all");
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Tabs value={value} onValueChange={next => setValue(next as string)}>
|
||||
<TabsList>
|
||||
<TabsTab value="all">All</TabsTab>
|
||||
<TabsTab value="public">Public</TabsTab>
|
||||
<TabsTab value="private">Private</TabsTab>
|
||||
<TabsIndicator />
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<span className="text-2 text-sand-11">
|
||||
Selected:
|
||||
{" "}
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Skeleton() {
|
||||
return <TabsSkeleton />;
|
||||
}
|
||||
@@ -18,13 +18,12 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
||||
|
||||
import { PageHeader } from "#/components/PageHeader/PageHeader";
|
||||
// Root of the tabs (Radix "Tabs"). Controlled the same way as Base UI's Tabs:
|
||||
// `value` / `onValueChange`, or left uncontrolled. See contrib/claude/ui.md.
|
||||
export type TabsProps = BaseTabs.Root.Props;
|
||||
|
||||
// Toolbar (All/Public/Private tabs, framework filter, search) and the document
|
||||
// count are deferred until the v2 Tabs/Select/TextField components exist.
|
||||
export default function DocumentsPage() {
|
||||
const { t } = useTranslation();
|
||||
return <PageHeader title={t("documents.title")} />;
|
||||
export function Tabs(props: TabsProps) {
|
||||
return <BaseTabs.Root {...props} />;
|
||||
}
|
||||
36
packages/ui/src/v2/Tabs/TabsIndicator.tsx
Normal file
36
packages/ui/src/v2/Tabs/TabsIndicator.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { tabsIndicator } from "./variants";
|
||||
|
||||
export type TabsIndicatorProps = Omit<ComponentProps<typeof BaseTabs.Indicator>, "className"> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// The sliding underline tracking the active tab, positioned from Base UI's
|
||||
// `--active-tab-left` / `--active-tab-width` CSS variables.
|
||||
export function TabsIndicator(props: TabsIndicatorProps) {
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return <BaseTabs.Indicator className={tabsIndicator({ className })} {...rest} />;
|
||||
}
|
||||
35
packages/ui/src/v2/Tabs/TabsList.tsx
Normal file
35
packages/ui/src/v2/Tabs/TabsList.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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 { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { tabsList } from "./variants";
|
||||
|
||||
export type TabsListProps = Omit<ComponentProps<typeof BaseTabs.List>, "className"> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// The tab bar: holds the tab triggers and (optionally) the sliding indicator.
|
||||
export function TabsList(props: TabsListProps) {
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return <BaseTabs.List className={tabsList({ className })} {...rest} />;
|
||||
}
|
||||
43
packages/ui/src/v2/Tabs/TabsSkeleton.tsx
Normal file
43
packages/ui/src/v2/Tabs/TabsSkeleton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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 { tabsSkeleton } from "./variants";
|
||||
|
||||
export type TabsSkeletonProps = Omit<ComponentProps<"div">, "children"> & {
|
||||
// Number of placeholder tabs to render (defaults to 3).
|
||||
count?: number;
|
||||
};
|
||||
|
||||
// Loading placeholder paired with the tab bar: a row of pulse blocks over the
|
||||
// shared bottom border.
|
||||
export function TabsSkeleton(props: TabsSkeletonProps) {
|
||||
const { count = 3, className, ...rest } = props;
|
||||
const { root, item } = tabsSkeleton();
|
||||
|
||||
return (
|
||||
<div className={root({ className })} aria-hidden {...rest}>
|
||||
{Array.from({ length: count }, (_, index) => (
|
||||
<span key={index} className={item()} style={{ width: `${64 + (index % 3) * 16}px` }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
packages/ui/src/v2/Tabs/TabsTab.tsx
Normal file
35
packages/ui/src/v2/Tabs/TabsTab.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
// 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 { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { tabsTab } from "./variants";
|
||||
|
||||
export type TabsTabProps = Omit<ComponentProps<typeof BaseTabs.Tab>, "className"> & {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
// A single tab trigger. Selected state is driven by Base UI (`data-active`).
|
||||
export function TabsTab(props: TabsTabProps) {
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return <BaseTabs.Tab className={tabsTab({ className })} {...rest} />;
|
||||
}
|
||||
54
packages/ui/src/v2/Tabs/variants.ts
Normal file
54
packages/ui/src/v2/Tabs/variants.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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";
|
||||
|
||||
// Tabs (Radix "Tabs" over Base UI's Tabs). An underlined tab bar whose active
|
||||
// item is tracked by a sliding indicator positioned from Base UI's
|
||||
// `--active-tab-*` CSS variables.
|
||||
|
||||
export const tabsList = tv({
|
||||
base: "relative flex items-center gap-1 border-b border-sand-a3",
|
||||
});
|
||||
|
||||
export const tabsTab = tv({
|
||||
base: [
|
||||
"relative flex h-full cursor-pointer items-center justify-center gap-2 px-2 py-4 text-2 text-sand-a11",
|
||||
"select-none outline-none transition-colors",
|
||||
"hover:text-sand-12",
|
||||
"focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1",
|
||||
"data-active:font-medium data-active:text-sand-12",
|
||||
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
],
|
||||
});
|
||||
|
||||
export const tabsIndicator = tv({
|
||||
base: [
|
||||
"absolute bottom-0 left-0 h-[2px] w-(--active-tab-width) rounded-1 bg-sand-12",
|
||||
"translate-x-(--active-tab-left) transition-all duration-200 ease-out",
|
||||
],
|
||||
});
|
||||
|
||||
export const tabsSkeleton = tv({
|
||||
slots: {
|
||||
root: "flex items-center gap-4 border-b border-sand-a3",
|
||||
item: "my-4 h-5 animate-pulse rounded-1 bg-sand-3",
|
||||
},
|
||||
});
|
||||
59
packages/ui/src/v2/Toaster/Toaster.tsx
Normal file
59
packages/ui/src/v2/Toaster/Toaster.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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 { Toast } from "@base-ui/react/toast";
|
||||
import { XIcon } from "@phosphor-icons/react";
|
||||
|
||||
import { toaster } from "./variants";
|
||||
|
||||
type ToastType = "success" | "error" | "neutral";
|
||||
|
||||
function resolveType(type: string | undefined): ToastType {
|
||||
return type === "success" || type === "error" ? type : "neutral";
|
||||
}
|
||||
|
||||
// Renders the active toasts from the Base UI toast manager. Mount once at the
|
||||
// app root, inside a `<Toast.Provider>`.
|
||||
export function Toaster() {
|
||||
const { toasts } = Toast.useToastManager();
|
||||
const slots = toaster();
|
||||
|
||||
return (
|
||||
<Toast.Portal>
|
||||
<Toast.Viewport className={slots.viewport()}>
|
||||
{toasts.map((toast) => {
|
||||
const variant = { type: resolveType(toast.type) };
|
||||
|
||||
return (
|
||||
<Toast.Root key={toast.id} toast={toast} className={slots.toast(variant)}>
|
||||
<div className={slots.content()}>
|
||||
<Toast.Title className={slots.title(variant)} />
|
||||
<Toast.Description className={slots.description()} />
|
||||
</div>
|
||||
<Toast.Close className={slots.close()} aria-label="Close">
|
||||
<XIcon />
|
||||
</Toast.Close>
|
||||
</Toast.Root>
|
||||
);
|
||||
})}
|
||||
</Toast.Viewport>
|
||||
</Toast.Portal>
|
||||
);
|
||||
}
|
||||
50
packages/ui/src/v2/Toaster/variants.ts
Normal file
50
packages/ui/src/v2/Toaster/variants.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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";
|
||||
|
||||
// Toaster (Radix "Toast" over Base UI's Toast). A bottom-right stack of
|
||||
// dismissible notifications, colored by type.
|
||||
|
||||
export const toaster = tv({
|
||||
slots: {
|
||||
viewport: "fixed right-4 bottom-4 z-50 flex w-full max-w-sm flex-col gap-2 outline-none",
|
||||
toast: [
|
||||
"pointer-events-auto flex items-start gap-3 rounded-3 border border-sand-a6 bg-sand-1 p-3 shadow-3",
|
||||
"transition-all duration-200 ease-out",
|
||||
"data-starting-style:translate-x-4 data-starting-style:opacity-0",
|
||||
"data-ending-style:translate-x-4 data-ending-style:opacity-0",
|
||||
],
|
||||
content: "flex min-w-0 flex-1 flex-col gap-1",
|
||||
title: "text-2 font-medium text-sand-12",
|
||||
description: "text-2 break-words text-sand-11",
|
||||
close: "flex size-5 shrink-0 items-center justify-center rounded-1 text-sand-a10 outline-none transition-colors hover:bg-sand-3 hover:text-sand-12 [&_svg]:size-4",
|
||||
},
|
||||
variants: {
|
||||
type: {
|
||||
success: { toast: "border-green-6 bg-green-2", title: "text-green-12" },
|
||||
error: { toast: "border-red-6 bg-red-2", title: "text-red-12" },
|
||||
neutral: {},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
type: "neutral",
|
||||
},
|
||||
});
|
||||
@@ -43,6 +43,11 @@ func NewAuditTrustCenterFilter() *AuditFilter {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *AuditFilter) WithTrustCenterVisibilities(visibilities ...TrustCenterVisibility) *AuditFilter {
|
||||
f.trustCenterVisibilities = visibilities
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *AuditFilter) SQLArguments() pgx.NamedArgs {
|
||||
args := pgx.NamedArgs{}
|
||||
|
||||
|
||||
@@ -63,6 +63,11 @@ func (f *DocumentFilter) WithPublished(published *bool) *DocumentFilter {
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *DocumentFilter) WithTrustCenterVisibilities(visibilities ...TrustCenterVisibility) *DocumentFilter {
|
||||
f.trustCenterVisibilities = visibilities
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *DocumentFilter) WithEmployeeIdentityID(identityID *gid.GID, modes ...EmployeeFilterMode) *DocumentFilter {
|
||||
f.employeeIdentityID = identityID
|
||||
f.employeeFilterModes = modes
|
||||
|
||||
@@ -16,6 +16,7 @@ type TrustCenter implements Node {
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
filter: TrustCenterVisibilityFilter
|
||||
): DocumentConnection! @goField(forceResolver: true)
|
||||
|
||||
audits(
|
||||
@@ -23,6 +24,7 @@ type TrustCenter implements Node {
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
filter: TrustCenterVisibilityFilter
|
||||
): AuditConnection! @goField(forceResolver: true)
|
||||
|
||||
subprocessors(
|
||||
@@ -50,6 +52,7 @@ type TrustCenter implements Node {
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
filter: TrustCenterVisibilityFilter
|
||||
): TrustCenterFileConnection! @goField(forceResolver: true)
|
||||
|
||||
complianceFrameworks(
|
||||
@@ -95,6 +98,18 @@ enum DocumentType
|
||||
)
|
||||
}
|
||||
|
||||
enum TrustCenterVisibility
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility") {
|
||||
PRIVATE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityPrivate")
|
||||
PUBLIC
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibilityPublic")
|
||||
}
|
||||
|
||||
input TrustCenterVisibilityFilter {
|
||||
visibility: TrustCenterVisibility
|
||||
}
|
||||
|
||||
type Document implements Node @nda {
|
||||
id: ID!
|
||||
title: String!
|
||||
|
||||
@@ -758,7 +758,7 @@ func (r *trustCenterResolver) Organization(ctx context.Context, obj *types.Trust
|
||||
}
|
||||
|
||||
// Documents is the resolver for the documents field.
|
||||
func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.DocumentConnection, error) {
|
||||
func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey, filter *types.TrustCenterVisibilityFilter) (*types.DocumentConnection, error) {
|
||||
compliancePage := compliancepage.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
|
||||
trustService := r.trust
|
||||
@@ -768,7 +768,12 @@ func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCen
|
||||
}
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
documentPage, err := trustService.Documents.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor)
|
||||
documentFilter := coredata.NewDocumentTrustCenterFilter()
|
||||
if filter != nil && filter.Visibility != nil {
|
||||
documentFilter = documentFilter.WithTrustCenterVisibilities(*filter.Visibility)
|
||||
}
|
||||
|
||||
documentPage, err := trustService.Documents.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, documentFilter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list public documents", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -778,7 +783,7 @@ func (r *trustCenterResolver) Documents(ctx context.Context, obj *types.TrustCen
|
||||
}
|
||||
|
||||
// Audits is the resolver for the audits field.
|
||||
func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.AuditConnection, error) {
|
||||
func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey, filter *types.TrustCenterVisibilityFilter) (*types.AuditConnection, error) {
|
||||
compliancePage := compliancepage.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
|
||||
trustService := r.trust
|
||||
@@ -788,7 +793,12 @@ func (r *trustCenterResolver) Audits(ctx context.Context, obj *types.TrustCenter
|
||||
}
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
auditPage, err := trustService.Audits.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor)
|
||||
auditFilter := coredata.NewAuditTrustCenterFilter()
|
||||
if filter != nil && filter.Visibility != nil {
|
||||
auditFilter = auditFilter.WithTrustCenterVisibilities(*filter.Visibility)
|
||||
}
|
||||
|
||||
auditPage, err := trustService.Audits.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, auditFilter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list public audits", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -888,7 +898,7 @@ func (r *trustCenterResolver) References(ctx context.Context, obj *types.TrustCe
|
||||
}
|
||||
|
||||
// TrustCenterFiles is the resolver for the trustCenterFiles field.
|
||||
func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TrustCenterFileConnection, error) {
|
||||
func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.TrustCenter, first *int, after *page.CursorKey, last *int, before *page.CursorKey, filter *types.TrustCenterVisibilityFilter) (*types.TrustCenterFileConnection, error) {
|
||||
compliancePage := compliancepage.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
|
||||
trustService := r.trust
|
||||
@@ -898,14 +908,19 @@ func (r *trustCenterResolver) TrustCenterFiles(ctx context.Context, obj *types.T
|
||||
}
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
filter := coredata.NewTrustCenterFileFilter(
|
||||
coredata.WithTrustCenterFileVisibilities(
|
||||
coredata.TrustCenterVisibilityPublic,
|
||||
coredata.TrustCenterVisibilityPrivate,
|
||||
),
|
||||
visibilities := []coredata.TrustCenterVisibility{
|
||||
coredata.TrustCenterVisibilityPublic,
|
||||
coredata.TrustCenterVisibilityPrivate,
|
||||
}
|
||||
if filter != nil && filter.Visibility != nil {
|
||||
visibilities = []coredata.TrustCenterVisibility{*filter.Visibility}
|
||||
}
|
||||
|
||||
fileFilter := coredata.NewTrustCenterFileFilter(
|
||||
coredata.WithTrustCenterFileVisibilities(visibilities...),
|
||||
)
|
||||
|
||||
trustCenterFilePage, err := trustService.TrustCenterFiles.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, filter)
|
||||
trustCenterFilePage, err := trustService.TrustCenterFiles.ListForOrganizationId(ctx, scope, obj.Organization.ID, cursor, fileFilter)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list public trust center files", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
|
||||
@@ -88,14 +88,17 @@ func (s AuditService) ListForOrganizationId(
|
||||
scope coredata.Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[coredata.AuditOrderField],
|
||||
filter *coredata.AuditFilter,
|
||||
) (*page.Page[*coredata.Audit, coredata.AuditOrderField], error) {
|
||||
var audits coredata.Audits
|
||||
|
||||
if filter == nil {
|
||||
filter = coredata.NewAuditTrustCenterFilter()
|
||||
}
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
filter := coredata.NewAuditTrustCenterFilter()
|
||||
|
||||
err := audits.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor, filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load audits: %w", err)
|
||||
|
||||
@@ -271,7 +271,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o
|
||||
},
|
||||
)
|
||||
|
||||
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor)
|
||||
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list documents: %w", err)
|
||||
}
|
||||
@@ -345,7 +345,7 @@ func (s *Service) fetchDocumentIDs(ctx context.Context, scope coredata.Scoper, o
|
||||
},
|
||||
)
|
||||
|
||||
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor)
|
||||
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list audits: %w", err)
|
||||
}
|
||||
@@ -454,7 +454,7 @@ func (s *Service) fetchDocuments(ctx context.Context, scope coredata.Scoper, org
|
||||
},
|
||||
)
|
||||
|
||||
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor)
|
||||
result, err := s.Documents.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list documents: %w", err)
|
||||
}
|
||||
@@ -500,7 +500,7 @@ func (s *Service) fetchAudits(ctx context.Context, scope coredata.Scoper, orgID
|
||||
},
|
||||
)
|
||||
|
||||
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor)
|
||||
result, err := s.Audits.ListForOrganizationId(ctx, scope, orgID, cursor, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list audits: %w", err)
|
||||
}
|
||||
|
||||
@@ -55,14 +55,17 @@ func (s *DocumentService) ListForOrganizationId(
|
||||
scope coredata.Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[coredata.DocumentOrderField],
|
||||
filter *coredata.DocumentFilter,
|
||||
) (*page.Page[*coredata.Document, coredata.DocumentOrderField], error) {
|
||||
var documents coredata.Documents
|
||||
|
||||
if filter == nil {
|
||||
filter = coredata.NewDocumentTrustCenterFilter()
|
||||
}
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
filter := coredata.NewDocumentTrustCenterFilter()
|
||||
|
||||
if err := documents.LoadPublishedByOrganizationID(ctx, conn, scope, organizationID, cursor, filter); err != nil {
|
||||
return fmt.Errorf("cannot load published documents: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user