Fix apps/trust lint issues

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-22 14:40:12 +04:00
parent dd23449cc0
commit 6ba2132cfe
23 changed files with 283 additions and 339 deletions

View File

@@ -153,7 +153,6 @@ const routes = [
children: [ children: [
{ {
index: true, index: true,
// Component: () => "hello world",
Component: lazy( Component: lazy(
() => () =>
import("./pages/organizations/employee/EmployeeDocumentsPageLoader"), import("./pages/organizations/employee/EmployeeDocumentsPageLoader"),

View File

@@ -1,6 +1,17 @@
import { browser } from "@probo/eslint-config"; import { defineConfig } from "eslint/config";
import { configs } from "@probo/eslint-config";
export default [ export default defineConfig([
{ ignores: ["dist", "eslint.config.mjs", "*.config.{js,mjs,ts}"] }, ...configs.base,
...browser(["./tsconfig.app.json", "./tsconfig.node.json"], import.meta.dirname), ...configs.ts,
]; ...configs.react,
configs.languageOptions.browser,
...configs.stylistic,
{
languageOptions: {
parserOptions: {
tsConfigRootDir: import.meta.dirname,
},
},
},
]);

View File

@@ -68,10 +68,10 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
audit.report?.hasUserRequestedAccess, audit.report?.hasUserRequestedAccess,
); );
const [requestAccess, isRequestingAccess] = const [requestAccess, isRequestingAccess]
useMutation<AuditRow_requestAccessMutation>(requestAccessMutation); = useMutation<AuditRow_requestAccessMutation>(requestAccessMutation);
const [commitDownload, downloading] = const [commitDownload, downloading]
useMutationWithToasts<AuditRowDownloadMutation>(downloadMutation); = useMutationWithToasts<AuditRowDownloadMutation>(downloadMutation);
const handleRequestAccess = () => { const handleRequestAccess = () => {
requestAccess({ requestAccess({
@@ -106,11 +106,11 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
}); });
}; };
const handleDownload = () => { const handleDownload = async () => {
if (!audit.report?.id) { if (!audit.report?.id) {
return; return;
} }
commitDownload({ await commitDownload({
variables: { variables: {
input: { input: {
reportId: audit.report.id, reportId: audit.report.id,
@@ -128,17 +128,20 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
<IconMedal size={16} className="flex-none text-txt-tertiary" /> <IconMedal size={16} className="flex-none text-txt-tertiary" />
{audit.framework.name} {audit.framework.name}
</div> </div>
{audit.report && audit.report.isUserAuthorized ? ( {audit.report && audit.report.isUserAuthorized
? (
<Button <Button
className="w-full md:w-max" className="w-full md:w-max"
variant="secondary" variant="secondary"
disabled={downloading} disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox} icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload} onClick={() => void handleDownload()}
> >
{__("Download")} {__("Download")}
</Button> </Button>
) : viewer ? ( )
: viewer
? (
<Button <Button
disabled={hasRequested || isRequestingAccess} disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max" className="w-full md:w-max"
@@ -148,7 +151,8 @@ export function AuditRow(props: { audit: AuditRowFragment$key }) {
> >
{hasRequested ? __("Access requested") : __("Request access")} {hasRequested ? __("Access requested") : __("Request access")}
</Button> </Button>
) : ( )
: (
<Button <Button
className="w-full md:w-max" className="w-full md:w-max"
variant="secondary" variant="secondary"

View File

@@ -56,10 +56,10 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
document.hasUserRequestedAccess, document.hasUserRequestedAccess,
); );
const [requestAccess, isRequestingAccess] = const [requestAccess, isRequestingAccess]
useMutation<DocumentRow_requestAccessMutation>(requestAccessMutation); = useMutation<DocumentRow_requestAccessMutation>(requestAccessMutation);
const [commitDownload, downloading] = const [commitDownload, downloading]
useMutationWithToasts<DocumentRowDownloadMutation>(downloadMutation); = useMutationWithToasts<DocumentRowDownloadMutation>(downloadMutation);
const handleRequestAccess = () => { const handleRequestAccess = () => {
requestAccess({ requestAccess({
@@ -94,8 +94,8 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
}); });
}; };
const handleDownload = () => { const handleDownload = async () => {
commitDownload({ await commitDownload({
variables: { variables: {
input: { input: {
documentId: document.id, documentId: document.id,
@@ -113,17 +113,20 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" /> <IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
{document.title} {document.title}
</div> </div>
{document.isUserAuthorized ? ( {document.isUserAuthorized
? (
<Button <Button
className="w-full md:w-max" className="w-full md:w-max"
variant="secondary" variant="secondary"
disabled={downloading} disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox} icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload} onClick={() => void handleDownload()}
> >
{__("Download")} {__("Download")}
</Button> </Button>
) : viewer ? ( )
: viewer
? (
<Button <Button
disabled={hasRequested || isRequestingAccess} disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max" className="w-full md:w-max"
@@ -133,7 +136,8 @@ export function DocumentRow(props: { document: DocumentRowFragment$key }) {
> >
{hasRequested ? __("Access requested") : __("Request access")} {hasRequested ? __("Access requested") : __("Request access")}
</Button> </Button>
) : ( )
: (
<Button <Button
className="w-full md:w-max" className="w-full md:w-max"
variant="secondary" variant="secondary"

View File

@@ -60,8 +60,8 @@ export function NDADialog({
}, },
}); });
const handleSubmit = handleSubmitWrapper(({ fullName }) => { const handleSubmit = handleSubmitWrapper(async ({ fullName }) => {
commitSigning({ await commitSigning({
variables: { variables: {
input: { input: {
fullName, fullName,
@@ -98,7 +98,7 @@ export function NDADialog({
</Button> </Button>
</Card> </Card>
)} )}
<form onSubmit={handleSubmit}> <form onSubmit={e => void handleSubmit(e)}>
<div className="mt-4"> <div className="mt-4">
<Field <Field
required required
@@ -129,7 +129,9 @@ export function NDADialog({
isMobile ? "mt-15" : "mt-30", isMobile ? "mt-15" : "mt-30",
)} )}
> >
Powered by <Logo withPicto className="h-6" /> Powered by
{" "}
<Logo withPicto className="h-6" />
</a> </a>
</div> </div>
{isDesktop && ( {isDesktop && (

View File

@@ -35,8 +35,8 @@ export function OrganizationSidebar({
const isAuthenticated = !!use(Viewer); const isAuthenticated = !!use(Viewer);
const { toast } = useToast(); const { toast } = useToast();
const [requestAllAccesses, isRequestingAccess] = const [requestAllAccesses, isRequestingAccess]
useMutation<OrganizationSidebar_requestAllAccessesMutation>( = useMutation<OrganizationSidebar_requestAllAccessesMutation>(
requestAllAccessesMutation, requestAllAccessesMutation,
); );
@@ -75,13 +75,15 @@ export function OrganizationSidebar({
return ( return (
<Card className="p-6 relative overflow-hidden border-b border-border-low isolate"> <Card className="p-6 relative overflow-hidden border-b border-border-low isolate">
<div className="h-21 bg-[#044E4114] absolute top-0 left-0 right-0 -z-1"></div> <div className="h-21 bg-[#044E4114] absolute top-0 left-0 right-0 -z-1"></div>
{trustCenter.organization.logoUrl ? ( {trustCenter.organization.logoUrl
? (
<img <img
alt="" alt=""
src={trustCenter.organization.logoUrl} src={trustCenter.organization.logoUrl}
className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1" className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1"
/> />
) : ( )
: (
<div className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1" /> <div className="size-24 rounded-2xl border border-border-mid shadow-mid bg-level-1" />
)} )}
<h1 className="text-2xl mt-6">{trustCenter.organization.name}</h1> <h1 className="text-2xl mt-6">{trustCenter.organization.name}</h1>
@@ -137,7 +139,7 @@ export function OrganizationSidebar({
gridTemplateColumns: "repeat(auto-fit, 75px", gridTemplateColumns: "repeat(auto-fit, 75px",
}} }}
> >
{trustCenter.audits.edges.map((audit) => ( {trustCenter.audits.edges.map(audit => (
<AuditRowAvatar key={audit.node.id} audit={audit.node} /> <AuditRowAvatar key={audit.node.id} audit={audit.node} />
))} ))}
</div> </div>
@@ -148,7 +150,8 @@ export function OrganizationSidebar({
)} )}
{/* Actions */} {/* Actions */}
{isAuthenticated ? ( {isAuthenticated
? (
<Button <Button
disabled={isRequestingAccess} disabled={isRequestingAccess}
variant="primary" variant="primary"
@@ -158,7 +161,8 @@ export function OrganizationSidebar({
> >
{__("Request access")} {__("Request access")}
</Button> </Button>
) : ( )
: (
<Button <Button
variant="primary" variant="primary"
icon={IconLock} icon={IconLock}

View File

@@ -15,8 +15,8 @@ import { IconMinusLarge } from "@probo/ui/src/Atoms/Icons/IconMinusLarge.tsx";
// Worker for PDF.js // Worker for PDF.js
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
const btnClass = const btnClass
"size-8 grid place-items-center hover:bg-secondary-hover cursor-pointer rounded-sm disabled:opacity-30 transition-all"; = "size-8 grid place-items-center hover:bg-secondary-hover cursor-pointer rounded-sm disabled:opacity-30 transition-all";
export function PDFPreview({ src, name }: { src: string; name?: string }) { export function PDFPreview({ src, name }: { src: string; name?: string }) {
const [numPages, setNumPages] = useState(0); const [numPages, setNumPages] = useState(0);
@@ -92,7 +92,10 @@ export function PDFPreview({ src, name }: { src: string; name?: string }) {
<IconChevronLeft size={16} /> <IconChevronLeft size={16} />
</button> </button>
<div> <div>
{currentPage} / {numPages} {currentPage}
{" "}
/
{numPages}
</div> </div>
<button onClick={movePage(1)} className={btnClass}> <button onClick={movePage(1)} className={btnClass}>
<IconChevronRight size={16} /> <IconChevronRight size={16} />
@@ -122,7 +125,7 @@ export function PDFPreview({ src, name }: { src: string; name?: string }) {
ref={documentRef} ref={documentRef}
> >
{numPages === 0 && <Spinner className="mx-auto" />} {numPages === 0 && <Spinner className="mx-auto" />}
{times(numPages, (index) => ( {times(numPages, index => (
<Page <Page
className="w-max h-max mx-auto shadow-mid" className="w-max h-max mx-auto shadow-mid"
key={index.toString()} key={index.toString()}

View File

@@ -25,14 +25,14 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
// Reset error boundary on page change // Reset error boundary on page change
useEffect(() => { useEffect(() => {
if ( if (
location.pathname !== baseLocation.current.pathname && location.pathname !== baseLocation.current.pathname
resetErrorBoundary && resetErrorBoundary
) { ) {
resetErrorBoundary(); resetErrorBoundary();
} }
}, [location, resetErrorBoundary]); }, [location, resetErrorBoundary]);
if (!error || (error && error.toString().includes("PAGE_NOT_FOUND"))) { if (!error || (error instanceof Error && error.message.includes("PAGE_NOT_FOUND"))) {
return ( return (
<div className={classNames.wrapper}> <div className={classNames.wrapper}>
<h1 className={classNames.title}> <h1 className={classNames.title}>
@@ -46,22 +46,21 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
); );
} }
if ( if (error instanceof Error
error && error.message
.toString()
.toLowerCase() .toLowerCase()
.match(/(token|expired|invalid|401|unauthorized)/) .match(/(token|expired|invalid|401|unauthorized)/)
) { ) {
const isExpiredToken = error.toString().toLowerCase().includes("expired"); const isExpiredToken = error.message.toLowerCase().includes("expired");
const title = isExpiredToken const title = isExpiredToken
? __("Expired token") ? __("Expired token")
: __("Invalid Access Link"); : __("Invalid Access Link");
const description = isExpiredToken const description = isExpiredToken
? __( ? __(
"This access link has expired. Trust center access links are valid for 7 days for security reasons." "This access link has expired. Trust center access links are valid for 7 days for security reasons.",
) )
: __( : __(
"This access link is not valid. It may have been revoked or the link might be incorrect." "This access link is not valid. It may have been revoked or the link might be incorrect.",
); );
return ( return (
<div className={classNames.wrapper}> <div className={classNames.wrapper}>
@@ -81,7 +80,8 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
<summary className={classNames.description}> <summary className={classNames.description}>
{__("Something went wrong")} {__("Something went wrong")}
</summary> </summary>
<p className={classNames.detail}>{error.toString()}</p> {error instanceof Error
&& <p className={classNames.detail}>{error.message}</p>}
</details> </details>
</div> </div>
); );

View File

@@ -58,12 +58,12 @@ export function TrustCenterFileRow(props: {
const file = useFragment(trustCenterFileRowFragment, props.file); const file = useFragment(trustCenterFileRowFragment, props.file);
const [hasRequested, setHasRequested] = useState(file.hasUserRequestedAccess); const [hasRequested, setHasRequested] = useState(file.hasUserRequestedAccess);
const [requestAccess, isRequestingAccess] = const [requestAccess, isRequestingAccess]
useMutation<TrustCenterFileRow_requestAccessMutation>( = useMutation<TrustCenterFileRow_requestAccessMutation>(
requestAccessMutation, requestAccessMutation,
); );
const [commitDownload, downloading] = const [commitDownload, downloading]
useMutationWithToasts<TrustCenterFileRowDownloadMutation>(downloadMutation); = useMutationWithToasts<TrustCenterFileRowDownloadMutation>(downloadMutation);
const handleRequestAccess = () => { const handleRequestAccess = () => {
requestAccess({ requestAccess({
@@ -98,8 +98,8 @@ export function TrustCenterFileRow(props: {
}); });
}; };
const handleDownload = () => { const handleDownload = async () => {
commitDownload({ await commitDownload({
variables: { variables: {
input: { input: {
trustCenterFileId: file.id, trustCenterFileId: file.id,
@@ -117,17 +117,20 @@ export function TrustCenterFileRow(props: {
<IconPageTextLine size={16} className=" flex-none text-txt-tertiary" /> <IconPageTextLine size={16} className=" flex-none text-txt-tertiary" />
{file.name} {file.name}
</div> </div>
{file.isUserAuthorized ? ( {file.isUserAuthorized
? (
<Button <Button
className="w-full md:w-max" className="w-full md:w-max"
variant="secondary" variant="secondary"
disabled={downloading} disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox} icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload} onClick={() => void handleDownload()}
> >
{__("Download")} {__("Download")}
</Button> </Button>
) : viewer ? ( )
: viewer
? (
<Button <Button
disabled={hasRequested || isRequestingAccess} disabled={hasRequested || isRequestingAccess}
className="w-full md:w-max" className="w-full md:w-max"
@@ -137,7 +140,8 @@ export function TrustCenterFileRow(props: {
> >
{hasRequested ? __("Access requested") : __("Request access")} {hasRequested ? __("Access requested") : __("Request access")}
</Button> </Button>
) : ( )
: (
<Button <Button
className="w-full md:w-max" className="w-full md:w-max"
variant="secondary" variant="secondary"

View File

@@ -25,35 +25,39 @@ export function VendorRow(props: { vendor: VendorRowFragment$key; hasAnyCountrie
return ( return (
<div className="flex text-sm leading-tight gap-3 md:items-center"> <div className="flex text-sm leading-tight gap-3 md:items-center">
{logo ? ( {logo
? (
<img <img
src={logo} src={logo}
className="size-8 md:size-6 flex-none rounded-lg" className="size-8 md:size-6 flex-none rounded-lg"
alt="" alt=""
/> />
) : ( )
: (
<div className="size-8 md:size-6 flex-none rounded-lg" /> <div className="size-8 md:size-6 flex-none rounded-lg" />
)} )}
<div className={`flex flex-col md:grid ${gridCols} flex-1 gap-0.5`}> <div className={`flex flex-col md:grid ${gridCols} flex-1 gap-0.5`}>
<div>{vendor.name}</div> <div>{vendor.name}</div>
{vendor.privacyPolicyUrl ? ( {vendor.privacyPolicyUrl
? (
<a <a
href={vendor.privacyPolicyUrl} href={vendor.privacyPolicyUrl}
target="_blank" target="_blank"
className={`flex gap-1 text-txt-info items-center hover:underline ${!props.hasAnyCountries ? 'md:justify-end' : ''}`} className={`flex gap-1 text-txt-info items-center hover:underline ${!props.hasAnyCountries ? "md:justify-end" : ""}`}
> >
<IconShield size={16} className="flex-none" /> <IconShield size={16} className="flex-none" />
<span>{__("Privacy")}</span> <span>{__("Privacy")}</span>
</a> </a>
) : ( )
: (
<div></div> <div></div>
)} )}
{vendor.countries.length > 0 && ( {vendor.countries.length > 0 && (
<div className={`flex gap-1 text-txt-secondary items-center ${props.hasAnyCountries ? 'md:justify-end' : ''}`}> <div className={`flex gap-1 text-txt-secondary items-center ${props.hasAnyCountries ? "md:justify-end" : ""}`}>
<IconPin size={16} className="flex-none" /> <IconPin size={16} className="flex-none" />
<span> <span>
{vendor.countries {vendor.countries
.map((country) => getCountryName(__, country)) .map(country => getCountryName(__, country))
.join(", ")} .join(", ")}
</span> </span>
</div> </div>

View File

@@ -12,7 +12,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
baseOptions?: { baseOptions?: {
onSuccess?: (response: T["response"]) => void; onSuccess?: (response: T["response"]) => void;
errorMessage?: string; errorMessage?: string;
} },
) { ) {
const [mutate, isLoading] = useMutation<T>(query); const [mutate, isLoading] = useMutation<T>(query);
const { toast } = useToast(); const { toast } = useToast();
@@ -22,7 +22,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
queryOptions: UseMutationConfig<T> & { queryOptions: UseMutationConfig<T> & {
onSuccess?: (response: T["response"]) => void; onSuccess?: (response: T["response"]) => void;
errorMessage?: string; errorMessage?: string;
} },
) => { ) => {
const options = { ...baseOptions, ...queryOptions }; const options = { ...baseOptions, ...queryOptions };
return new Promise<void>((resolve, reject) => return new Promise<void>((resolve, reject) =>
@@ -34,11 +34,11 @@ export function useMutationWithToasts<T extends MutationParameters>(
toast({ toast({
title: __("Error"), title: __("Error"),
description: description:
options.errorMessage ?? options.errorMessage
__("Failed to commit this operation."), ?? __("Failed to commit this operation."),
variant: "error", variant: "error",
}); });
reject(error); reject(error instanceof Error ? error : new Error(__("Failed to commit this operation.")));
return; return;
} }
options.onSuccess?.(response); options.onSuccess?.(response);
@@ -53,10 +53,10 @@ export function useMutationWithToasts<T extends MutationParameters>(
}); });
reject(error); reject(error);
}, },
}) }),
); );
}, },
[mutate, toast, __, baseOptions] [mutate, toast, __, baseOptions],
); );
return [mutateWithToast, isLoading] as const; return [mutateWithToast, isLoading] as const;

View File

@@ -21,10 +21,10 @@ export function MainLayout(props: Props) {
if (!trustCenter) { if (!trustCenter) {
return null; return null;
} }
const showNDADialog = const showNDADialog
trustCenter.isViewerMember && = trustCenter.isViewerMember
!trustCenter.hasAcceptedNonDisclosureAgreement && && !trustCenter.hasAcceptedNonDisclosureAgreement
trustCenter.ndaFileUrl; && trustCenter.ndaFileUrl;
return ( return (
<Viewer value={data.viewer}> <Viewer value={data.viewer}>
<TrustCenterProvider trustCenter={trustCenter}> <TrustCenterProvider trustCenter={trustCenter}>
@@ -51,7 +51,9 @@ export function MainLayout(props: Props) {
href="https://www.getprobo.com/" href="https://www.getprobo.com/"
className="flex gap-2 text-sm font-medium text-txt-tertiary items-center w-max mx-auto my-10" className="flex gap-2 text-sm font-medium text-txt-tertiary items-center w-max mx-auto my-10"
> >
{__("Powered by")} <Logo withPicto className="h-6" /> {__("Powered by")}
{" "}
<Logo withPicto className="h-6" />
</a> </a>
</TrustCenterProvider> </TrustCenterProvider>
</Viewer> </Viewer>

View File

@@ -23,5 +23,5 @@ createRoot(document.getElementById("root")!).render(
</TranslatorProvider> </TranslatorProvider>
</RelayProvider> </RelayProvider>
</QueryClientProvider> </QueryClientProvider>
</StrictMode> </StrictMode>,
); );

View File

@@ -18,16 +18,16 @@ export function DocumentsPage({ queryRef }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const data = usePreloadedQuery<TrustGraphCurrentDocumentsQuery>( const data = usePreloadedQuery<TrustGraphCurrentDocumentsQuery>(
currentTrustDocumentsQuery, currentTrustDocumentsQuery,
queryRef queryRef,
); );
const documents = const documents
data.currentTrustCenter?.documents.edges.map((edge) => edge.node) ?? []; = data.currentTrustCenter?.documents.edges.map(edge => edge.node) ?? [];
const files = const files
data.currentTrustCenter?.trustCenterFiles.edges.map((edge) => edge.node) ?? []; = data.currentTrustCenter?.trustCenterFiles.edges.map(edge => edge.node) ?? [];
const documentsPerType = groupBy(documents, (document) => const documentsPerType = groupBy(documents, document =>
documentTypeLabel(document.documentType, __) documentTypeLabel(document.documentType, __),
); );
const filesPerCategory = groupBy(files, (file) => file.category); const filesPerCategory = groupBy(files, file => file.category);
return ( return (
<div> <div>
<h2 className="font-medium mb-1">{__("Documents")}</h2> <h2 className="font-medium mb-1">{__("Documents")}</h2>
@@ -38,7 +38,7 @@ export function DocumentsPage({ queryRef }: Props) {
{objectEntries(documentsPerType).map(([label, documents]) => ( {objectEntries(documentsPerType).map(([label, documents]) => (
<Fragment key={label}> <Fragment key={label}>
<RowHeader>{label}</RowHeader> <RowHeader>{label}</RowHeader>
{documents.map((document) => ( {documents.map(document => (
<DocumentRow key={document.id} document={document} /> <DocumentRow key={document.id} document={document} />
))} ))}
</Fragment> </Fragment>
@@ -46,7 +46,7 @@ export function DocumentsPage({ queryRef }: Props) {
{objectEntries(filesPerCategory).map(([category, files]) => ( {objectEntries(filesPerCategory).map(([category, files]) => (
<Fragment key={category}> <Fragment key={category}>
<RowHeader>{category}</RowHeader> <RowHeader>{category}</RowHeader>
{files.map((file) => ( {files.map(file => (
<TrustCenterFileRow key={file.id} file={file} /> <TrustCenterFileRow key={file.id} file={file} />
))} ))}
</Fragment> </Fragment>

View File

@@ -67,14 +67,14 @@ const overviewFragment = graphql`
export function OverviewPage() { export function OverviewPage() {
const { trustCenter } = useOutletContext<{ const { trustCenter } = useOutletContext<{
trustCenter: OverviewPageFragment$key & trustCenter: OverviewPageFragment$key
TrustGraphCurrentQuery$data["currentTrustCenter"]; & TrustGraphCurrentQuery$data["currentTrustCenter"];
}>(); }>();
const fragment = useFragment(overviewFragment, trustCenter); const fragment = useFragment(overviewFragment, trustCenter);
return ( return (
<div> <div>
<References <References
references={fragment.references.edges.map((edge) => edge.node)} references={fragment.references.edges.map(edge => edge.node)}
/> />
<Documents <Documents
audits={trustCenter.audits.edges} audits={trustCenter.audits.edges}
@@ -106,12 +106,12 @@ function Documents({
}) { }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const documentsPerType = groupBy( const documentsPerType = groupBy(
documents.map((edge) => edge.node), documents.map(edge => edge.node),
(node) => documentTypeLabel(node.documentType, __), node => documentTypeLabel(node.documentType, __),
); );
const filesPerCategory = groupBy( const filesPerCategory = groupBy(
files.map((edge) => edge.node), files.map(edge => edge.node),
(node) => node.category, node => node.category,
); );
const hasAudits = audits.length > 0; const hasAudits = audits.length > 0;
const hasDocuments = hasAudits || documents.length > 0 || files.length > 0; const hasDocuments = hasAudits || documents.length > 0 || files.length > 0;
@@ -130,7 +130,7 @@ function Documents({
{audits.length > 0 && ( {audits.length > 0 && (
<> <>
<RowHeader>{__("Compliance")}</RowHeader> <RowHeader>{__("Compliance")}</RowHeader>
{audits.map((audit) => ( {audits.map(audit => (
<AuditRow key={audit.node.id} audit={audit.node} /> <AuditRow key={audit.node.id} audit={audit.node} />
))} ))}
</> </>
@@ -138,7 +138,7 @@ function Documents({
{objectEntries(documentsPerType).map(([label, documents]) => ( {objectEntries(documentsPerType).map(([label, documents]) => (
<Fragment key={label}> <Fragment key={label}>
<RowHeader>{label}</RowHeader> <RowHeader>{label}</RowHeader>
{documents.map((document) => ( {documents.map(document => (
<DocumentRow key={document.id} document={document} /> <DocumentRow key={document.id} document={document} />
))} ))}
</Fragment> </Fragment>
@@ -146,7 +146,7 @@ function Documents({
{objectEntries(filesPerCategory).map(([category, files]) => ( {objectEntries(filesPerCategory).map(([category, files]) => (
<Fragment key={category}> <Fragment key={category}>
<RowHeader>{category}</RowHeader> <RowHeader>{category}</RowHeader>
{files.map((file) => ( {files.map(file => (
<TrustCenterFileRow key={file.id} file={file} /> <TrustCenterFileRow key={file.id} file={file} />
))} ))}
</Fragment> </Fragment>
@@ -189,7 +189,7 @@ function Subprocessors({
)} )}
</p> </p>
<Rows className="mb-8 *:py-5"> <Rows className="mb-8 *:py-5">
{vendors.map((vendor) => ( {vendors.map(vendor => (
<VendorRow <VendorRow
key={vendor.node.id} key={vendor.node.id}
vendor={vendor.node} vendor={vendor.node}
@@ -223,7 +223,7 @@ function References({ references }: { references: Reference[] }) {
<div className="mb-8"> <div className="mb-8">
<h2 className="font-medium mb-4">{__("Trusted by")}</h2> <h2 className="font-medium mb-4">{__("Trusted by")}</h2>
<Card className="grid grid-cols-2 flex-wrap p-6 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7"> <Card className="grid grid-cols-2 flex-wrap p-6 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7">
{references.map((reference) => ( {references.map(reference => (
<a <a
key={reference.id} key={reference.id}
href={reference.websiteUrl} href={reference.websiteUrl}

View File

@@ -13,8 +13,8 @@ type Props = {
export function SubprocessorsPage({ queryRef }: Props) { export function SubprocessorsPage({ queryRef }: Props) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const data = usePreloadedQuery(currentTrustVendorsQuery, queryRef); const data = usePreloadedQuery(currentTrustVendorsQuery, queryRef);
const vendors = const vendors
data.currentTrustCenter?.vendors.edges.map((edge) => edge.node) ?? []; = data.currentTrustCenter?.vendors.edges.map(edge => edge.node) ?? [];
const hasAnyCountries = vendors.some(vendor => vendor.countries.length > 0); const hasAnyCountries = vendors.some(vendor => vendor.countries.length > 0);
@@ -24,11 +24,11 @@ export function SubprocessorsPage({ queryRef }: Props) {
<p className="text-sm text-txt-secondary mb-4"> <p className="text-sm text-txt-secondary mb-4">
{sprintf( {sprintf(
__("Third-party subprocessors %s work with:"), __("Third-party subprocessors %s work with:"),
data.currentTrustCenter?.organization.name ?? "" data.currentTrustCenter?.organization.name ?? "",
)} )}
</p> </p>
<Rows> <Rows>
{vendors.map((vendor) => ( {vendors.map(vendor => (
<VendorRow key={vendor.id} vendor={vendor} hasAnyCountries={hasAnyCountries} /> <VendorRow key={vendor.id} vendor={vendor} hasAnyCountries={hasAnyCountries} />
))} ))}
</Rows> </Rows>

View File

@@ -59,12 +59,11 @@ export function ConnectPage(props: {
if (!magicLinkSent && interval.current) { if (!magicLinkSent && interval.current) {
clearInterval(interval.current); clearInterval(interval.current);
interval.current = undefined; interval.current = undefined;
setTimer(timerDurationSeconds);
} }
if (magicLinkSent) { if (magicLinkSent) {
clearInterval(interval.current); clearInterval(interval.current);
interval.current = setInterval(() => { interval.current = setInterval(() => {
setTimer((timer) => Math.max(timer - 1, 0)); setTimer(timer => Math.max(timer - 1, 0));
}, 1000); }, 1000);
} }
@@ -138,7 +137,7 @@ export function ConnectPage(props: {
</p> </p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field <Field
label={__("Email")} label={__("Email")}
placeholder="john.doe@acme.com" placeholder="john.doe@acme.com"

View File

@@ -5,8 +5,8 @@ import { RelayProvider } from "/providers/RelayProviders";
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql"; import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
function ConnectPageLoader() { function ConnectPageLoader() {
const [queryRef, loadQuery] = const [queryRef, loadQuery]
useQueryLoader<ConnectPageQuery>(connectPageQuery); = useQueryLoader<ConnectPageQuery>(connectPageQuery);
useEffect(() => { useEffect(() => {
if (!queryRef) { if (!queryRef) {

View File

@@ -42,7 +42,7 @@ export default function VerifyMagicLinkPagePageMutation() {
verifyMagicLinkMutation, verifyMagicLinkMutation,
); );
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit((data) => {
verifyMagicLink({ verifyMagicLink({
variables: { variables: {
input: { input: {
@@ -79,7 +79,7 @@ export default function VerifyMagicLinkPagePageMutation() {
useEffect(() => { useEffect(() => {
if (!submittedRef.current && searchParams.get("token")) { if (!submittedRef.current && searchParams.get("token")) {
handleSubmit(); void handleSubmit();
submittedRef.current = true; submittedRef.current = true;
} }
}); });
@@ -94,7 +94,7 @@ export default function VerifyMagicLinkPagePageMutation() {
</p> </p>
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field <Field
label={__("Confirmation Token")} label={__("Confirmation Token")}
type="text" type="text"

View File

@@ -1,14 +1,13 @@
import { import {
Environment, Environment,
type FetchFunction,
Network, Network,
RecordSource, RecordSource,
Store, Store,
} from "relay-runtime"; } from "relay-runtime";
import { GraphQLError } from "graphql";
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { RelayEnvironmentProvider } from "react-relay"; import { RelayEnvironmentProvider } from "react-relay";
import { getPathPrefix } from "/utils/pathPrefix"; import { getPathPrefix } from "/utils/pathPrefix";
import { makeFetchQuery } from "@probo/relay";
export class UnAuthenticatedError extends Error { export class UnAuthenticatedError extends Error {
constructor() { constructor() {
@@ -43,8 +42,8 @@ export function buildEndpoint(): string {
host = window.location.origin; host = window.location.origin;
} }
const formattedHost = const formattedHost
host.startsWith("http://") || host.startsWith("https://") = host.startsWith("http://") || host.startsWith("https://")
? host ? host
: `https://${host}`; : `https://${host}`;
@@ -63,99 +62,6 @@ export function buildEndpoint(): string {
return url.toString(); return url.toString();
} }
const hasUnauthenticatedError = (error: GraphQLError) =>
error.extensions?.code == "UNAUTHENTICATED";
const hasInvalidError = (error: GraphQLError) =>
error.extensions?.code == "INVALID_REQUEST";
const fetchRelay: FetchFunction = async (
request,
variables,
_,
uploadables,
) => {
const requestInit: RequestInit = {
method: "POST",
credentials: "include",
headers: {},
};
if (uploadables) {
const formData = new FormData();
formData.append(
"operations",
JSON.stringify({
operationName: request.name,
query: request.text,
variables: variables,
}),
);
const uploadableMap: {
[key: string]: string[];
} = {};
Object.keys(uploadables).forEach((key, index) => {
uploadableMap[index] = [`variables.${key}`];
});
formData.append("map", JSON.stringify(uploadableMap));
Object.keys(uploadables).forEach((key, index) => {
formData.append(index.toString(), uploadables[key]);
});
requestInit.body = formData;
} else {
// Extract slug from URL if present for slug-based routing
const slugMatch = window.location.pathname.match(/^\/trust\/([^/]+)/);
const slug = slugMatch ? slugMatch[1] : null;
requestInit.headers = {
Accept:
"application/graphql-response+json; charset=utf-8, application/json; charset=utf-8",
"Content-Type": "application/json",
...(slug ? { "X-Trust-Slug": slug } : {}),
};
requestInit.body = JSON.stringify({
operationName: request.name,
query: request.text,
variables,
});
}
const response = await fetch(buildEndpoint(), requestInit);
if (response.status === 500) {
throw new InternalServerError();
}
const json = await response.json();
if (json.errors) {
const errors = json.errors as GraphQLError[];
if (errors.find(hasUnauthenticatedError)) {
throw new UnAuthenticatedError();
}
const invalidError = errors.find(hasInvalidError);
if (invalidError) {
throw new InvalidError(
invalidError.message,
(invalidError.extensions.field as string) ?? "",
(invalidError.extensions.cause as string) ?? "",
);
}
throw new Error(`Error fetching GraphQL query '${request.name}'`);
}
return json;
};
const source = new RecordSource(); const source = new RecordSource();
const store = new Store(source, { const store = new Store(source, {
queryCacheExpirationTime: 1 * 60 * 1000, queryCacheExpirationTime: 1 * 60 * 1000,
@@ -164,7 +70,7 @@ const store = new Store(source, {
export const consoleEnvironment = new Environment({ export const consoleEnvironment = new Environment({
configName: "trust", configName: "trust",
network: Network.create(fetchRelay), network: Network.create(makeFetchQuery(buildEndpoint())),
store, store,
}); });

View File

@@ -27,7 +27,7 @@ import {
function ErrorBoundary({ error: propsError }: { error?: string }) { function ErrorBoundary({ error: propsError }: { error?: string }) {
const error = useRouteError() ?? propsError; const error = useRouteError() ?? propsError;
return <PageError error={error?.toString()} />; return <PageError error={error instanceof Error ? error.message : ""} />;
} }
const routes = [ const routes = [
@@ -41,7 +41,8 @@ const routes = [
}, },
{ {
path: "/", path: "/",
loader: async () => { loader: () => {
// eslint-disable-next-line
throw redirect("/overview"); throw redirect("/overview");
}, },
Component: Fragment, Component: Fragment,

View File

@@ -1,5 +1,5 @@
export type NodeOf<T> = export type NodeOf<T>
NonNullable<T> extends = NonNullable<T> extends
| { readonly edges: ReadonlyArray<{ readonly node: infer U }> } | { readonly edges: ReadonlyArray<{ readonly node: infer U }> }
| undefined | undefined
? U ? U

View File

@@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"allowJs": true,
"target": "ES2022", "target": "ES2022",
"lib": ["ES2023"], "lib": ["ES2023"],
"module": "ESNext", "module": "ESNext",
@@ -21,5 +22,5 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts", "eslint.config.mjs"]
} }