Fix apps/console lint issues

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-22 12:51:08 +04:00
parent b4a73f2eea
commit dd23449cc0
244 changed files with 7685 additions and 8029 deletions

View File

@@ -1,5 +1,5 @@
/** /**
* @generated SignedSource<<098714fcfe212fae40bdad19b10f8a00>> * @generated SignedSource<<d78dfc5b8db81e410f3f606fb91cd137>>
* @lightSyntaxTransform * @lightSyntaxTransform
* @nogrep * @nogrep
*/ */
@@ -20,6 +20,7 @@ export type NewDomainDialogMutation$variables = {
export type NewDomainDialogMutation$data = { export type NewDomainDialogMutation$data = {
readonly createCustomDomain: { readonly createCustomDomain: {
readonly customDomain: { readonly customDomain: {
readonly canDelete: boolean;
readonly createdAt: string; readonly createdAt: string;
readonly dnsRecords: ReadonlyArray<{ readonly dnsRecords: ReadonlyArray<{
readonly name: string; readonly name: string;
@@ -159,6 +160,19 @@ v1 = [
"kind": "ScalarField", "kind": "ScalarField",
"name": "sslExpiresAt", "name": "sslExpiresAt",
"storageKey": null "storageKey": null
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:custom-domain:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:custom-domain:delete\")"
} }
], ],
"storageKey": null "storageKey": null
@@ -185,16 +199,16 @@ return {
"selections": (v1/*: any*/) "selections": (v1/*: any*/)
}, },
"params": { "params": {
"cacheID": "b8aa27fe9ab3442dea9b96a70966524b", "cacheID": "39d421d36a6418d0ee687d43b9d7a7ee",
"id": null, "id": null,
"metadata": {}, "metadata": {},
"name": "NewDomainDialogMutation", "name": "NewDomainDialogMutation",
"operationKind": "mutation", "operationKind": "mutation",
"text": "mutation NewDomainDialogMutation(\n $input: CreateCustomDomainInput!\n) {\n createCustomDomain(input: $input) {\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n }\n }\n}\n" "text": "mutation NewDomainDialogMutation(\n $input: CreateCustomDomainInput!\n) {\n createCustomDomain(input: $input) {\n customDomain {\n id\n domain\n sslStatus\n dnsRecords {\n type\n name\n value\n ttl\n purpose\n }\n createdAt\n updatedAt\n sslExpiresAt\n canDelete: permission(action: \"core:custom-domain:delete\")\n }\n }\n}\n"
} }
}; };
})(); })();
(node as any).hash = "f49cd22c5d6656e662fad5afab6fc344"; (node as any).hash = "37ef764ab4f78a6898f06d00374347ee";
export default node; export default node;

View File

@@ -26,8 +26,8 @@ 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();
} }
@@ -54,7 +54,7 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
); );
} }
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}>
@@ -68,7 +68,7 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
); );
} }
if (error && error.toString().includes("FORBIDDEN")) { if (error instanceof Error && error.message.includes("FORBIDDEN")) {
return ( return (
<div className={classNames.wrapper}> <div className={classNames.wrapper}>
<h1 className={classNames.title}> <h1 className={classNames.title}>
@@ -82,7 +82,7 @@ export function PageError({ resetErrorBoundary, error: propsError }: Props) {
); );
} }
if (error && error.toString().includes("UNAUTHORIZED")) { if (error instanceof Error && error.message.includes("UNAUTHORIZED")) {
return ( return (
<div className={classNames.wrapper}> <div className={classNames.wrapper}>
<h1 className={classNames.title}> <h1 className={classNames.title}>
@@ -103,7 +103,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

@@ -46,8 +46,8 @@ export function SnapshotBanner({ snapshotId }: Props) {
} }
if ( if (
snapshot.type && snapshot.type
!isSnapshotTypeValidForUrl(snapshot.type, location.pathname) && !isSnapshotTypeValidForUrl(snapshot.type, location.pathname)
) { ) {
throw new Error("PAGE_NOT_FOUND"); throw new Error("PAGE_NOT_FOUND");
} }
@@ -58,7 +58,9 @@ export function SnapshotBanner({ snapshotId }: Props) {
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="font-medium text-warning-800"> <span className="font-medium text-warning-800">
{__("Snapshot")} {snapshot.name} {__("Snapshot")}
{" "}
{snapshot.name}
</span> </span>
</div> </div>
<p className="text-sm text-warning-700"> <p className="text-sm text-warning-700">

View File

@@ -141,7 +141,7 @@ export function AssetsTable(props: Props) {
<VendorsCell <VendorsCell
name="vendorIds" name="vendorIds"
organizationId={organizationId} organizationId={organizationId}
defaultValue={item?.vendors?.edges?.map((edge) => edge.node) ?? []} defaultValue={item?.vendors?.edges?.map(edge => edge.node) ?? []}
/> />
</> </>
)} )}
@@ -163,7 +163,7 @@ const useDeleteAsset = (connectionId: string) => {
promisifyMutation(mutate)({ promisifyMutation(mutate)({
variables: { variables: {
input: { input: {
assetId: asset.id!, assetId: asset.id,
}, },
connections: [connectionId], connections: [connectionId],
}, },
@@ -171,7 +171,7 @@ const useDeleteAsset = (connectionId: string) => {
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete "%s". This action cannot be undone.', "This will permanently delete \"%s\". This action cannot be undone.",
), ),
asset.name, asset.name,
), ),

View File

@@ -39,7 +39,7 @@ export function ReadOnlyAssetsTable(props: Props) {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{assets.map((entry) => ( {assets.map(entry => (
<AssetRow key={entry.id} entry={entry} /> <AssetRow key={entry.id} entry={entry} />
))} ))}
</Tbody> </Tbody>
@@ -51,7 +51,7 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const { snapshotId } = useParams<{ snapshotId?: string }>(); const { snapshotId } = useParams<{ snapshotId?: string }>();
const vendors = entry.vendors?.edges.map((edge) => edge.node) ?? []; const vendors = entry.vendors?.edges.map(edge => edge.node) ?? [];
return ( return (
<Tr <Tr
@@ -70,9 +70,10 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
<Td>{entry.amount}</Td> <Td>{entry.amount}</Td>
<Td>{entry.owner?.fullName ?? __("Unassigned")}</Td> <Td>{entry.owner?.fullName ?? __("Unassigned")}</Td>
<Td> <Td>
{vendors.length > 0 ? ( {vendors.length > 0
? (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{vendors.slice(0, 3).map((vendor) => ( {vendors.slice(0, 3).map(vendor => (
<Badge <Badge
key={vendor.id} key={vendor.id}
variant="neutral" variant="neutral"
@@ -88,11 +89,13 @@ function AssetRow({ entry }: { entry: AssetEntry }) {
))} ))}
{vendors.length > 3 && ( {vendors.length > 3 && (
<Badge variant="neutral" className="text-xs"> <Badge variant="neutral" className="text-xs">
+{vendors.length - 3} +
{vendors.length - 3}
</Badge> </Badge>
)} )}
</div> </div>
) : ( )
: (
<span className="text-txt-secondary text-sm">{__("None")}</span> <span className="text-txt-secondary text-sm">{__("None")}</span>
)} )}
</Td> </Td>

View File

@@ -131,7 +131,7 @@ export function LinkedAuditsCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{audits.map((audit) => ( {audits.map(audit => (
<AuditRow <AuditRow
key={audit.id} key={audit.id}
audit={audit} audit={audit}

View File

@@ -104,15 +104,15 @@ function LinkedAuditsDialogContent(props: Omit<Props, "children">) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const audits = useMemo( const audits = useMemo(
() => data.audits?.edges?.map((edge) => edge.node) ?? [], () => data.audits?.edges?.map(edge => edge.node) ?? [],
[data.audits], [data.audits],
); );
const linkedIds = useMemo(() => { const linkedIds = useMemo(() => {
return new Set(props.linkedAudits?.map((a) => a.id) ?? []); return new Set(props.linkedAudits?.map(a => a.id) ?? []);
}, [props.linkedAudits]); }, [props.linkedAudits]);
const filteredAudits = useMemo(() => { const filteredAudits = useMemo(() => {
return audits.filter((audit) => return audits.filter(audit =>
(audit.name || "").toLowerCase().includes(search.toLowerCase()), (audit.name || "").toLowerCase().includes(search.toLowerCase()),
); );
}, [audits, search]); }, [audits, search]);
@@ -127,7 +127,7 @@ function LinkedAuditsDialogContent(props: Omit<Props, "children">) {
/> />
</div> </div>
<div className="divide-y divide-border-low"> <div className="divide-y divide-border-low">
{filteredAudits.map((audit) => ( {filteredAudits.map(audit => (
<AuditRow <AuditRow
key={audit.id} key={audit.id}
audit={audit} audit={audit}
@@ -186,7 +186,9 @@ function AuditRow(props: RowProps) {
asChild asChild
> >
<span> <span>
<IconComponent size={16} /> {isLinked ? __("Unlink") : __("Link")} <IconComponent size={16} />
{" "}
{isLinked ? __("Unlink") : __("Link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -111,7 +111,7 @@ export function LinkedControlsCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{controls.map((control) => ( {controls.map(control => (
<ControlRow <ControlRow
key={control.id} key={control.id}
control={control} control={control}
@@ -152,7 +152,8 @@ function ControlRow(props: {
> >
<Td> <Td>
<span className="inline-flex gap-2 items-center"> <span className="inline-flex gap-2 items-center">
{control.framework.name}{" "} {control.framework.name}
{" "}
<Badge size="md">{control.sectionTitle}</Badge> <Badge size="md">{control.sectionTitle}</Badge>
</span> </span>
</Td> </Td>

View File

@@ -12,6 +12,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { import {
Suspense, Suspense,
useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@@ -104,11 +105,11 @@ export function LinkedControlsDialog(props: Props) {
</div> </div>
<div ref={contentRef}> <div ref={contentRef}>
<Suspense <Suspense
fallback={ fallback={(
<div style={{ minHeight }}> <div style={{ minHeight }}>
<Spinner centered /> <Spinner centered />
</div> </div>
} )}
> >
<LinkedControlsDialogContent {...props} ref={searchRef} /> <LinkedControlsDialogContent {...props} ref={searchRef} />
</Suspense> </Suspense>
@@ -123,32 +124,37 @@ function LinkedControlsDialogContent(props: Props & { ref: SearchRef }) {
const mainData = useLazyLoadQuery<LinkedControlsDialogQuery>(query, { const mainData = useLazyLoadQuery<LinkedControlsDialogQuery>(query, {
organizationId, organizationId,
}); });
const { data, loadNext, hasNext, isLoadingNext, refetch } = const { data, loadNext, hasNext, isLoadingNext, refetch } = usePaginationFragment(
usePaginationFragment(
controlsFragment, controlsFragment,
mainData.organization as LinkedControlsDialogFragment$key, mainData.organization as LinkedControlsDialogFragment$key,
); );
const controls = data.controls?.edges?.map((edge) => edge.node) ?? []; const controls = data.controls?.edges?.map(edge => edge.node) ?? [];
const controlIds = useMemo(() => { const controlIds = useMemo(() => {
return new Set(props.linkedControls?.map((c) => c.id) ?? []); return new Set(props.linkedControls?.map(c => c.id) ?? []);
}, [props.linkedControls]); }, [props.linkedControls]);
props.ref.current = { const handleSearch = useDebounceCallback((v: string) => {
search: useDebounceCallback((v: string) => {
refetch({ refetch({
first: 20, first: 20,
filter: { filter: {
query: v, query: v,
}, },
}); });
}, 500), }, 500);
useEffect(() => {
if (!props.ref.current) {
props.ref.current = {
search: handleSearch,
}; };
}
});
return ( return (
<> <>
<div className="divide-y divide-border-low"> <div className="divide-y divide-border-low">
{controls.map((control) => ( {controls.map(control => (
<ControlRow <ControlRow
key={control.id} key={control.id}
control={control} control={control}
@@ -182,7 +188,10 @@ function ControlRow(
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full text-start" className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full text-start"
onClick={() => onClick(props.control.id)} onClick={() => onClick(props.control.id)}
> >
{props.control.sectionTitle} : {props.control.name} {props.control.sectionTitle}
{" "}
:
{props.control.name}
<Badge>{props.control.framework.name}</Badge> <Badge>{props.control.framework.name}</Badge>
<Button <Button
disabled={props.disabled} disabled={props.disabled}
@@ -191,7 +200,9 @@ function ControlRow(
asChild asChild
> >
<span> <span>
<IconComponent size={16} /> {isLinked ? __("Unlink") : __("Link")} <IconComponent size={16} />
{" "}
{isLinked ? __("Unlink") : __("Link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -35,7 +35,7 @@ type BulkExportFormData = z.infer<typeof bulkExportSchema>;
type Props = { type Props = {
children: ReactNode; children: ReactNode;
onExport: (options: BulkExportFormData) => void; onExport: (options: BulkExportFormData) => Promise<void>;
isLoading?: boolean; isLoading?: boolean;
defaultEmail: string; defaultEmail: string;
selectedCount: number; selectedCount: number;
@@ -59,7 +59,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
watermarkEmail: defaultEmail, watermarkEmail: defaultEmail,
withSignatures: true, withSignatures: true,
}, },
} },
); );
const watchWatermark = watch("withWatermark"); const watchWatermark = watch("withWatermark");
@@ -70,14 +70,14 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
close: () => dialogRef.current?.close(), close: () => dialogRef.current?.close(),
})); }));
const onSubmit = handleSubmit((data) => { const onSubmit = async (data: BulkExportFormData) => {
const options = { const options = {
...data, ...data,
watermarkEmail: data.withWatermark ? data.watermarkEmail : undefined, watermarkEmail: data.withWatermark ? data.watermarkEmail : undefined,
}; };
onExport(options); await onExport(options);
dialogRef.current?.close(); dialogRef.current?.close();
}); };
return ( return (
<> <>
@@ -87,13 +87,13 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
ref={dialogRef} ref={dialogRef}
title={sprintf(__("Export %s Documents"), selectedCount)} title={sprintf(__("Export %s Documents"), selectedCount)}
> >
<form onSubmit={onSubmit}> <form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent className="space-y-4" padded> <DialogContent className="space-y-4" padded>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Checkbox <Checkbox
checked={watchSignatures} checked={watchSignatures}
onChange={(checked) => setValue("withSignatures", checked)} onChange={checked => setValue("withSignatures", checked)}
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
@@ -108,7 +108,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Checkbox <Checkbox
checked={watchWatermark} checked={watchWatermark}
onChange={(checked) => setValue("withWatermark", checked)} onChange={checked => setValue("withWatermark", checked)}
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
@@ -146,12 +146,14 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
> >
{isLoading ? ( {isLoading
? (
<> <>
<Spinner size={16} /> <Spinner size={16} />
{__("Exporting...")} {__("Exporting...")}
</> </>
) : ( )
: (
__("Export Documents") __("Export Documents")
)} )}
</Button> </Button>
@@ -160,5 +162,7 @@ export const BulkExportDialog = forwardRef<BulkExportDialogRef, Props>(
</Dialog> </Dialog>
</> </>
); );
} },
); );
BulkExportDialog.displayName = "BulkExportDialog";

View File

@@ -145,7 +145,7 @@ export function LinkedDocumentsCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{documents.map((document) => ( {documents.map(document => (
<DocumentRow <DocumentRow
key={document.id} key={document.id}
document={document} document={document}

View File

@@ -102,15 +102,15 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const documents = useMemo( const documents = useMemo(
() => data.documents?.edges?.map((edge) => edge.node) ?? [], () => data.documents?.edges?.map(edge => edge.node) ?? [],
[data.documents], [data.documents],
); );
const linkedIds = useMemo(() => { const linkedIds = useMemo(() => {
return new Set(props.linkedDocuments?.map((m) => m.id) ?? []); return new Set(props.linkedDocuments?.map(m => m.id) ?? []);
}, [props.linkedDocuments]); }, [props.linkedDocuments]);
const filteredDocuments = useMemo(() => { const filteredDocuments = useMemo(() => {
return documents.filter((document) => return documents.filter(document =>
document.title.toLowerCase().includes(search.toLowerCase()), document.title.toLowerCase().includes(search.toLowerCase()),
); );
}, [documents, search]); }, [documents, search]);
@@ -125,7 +125,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
/> />
</div> </div>
<div className="divide-y divide-border-low"> <div className="divide-y divide-border-low">
{filteredDocuments.map((document) => ( {filteredDocuments.map(document => (
<DocumentRow <DocumentRow
key={document.id} key={document.id}
document={document} document={document}
@@ -177,7 +177,9 @@ function DocumentRow(props: RowProps) {
asChild asChild
> >
<span> <span>
<IconComponent size={16} /> {isLinked ? __("Unlink") : __("Link")} <IconComponent size={16} />
{" "}
{isLinked ? __("Unlink") : __("Link")}
</span> </span>
</Button> </Button>
</button> </button>

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} />
@@ -123,7 +126,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

@@ -42,8 +42,8 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
const { __ } = useTranslate(); const { __ } = useTranslate();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const { register, handleSubmit, formState, watch, setValue } = const { register, handleSubmit, formState, watch, setValue }
useFormWithSchema(pdfDownloadSchema, { = useFormWithSchema(pdfDownloadSchema, {
defaultValues: { defaultValues: {
withWatermark: false, withWatermark: false,
watermarkEmail: defaultEmail, watermarkEmail: defaultEmail,
@@ -59,14 +59,14 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
close: () => dialogRef.current?.close(), close: () => dialogRef.current?.close(),
})); }));
const onSubmit = handleSubmit((data) => { const onSubmit = (data: PdfDownloadFormData) => {
const options = { const options = {
...data, ...data,
watermarkEmail: data.withWatermark ? data.watermarkEmail : undefined, watermarkEmail: data.withWatermark ? data.watermarkEmail : undefined,
}; };
onDownload(options); onDownload(options);
dialogRef.current?.close(); dialogRef.current?.close();
}); };
return ( return (
<> <>
@@ -76,13 +76,13 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
ref={dialogRef} ref={dialogRef}
title={__("Download PDF Options")} title={__("Download PDF Options")}
> >
<form onSubmit={onSubmit}> <form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent className="space-y-4" padded> <DialogContent className="space-y-4" padded>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Checkbox <Checkbox
checked={watchSignatures} checked={watchSignatures}
onChange={(checked) => setValue("withSignatures", checked)} onChange={checked => setValue("withSignatures", checked)}
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
@@ -99,7 +99,7 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Checkbox <Checkbox
checked={watchWatermark} checked={watchWatermark}
onChange={(checked) => setValue("withWatermark", checked)} onChange={checked => setValue("withWatermark", checked)}
/> />
<div className="flex-1"> <div className="flex-1">
<label className="text-sm font-medium text-txt-primary cursor-pointer"> <label className="text-sm font-medium text-txt-primary cursor-pointer">
@@ -130,12 +130,14 @@ export const PdfDownloadDialog = forwardRef<PdfDownloadDialogRef, Props>(
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button type="submit" disabled={isLoading}> <Button type="submit" disabled={isLoading}>
{isLoading ? ( {isLoading
? (
<> <>
<Spinner size={16} /> <Spinner size={16} />
{__("Downloading...")} {__("Downloading...")}
</> </>
) : ( )
: (
__("Download PDF") __("Download PDF")
)} )}
</Button> </Button>

View File

@@ -74,10 +74,10 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
{ organizationId }, { organizationId },
{ fetchPolicy: "network-only" }, { fetchPolicy: "network-only" },
); );
const audits = const audits
data?.organization?.audits?.edges = data?.organization?.audits?.edges
?.map((edge) => edge.node) ?.map(edge => edge.node)
.filter((node) => node !== null) ?? []; .filter(node => node !== null) ?? [];
const NONE_VALUE = "__NONE__"; const NONE_VALUE = "__NONE__";
@@ -91,9 +91,8 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
id={name} id={name}
variant="editor" variant="editor"
placeholder={__("Select an audit")} placeholder={__("Select an audit")}
onValueChange={(value) => onValueChange={value =>
field.onChange(value === NONE_VALUE ? "" : value) field.onChange(value === NONE_VALUE ? "" : value)}
}
key={audits?.length.toString() ?? "0"} key={audits?.length.toString() ?? "0"}
{...field} {...field}
className="w-full" className="w-full"
@@ -102,7 +101,7 @@ function AuditSelectWithQuery<T extends FieldValues = FieldValues>(
<Option value={NONE_VALUE}> <Option value={NONE_VALUE}>
<span className="text-txt-tertiary">{__("None")}</span> <span className="text-txt-tertiary">{__("None")}</span>
</Option> </Option>
{audits?.map((audit) => ( {audits?.map(audit => (
<Option key={audit.id} value={audit.id}> <Option key={audit.id} value={audit.id}>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<span> <span>

View File

@@ -1,18 +1,25 @@
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { Field } from "@probo/ui"; import { Field } from "@probo/ui";
import { Controller, type FieldValues } from "react-hook-form"; import { Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { Select } from "@probo/ui"; import { Select } from "@probo/ui";
type Props<T extends typeof Field | typeof Select, TFieldValues extends FieldValues = FieldValues> = type Props<
ComponentProps<T> & Omit<ComponentProps<typeof Controller<TFieldValues>>, "render">; T extends typeof Field | typeof Select,
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>
= ComponentProps<T> & Omit<ComponentProps<typeof Controller<TFieldValues, TName>>, "render">;
export function ControlledField<TFieldValues extends FieldValues = FieldValues>({ export function ControlledField<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
control, control,
name, name,
...props ...props
}: Props<typeof Field, TFieldValues>) { }: Props<typeof Field, TFieldValues, TName>) {
return ( return (
<Controller <Controller<TFieldValues, TName>
control={control} control={control}
name={name} name={name}
render={({ field }) => ( render={({ field }) => (
@@ -21,7 +28,7 @@ export function ControlledField<TFieldValues extends FieldValues = FieldValues>(
{...props} {...props}
{...field} {...field}
// TODO : Find a better way to handle this case (comparing number and string for select create issues) // TODO : Find a better way to handle this case (comparing number and string for select create issues)
value={field.value ? field.value.toString() : ""} value={field.value ? (field.value as readonly string[] | string | number).toString() : ""}
onValueChange={field.onChange} onValueChange={field.onChange}
/> />
</> </>

View File

@@ -1,5 +1,5 @@
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import { Badge, Input, IconCrossLargeX } from "@probo/ui"; import { Badge, Input, IconCrossLargeX } from "@probo/ui";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form"; import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { getCountryName, getCountryOptions, countries, type CountryCode } from "@probo/helpers"; import { getCountryName, getCountryOptions, countries, type CountryCode } from "@probo/helpers";
@@ -35,16 +35,16 @@ type CountriesFieldInputProps = {
function CountriesFieldInput(props: CountriesFieldInputProps) { function CountriesFieldInput(props: CountriesFieldInputProps) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const animateBadge = useRef(false); const [animateBadge, setAnimateBadge] = useState(false);
const addCountry = (code: string) => { const addCountry = (code: string) => {
animateBadge.current = true; setAnimateBadge(true);
props.onValueChange([...props.value, code]); props.onValueChange([...props.value, code]);
}; };
const removeCountry = (code: string) => { const removeCountry = (code: string) => {
animateBadge.current = true; setAnimateBadge(true);
props.onValueChange(props.value.filter((v) => v !== code)); props.onValueChange(props.value.filter(v => v !== code));
}; };
return ( return (
@@ -52,7 +52,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
{props.value.length > 0 && ( {props.value.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{props.value.map((countryCode) => ( {props.value.map(countryCode => (
<Badge asChild size="md" key={countryCode}> <Badge asChild size="md" key={countryCode}>
<button <button
onClick={() => removeCountry(countryCode)} onClick={() => removeCountry(countryCode)}
@@ -61,8 +61,8 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
className={clsx( className={clsx(
"hover:bg-subtle-hover cursor-pointer", "hover:bg-subtle-hover cursor-pointer",
props.disabled && "opacity-50 cursor-not-allowed", props.disabled && "opacity-50 cursor-not-allowed",
animateBadge.current && animateBadge
"starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent" && "starting:opacity-0 starting:w-0 w-max transition-all duration-500 starting:bg-accent",
)} )}
> >
{getCountryName(__, countryCode as CountryCode)} {getCountryName(__, countryCode as CountryCode)}
@@ -78,7 +78,7 @@ function CountriesFieldInput(props: CountriesFieldInputProps) {
{!props.disabled && ( {!props.disabled && (
<CountryInput <CountryInput
availableCountries={countries.filter( availableCountries={countries.filter(
(c: CountryCode) => !props.value.includes(c) (c: CountryCode) => !props.value.includes(c),
)} )}
onAdd={addCountry} onAdd={addCountry}
/> />
@@ -108,7 +108,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
const filteredCountries = countryOptions const filteredCountries = countryOptions
.filter((option: { value: string; label: string }) => availableCountries.includes(option.value as CountryCode)) .filter((option: { value: string; label: string }) => availableCountries.includes(option.value as CountryCode))
.filter((option: { value: string; label: string }) => .filter((option: { value: string; label: string }) =>
option.label.toLowerCase().includes(search.toLowerCase()) option.label.toLowerCase().includes(search.toLowerCase()),
); );
const handleCountryClick = (value: string) => { const handleCountryClick = (value: string) => {
@@ -123,7 +123,7 @@ function CountryInput({ availableCountries, onAdd }: CountryInputProps) {
<Input <Input
type="text" type="text"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
onFocus={() => setIsOpen(true)} onFocus={() => setIsOpen(true)}
placeholder={__("Search and add countries...")} placeholder={__("Search and add countries...")}
className="w-full pr-8" className="w-full pr-8"

View File

@@ -10,7 +10,7 @@ export function DocumentClassificationOptions() {
return ( return (
<> <>
{documentClassifications.map((classification) => ( {documentClassifications.map(classification => (
<Option key={classification} value={classification}> <Option key={classification} value={classification}>
{getDocumentClassificationLabel(__, classification)} {getDocumentClassificationLabel(__, classification)}
</Option> </Option>

View File

@@ -7,7 +7,7 @@ export function DocumentTypeOptions() {
return ( return (
<> <>
{documentTypes.map((type) => ( {documentTypes.map(type => (
<Option key={type} value={type}> <Option key={type} value={type}>
{getDocumentTypeLabel(__, type)} {getDocumentTypeLabel(__, type)}
</Option> </Option>

View File

@@ -12,7 +12,9 @@ type Props<TFieldValues extends FieldValues = FieldValues> = {
/** /**
* A field to handle multiple emails * A field to handle multiple emails
*/ */
export function EmailsField<TFieldValues extends FieldValues = FieldValues>({ control, register }: Props<TFieldValues>) { export function EmailsField<
TFieldValues extends FieldValues = FieldValues,
>({ control, register }: Props<TFieldValues>) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
name: "additionalEmailAddresses" as ArrayPath<TFieldValues>, name: "additionalEmailAddresses" as ArrayPath<TFieldValues>,

View File

@@ -4,7 +4,10 @@ import { useTranslate } from "@probo/i18n";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form"; import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { usePaginatedMeasures } from "/hooks/graph/usePaginatedMeasures"; import { usePaginatedMeasures } from "/hooks/graph/usePaginatedMeasures";
type Props<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = { type Props<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
organizationId: string; organizationId: string;
control: Control<TFieldValues>; control: Control<TFieldValues>;
name: TName; name: TName;
@@ -39,7 +42,7 @@ export function MeasureSelectField<TFieldValues extends FieldValues = FieldValue
} }
function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>( function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
props: Pick<Props<TFieldValues>, "organizationId" | "control" | "name" | "disabled" | "optional"> props: Pick<Props<TFieldValues>, "organizationId" | "control" | "name" | "disabled" | "optional">,
) { ) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { name, organizationId, control, disabled, optional } = props; const { name, organizationId, control, disabled, optional } = props;
@@ -49,16 +52,16 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
return ( return (
data?.measures.edges data?.measures.edges
?.filter( ?.filter(
(edge) => edge =>
edge.node.name.toLowerCase().includes(search.toLowerCase()) || edge.node.name.toLowerCase().includes(search.toLowerCase())
edge.node.description?.toLowerCase().includes(search.toLowerCase()) || edge.node.description?.toLowerCase().includes(search.toLowerCase()),
) )
.map((edge) => edge.node) ?? [] .map(edge => edge.node) ?? []
); );
}, [data?.measures.edges, search]); }, [data?.measures.edges, search]);
const allMeasures = useMemo(() => { const allMeasures = useMemo(() => {
return data?.measures.edges?.map((edge) => edge.node) ?? []; return data?.measures.edges?.map(edge => edge.node) ?? [];
}, [data?.measures.edges]); }, [data?.measures.edges]);
return ( return (
@@ -67,7 +70,7 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
control={control} control={control}
name={name} name={name}
render={({ field }) => { render={({ field }) => {
const selectedMeasure = field.value ? allMeasures?.find((m) => m.id === field.value) : null; const selectedMeasure = field.value ? allMeasures?.find(m => m.id === field.value) : null;
return ( return (
<Combobox <Combobox
@@ -87,7 +90,7 @@ function MeasureSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
{__("None")} {__("None")}
</ComboboxItem> </ComboboxItem>
)} )}
{measures?.map((m) => ( {measures?.map(m => (
<ComboboxItem <ComboboxItem
key={m.id} key={m.id}
onClick={() => { onClick={() => {

View File

@@ -44,7 +44,7 @@ export function PeopleMultiSelectField<T extends FieldValues = FieldValues>({
} }
function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>( function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedPeople"> props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedPeople">,
) { ) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { name, organizationId, control, selectedPeople = [] } = props; const { name, organizationId, control, selectedPeople = [] } = props;
@@ -52,7 +52,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const allPeople = [...people]; const allPeople = [...people];
selectedPeople.forEach(selectedPerson => { selectedPeople.forEach((selectedPerson) => {
if (!allPeople.find(p => p.id === selectedPerson.id)) { if (!allPeople.find(p => p.id === selectedPerson.id)) {
allPeople.push({ allPeople.push({
id: selectedPerson.id, id: selectedPerson.id,
@@ -99,7 +99,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
open={isOpen} open={isOpen}
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
> >
{availablePeople.map((person) => ( {availablePeople.map(person => (
<Option key={person.id} value={person.id} className="flex gap-2"> <Option key={person.id} value={person.id} className="flex gap-2">
<Avatar <Avatar
name={person.fullName} name={person.fullName}
@@ -120,7 +120,7 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
{selectedPeople.length > 0 && ( {selectedPeople.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedPeople.map((person) => ( {selectedPeople.map(person => (
<Badge key={person.id} variant="neutral" className="flex items-center gap-2"> <Badge key={person.id} variant="neutral" className="flex items-center gap-2">
<Avatar <Avatar
name={person.fullName} name={person.fullName}
@@ -153,4 +153,3 @@ function PeopleMultiSelectWithQuery<T extends FieldValues = FieldValues>(
</> </>
); );
} }

View File

@@ -4,7 +4,10 @@ import { useTranslate } from "@probo/i18n";
import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form"; import { type Control, Controller, type FieldPath, type FieldValues } from "react-hook-form";
import { usePeople } from "/hooks/graph/PeopleGraph.ts"; import { usePeople } from "/hooks/graph/PeopleGraph.ts";
type Props<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>> = { type Props<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
organizationId: string; organizationId: string;
control: Control<TFieldValues>; control: Control<TFieldValues>;
name: TName; name: TName;
@@ -55,16 +58,15 @@ function PeopleSelectWithQuery<TFieldValues extends FieldValues = FieldValues>(
id={name} id={name}
variant="editor" variant="editor"
placeholder={__("Select an owner")} placeholder={__("Select an owner")}
onValueChange={(value) => onValueChange={value =>
field.onChange(value === "__NONE__" ? null : value) field.onChange(value === "__NONE__" ? null : value)}
}
key={people?.length.toString() ?? "0"} key={people?.length.toString() ?? "0"}
{...field} {...field}
className="w-full" className="w-full"
value={field.value ?? (props.optional ? "__NONE__" : "")} value={field.value ?? (props.optional ? "__NONE__" : "")}
> >
{props.optional && <Option value="__NONE__">{__("None")}</Option>} {props.optional && <Option value="__NONE__">{__("None")}</Option>}
{people?.map((p) => ( {people?.map(p => (
<Option key={p.id} value={p.id} className="flex gap-2"> <Option key={p.id} value={p.id} className="flex gap-2">
<Avatar name={p.fullName} /> <Avatar name={p.fullName} />
{p.fullName} {p.fullName}

View File

@@ -21,7 +21,7 @@ export function SpecialOrCriminalDataOptions() {
return ( return (
<> <>
{options.map((option) => ( {options.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{option.label} {option.label}
</Option> </Option>
@@ -47,7 +47,7 @@ export function LawfulBasisOptions() {
return ( return (
<> <>
{options.map((option) => ( {options.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{option.label} {option.label}
</Option> </Option>
@@ -113,7 +113,7 @@ export function TransferSafeguardsOptions() {
return ( return (
<> <>
{options.map((option) => ( {options.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{option.label} {option.label}
</Option> </Option>
@@ -135,7 +135,7 @@ export function DataProtectionImpactAssessmentOptions() {
return ( return (
<> <>
{options.map((option) => ( {options.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{option.label} {option.label}
</Option> </Option>
@@ -157,7 +157,7 @@ export function TransferImpactAssessmentOptions() {
return ( return (
<> <>
{options.map((option) => ( {options.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{option.label} {option.label}
</Option> </Option>
@@ -179,7 +179,7 @@ export function RoleOptions() {
return ( return (
<> <>
{options.map((option) => ( {options.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{option.label} {option.label}
</Option> </Option>

View File

@@ -7,7 +7,7 @@ export function SnapshotTypeOptions() {
return ( return (
<> <>
{snapshotTypes.map((type) => ( {snapshotTypes.map(type => (
<Option key={type} value={type}> <Option key={type} value={type}>
{getSnapshotTypeLabel(__, type)} {getSnapshotTypeLabel(__, type)}
</Option> </Option>

View File

@@ -1,681 +0,0 @@
import { useTranslate } from "@probo/i18n";
import {
Field,
Select,
Option,
Checkbox,
Textarea,
Spinner,
Button,
IconChevronDown,
IconChevronUp,
IconTrashCan,
IconPlusLarge,
} from "@probo/ui";
import { Suspense, useState, useMemo, useEffect } from "react";
import {
Controller,
type Control,
type UseFormSetValue,
type FieldValues,
type Path,
type PathValue,
} from "react-hook-form";
import { useLazyLoadQuery } from "react-relay";
import { graphql } from "relay-runtime";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { StateOfApplicabilityControlsFieldFrameworksQuery } from "/__generated__/core/StateOfApplicabilityControlsFieldFrameworksQuery.graphql";
import type { StateOfApplicabilityControlsFieldFrameworkControlsQuery } from "/__generated__/core/StateOfApplicabilityControlsFieldFrameworkControlsQuery.graphql";
const frameworksQuery = graphql`
query StateOfApplicabilityControlsFieldFrameworksQuery(
$organizationId: ID!
) {
organization: node(id: $organizationId) {
... on Organization {
frameworks(first: 100) {
edges {
node {
id
name
}
}
}
}
}
}
`;
const frameworkControlsQuery = graphql`
query StateOfApplicabilityControlsFieldFrameworkControlsQuery(
$frameworkId: ID!
) {
framework: node(id: $frameworkId) {
... on Framework {
id
controls(
first: 500
orderBy: { field: SECTION_TITLE, direction: ASC }
) {
edges {
node {
id
sectionTitle
name
}
}
}
}
}
}
`;
type ControlSelection = {
controlId: string;
state: "EXCLUDED" | "IMPLEMENTED" | "NOT_IMPLEMENTED";
exclusionJustification?: string;
};
type FrameworkData = {
id: string;
name: string;
controls: Array<{
id: string;
sectionTitle: string;
name: string;
}>;
};
type Props<T extends FieldValues = FieldValues> = {
control: Control<T>;
setValue: UseFormSetValue<T>;
name: string;
initialControls?: ControlSelection[];
initialFrameworkIds?: Set<string>;
};
export function StateOfApplicabilityControlsField<
T extends FieldValues = FieldValues,
>({ control, setValue, name, initialControls, initialFrameworkIds }: Props<T>) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
// Initialize form value with initialControls
useEffect(() => {
if (initialControls && initialControls.length > 0) {
setValue(name as Path<T>, initialControls as PathValue<T, Path<T>>);
}
}, [initialControls, setValue, name]);
// Initialize framework selection from initialFrameworkIds
const [selectedFrameworkIds, setSelectedFrameworkIds] = useState<
Set<string>
>(initialFrameworkIds || new Set());
const [expandedFrameworks, setExpandedFrameworks] = useState<Set<string>>(
initialFrameworkIds || new Set(),
);
const [frameworkDataMap, setFrameworkDataMap] = useState<
Map<string, FrameworkData>
>(new Map());
const [newFrameworkId, setNewFrameworkId] = useState<string>("");
// Update framework selection when initialFrameworkIds changes
useEffect(() => {
if (initialFrameworkIds) {
setSelectedFrameworkIds(initialFrameworkIds);
setExpandedFrameworks(initialFrameworkIds);
}
}, [initialFrameworkIds]);
const addFramework = (frameworkId: string) => {
if (!frameworkId || selectedFrameworkIds.has(frameworkId)) return;
setSelectedFrameworkIds(
new Set([...selectedFrameworkIds, frameworkId]),
);
setExpandedFrameworks(new Set([...expandedFrameworks, frameworkId]));
setNewFrameworkId("");
};
const removeFramework = (frameworkId: string) => {
const newSet = new Set(selectedFrameworkIds);
newSet.delete(frameworkId);
setSelectedFrameworkIds(newSet);
const newExpanded = new Set(expandedFrameworks);
newExpanded.delete(frameworkId);
setExpandedFrameworks(newExpanded);
const newMap = new Map(frameworkDataMap);
newMap.delete(frameworkId);
setFrameworkDataMap(newMap);
};
const toggleFramework = (frameworkId: string) => {
const newExpanded = new Set(expandedFrameworks);
if (newExpanded.has(frameworkId)) {
newExpanded.delete(frameworkId);
} else {
newExpanded.add(frameworkId);
}
setExpandedFrameworks(newExpanded);
};
return (
<div className="space-y-4">
<div>
<h4 className="font-medium text-txt-primary mb-4">
{__("Select Controls")}
</h4>
<div className="space-y-3">
<Field label={__("Add Framework")}>
<Suspense
fallback={
<Select
variant="editor"
disabled
placeholder={__("Loading...")}
/>
}
>
<FrameworkSelect
organizationId={organizationId}
selectedFrameworkIds={selectedFrameworkIds}
value={newFrameworkId}
onValueChange={setNewFrameworkId}
onAdd={addFramework}
/>
</Suspense>
</Field>
{Array.from(selectedFrameworkIds).map((frameworkId) => (
<Suspense
key={frameworkId}
fallback={
<div className="flex items-center justify-center py-8">
<Spinner />
</div>
}
>
<FrameworkSection
frameworkId={frameworkId}
isExpanded={expandedFrameworks.has(frameworkId)}
onToggle={() => toggleFramework(frameworkId)}
onRemove={() => removeFramework(frameworkId)}
control={control}
name={name}
frameworkDataMap={frameworkDataMap}
setFrameworkDataMap={setFrameworkDataMap}
/>
</Suspense>
))}
</div>
</div>
</div>
);
}
function FrameworkSelect({
organizationId,
selectedFrameworkIds,
value,
onValueChange,
onAdd,
}: {
organizationId: string;
selectedFrameworkIds: Set<string>;
value: string;
onValueChange: (value: string) => void;
onAdd: (frameworkId: string) => void;
}) {
const { __ } = useTranslate();
const data =
useLazyLoadQuery<StateOfApplicabilityControlsFieldFrameworksQuery>(
frameworksQuery,
{ organizationId },
{ fetchPolicy: "network-only" },
);
const frameworks: Array<{ id: string; name: string }> =
(data?.organization &&
"frameworks" in data.organization &&
data.organization.frameworks?.edges
?.map((edge) => edge.node)
.filter(
(node): node is NonNullable<typeof node> => node !== null,
)) ||
[];
const availableFrameworks = frameworks.filter(
(framework: { id: string; name: string }) =>
!selectedFrameworkIds.has(framework.id),
);
return (
<div className="flex gap-2">
<Select
variant="editor"
placeholder={__("Select a framework")}
onValueChange={onValueChange}
value={value}
className="flex-1"
>
{availableFrameworks.map(
(framework: { id: string; name: string }) => (
<Option key={framework.id} value={framework.id}>
{framework.name}
</Option>
),
)}
</Select>
<Button
type="button"
variant="secondary"
icon={IconPlusLarge}
onClick={() => onAdd(value)}
disabled={!value || selectedFrameworkIds.has(value)}
>
{__("Add")}
</Button>
</div>
);
}
function FrameworkSection<T extends FieldValues = FieldValues>({
frameworkId,
isExpanded,
onToggle,
onRemove,
control,
name,
frameworkDataMap,
setFrameworkDataMap,
}: {
frameworkId: string;
isExpanded: boolean;
onToggle: () => void;
onRemove: () => void;
control: Control<T>;
name: string;
frameworkDataMap: Map<string, FrameworkData>;
setFrameworkDataMap: React.Dispatch<
React.SetStateAction<Map<string, FrameworkData>>
>;
}) {
const { __ } = useTranslate();
const data =
useLazyLoadQuery<StateOfApplicabilityControlsFieldFrameworkControlsQuery>(
frameworkControlsQuery,
{ frameworkId },
{ fetchPolicy: "network-only" },
);
const framework =
data?.framework && "controls" in data.framework ? data.framework : null;
const frameworkName: string =
framework && "name" in framework && typeof framework.name === "string"
? framework.name
: "";
const controls: Array<{ id: string; sectionTitle: string; name: string }> =
useMemo(
() =>
framework?.controls?.edges
?.map((edge) => edge.node)
.filter(
(node): node is NonNullable<typeof node> =>
node !== null,
) ?? [],
[framework],
);
useEffect(() => {
if (framework && !frameworkDataMap.has(frameworkId)) {
setFrameworkDataMap((prev) => {
const newMap = new Map(prev);
newMap.set(frameworkId, {
id: frameworkId,
name: frameworkName,
controls,
});
return newMap;
});
}
}, [
framework,
frameworkId,
frameworkName,
controls,
frameworkDataMap,
setFrameworkDataMap,
]);
const cachedData = frameworkDataMap.get(frameworkId);
const displayName: string = cachedData?.name || frameworkName;
const displayControls: Array<{
id: string;
sectionTitle: string;
name: string;
}> = cachedData?.controls || controls;
return (
<div className="border border-border-low rounded-lg">
<div className="flex items-center justify-between p-4">
<button
type="button"
onClick={onToggle}
className="flex items-center gap-2 flex-1 text-left hover:bg-subtle -m-4 p-4 rounded-lg"
>
{isExpanded ? (
<IconChevronUp
size={16}
className="text-txt-tertiary"
/>
) : (
<IconChevronDown
size={16}
className="text-txt-tertiary"
/>
)}
<span className="font-medium text-txt-primary">
{displayName}
</span>
<span className="text-sm text-txt-tertiary">
({displayControls.length} {__("controls")})
</span>
</button>
<Button
type="button"
variant="secondary"
icon={IconTrashCan}
onClick={onRemove}
className="ml-2"
>
{__("Remove")}
</Button>
</div>
{isExpanded && (
<div className="border-t border-border-low">
<Controller
control={control}
name={name as Path<T>}
render={({ field }) => {
const selectedControls: ControlSelection[] = (
Array.isArray(field.value) ? field.value : []
) as ControlSelection[];
const selectedControlIds = new Set(
selectedControls.map(
(c: ControlSelection) => c.controlId,
),
);
const toggleControl = (controlId: string) => {
const isSelected =
selectedControlIds.has(controlId);
if (isSelected) {
field.onChange(
selectedControls.filter(
(c: ControlSelection) =>
c.controlId !== controlId,
),
);
} else {
field.onChange([
...selectedControls,
{
controlId,
state: "IMPLEMENTED" as const,
exclusionJustification: undefined,
},
]);
}
};
const updateControlState = (
controlId: string,
state:
| "EXCLUDED"
| "IMPLEMENTED"
| "NOT_IMPLEMENTED",
) => {
field.onChange(
selectedControls.map(
(c: ControlSelection) =>
c.controlId === controlId
? { ...c, state }
: c,
),
);
};
const updateJustification = (
controlId: string,
exclusionJustification: string,
) => {
field.onChange(
selectedControls.map(
(c: ControlSelection) =>
c.controlId === controlId
? {
...c,
exclusionJustification,
}
: c,
),
);
};
const getControlState = (controlId: string) => {
const selected = selectedControls.find(
(c: ControlSelection) =>
c.controlId === controlId,
);
return selected?.state || "IMPLEMENTED";
};
const getExclusionJustification = (
controlId: string,
) => {
const selected = selectedControls.find(
(c: ControlSelection) =>
c.controlId === controlId,
);
return selected?.exclusionJustification || "";
};
const selectAll = () => {
const newSelectedControls: ControlSelection[] =
[...selectedControls];
displayControls.forEach((ctrl) => {
if (!selectedControlIds.has(ctrl.id)) {
newSelectedControls.push({
controlId: ctrl.id,
state: "IMPLEMENTED" as const,
exclusionJustification: undefined,
});
}
});
field.onChange(newSelectedControls);
};
const deselectAll = () => {
const controlIdsToRemove = new Set(
displayControls.map((ctrl) => ctrl.id),
);
const newSelectedControls =
selectedControls.filter(
(c: ControlSelection) =>
!controlIdsToRemove.has(
c.controlId,
),
);
field.onChange(newSelectedControls);
};
const allSelected =
displayControls.length > 0 &&
displayControls.every((ctrl) =>
selectedControlIds.has(ctrl.id),
);
return (
<div className="p-4 space-y-4">
{displayControls.length > 0 && (
<div className="flex justify-end">
<Button
type="button"
variant="quaternary"
onClick={
allSelected
? deselectAll
: selectAll
}
className="text-xs h-7 min-h-7"
>
{allSelected
? __("Deselect All")
: __("Select All")}
</Button>
</div>
)}
<div className="border border-border-low rounded-lg max-h-96 overflow-y-auto">
{displayControls.length === 0 ? (
<div className="p-4 text-center text-txt-tertiary">
{__(
"No controls found in this framework",
)}
</div>
) : (
<div className="divide-y divide-border-low">
{displayControls.map((ctrl) => {
const isSelected =
selectedControlIds.has(
ctrl.id,
);
const state =
getControlState(
ctrl.id,
);
const exclusionJustification =
getExclusionJustification(
ctrl.id,
);
return (
<div
key={ctrl.id}
className="p-4 space-y-3"
>
<div className="flex items-start gap-3">
<Checkbox
checked={
isSelected
}
onChange={() =>
toggleControl(
ctrl.id,
)
}
/>
<div className="flex-1 min-w-0">
<div className="font-medium">
{
ctrl.sectionTitle
}
:{" "}
{
ctrl.name
}
</div>
</div>
</div>
{isSelected && (
<div className="ml-7 space-y-2">
<Field
label={__(
"State",
)}
>
<Select
variant="editor"
value={
state
}
onValueChange={(
value,
) =>
updateControlState(
ctrl.id,
value as
| "EXCLUDED"
| "IMPLEMENTED"
| "NOT_IMPLEMENTED",
)
}
className="w-full"
>
<Option value="IMPLEMENTED">
{__(
"Implemented",
)}
</Option>
<Option value="NOT_IMPLEMENTED">
{__(
"Not Implemented",
)}
</Option>
<Option value="EXCLUDED">
{__(
"Excluded",
)}
</Option>
</Select>
</Field>
{state ===
"EXCLUDED" ||
(state ===
"NOT_IMPLEMENTED" && (
<Field
label={__(
"Justification",
)}
>
<Textarea
value={
exclusionJustification
}
onChange={(
e,
) =>
updateJustification(
ctrl.id,
e
.target
.value,
)
}
placeholder={__(
"Reason for exclusion",
)}
autogrow
/>
</Field>
))}
</div>
)}
</div>
);
})}
</div>
)}
</div>
</div>
);
}}
/>
</div>
)}
</div>
);
}

View File

@@ -45,7 +45,7 @@ export function VendorsMultiSelectField<T extends FieldValues = FieldValues>({
} }
function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>( function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedVendors"> props: Pick<Props<T>, "organizationId" | "control" | "name" | "disabled" | "selectedVendors">,
) { ) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { name, organizationId, control, selectedVendors = [] } = props; const { name, organizationId, control, selectedVendors = [] } = props;
@@ -54,7 +54,7 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
const allVendors = [...vendors]; const allVendors = [...vendors];
if (props.disabled) { if (props.disabled) {
selectedVendors.forEach(selectedVendor => { selectedVendors.forEach((selectedVendor) => {
if (!allVendors.find(v => v.id === selectedVendor.id)) { if (!allVendors.find(v => v.id === selectedVendor.id)) {
allVendors.push(selectedVendor); allVendors.push(selectedVendor);
} }
@@ -98,7 +98,7 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
open={isOpen} open={isOpen}
onOpenChange={setIsOpen} onOpenChange={setIsOpen}
> >
{availableVendors.map((vendor) => ( {availableVendors.map(vendor => (
<Option key={vendor.id} value={vendor.id} className="flex gap-2"> <Option key={vendor.id} value={vendor.id} className="flex gap-2">
<Avatar <Avatar
name={vendor.name} name={vendor.name}
@@ -120,7 +120,7 @@ function VendorsMultiSelectWithQuery<T extends FieldValues = FieldValues>(
{selectedVendors.length > 0 && ( {selectedVendors.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{selectedVendors.map((vendor) => ( {selectedVendors.map(vendor => (
<Badge key={vendor.id} variant="neutral" className="flex items-center gap-2"> <Badge key={vendor.id} variant="neutral" className="flex items-center gap-2">
<Avatar <Avatar
name={vendor.name} name={vendor.name}

View File

@@ -136,7 +136,7 @@ export function LinkedMeasuresCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{measures.map((measure) => ( {measures.map(measure => (
<MeasureRow <MeasureRow
key={measure.id} key={measure.id}
measure={measure} measure={measure}

View File

@@ -44,28 +44,28 @@ export function LinkedMeasureDialog({ children, ...props }: Props) {
function LinkedMeasuresDialogContent(props: Omit<Props, "children">) { function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { data, loadNext, hasNext, isLoadingNext } = const { data, loadNext, hasNext, isLoadingNext }
usePaginatedMeasures(organizationId); = usePaginatedMeasures(organizationId);
const { __ } = useTranslate(); const { __ } = useTranslate();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [category, setCategory] = useState<string | null>(null); const [category, setCategory] = useState<string | null>(null);
const measures = useMemo(() => data.measures?.edges?.map((edge) => edge.node) ?? [], [data.measures]); const measures = useMemo(() => data.measures?.edges?.map(edge => edge.node) ?? [], [data.measures]);
const linkedIds = useMemo(() => { const linkedIds = useMemo(() => {
return new Set(props.linkedMeasures?.map((m) => m.id) ?? []); return new Set(props.linkedMeasures?.map(m => m.id) ?? []);
}, [props.linkedMeasures]); }, [props.linkedMeasures]);
const filteredMeasures = useMemo(() => { const filteredMeasures = useMemo(() => {
return measures.filter( return measures.filter(
(measure) => measure =>
(category === null || measure.category === category) && (category === null || measure.category === category)
(measure.name.toLowerCase().includes(search.toLowerCase()) || && (measure.name.toLowerCase().includes(search.toLowerCase())
measure.description?.toLowerCase().includes(search.toLowerCase())) || measure.description?.toLowerCase().includes(search.toLowerCase())),
); );
}, [measures, search, category]); }, [measures, search, category]);
const categories = useMemo( const categories = useMemo(
() => Array.from(new Set(measures.map((m) => m.category))), () => Array.from(new Set(measures.map(m => m.category))),
[measures] [measures],
); );
return ( return (
@@ -82,7 +82,7 @@ function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
onValueChange={setCategory} onValueChange={setCategory}
className="max-w-[180px]" className="max-w-[180px]"
> >
{categories.map((category) => ( {categories.map(category => (
<Option key={category} value={category}> <Option key={category} value={category}>
{category} {category}
</Option> </Option>
@@ -90,7 +90,7 @@ function LinkedMeasuresDialogContent(props: Omit<Props, "children">) {
</Select> </Select>
</div> </div>
<div className="divide-y divide-border-low"> <div className="divide-y divide-border-low">
{filteredMeasures.map((measure) => ( {filteredMeasures.map(measure => (
<MeasureRow <MeasureRow
key={measure.id} key={measure.id}
measure={measure} measure={measure}
@@ -140,7 +140,9 @@ function MeasureRow(props: RowProps) {
asChild asChild
> >
<span> <span>
<IconComponent size={16} /> {isLinked ? __("Unlink") : __("Link")} <IconComponent size={16} />
{" "}
{isLinked ? __("Unlink") : __("Link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -143,7 +143,7 @@ export function LinkedObligationsCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{obligations.map((obligation) => ( {obligations.map(obligation => (
<ObligationRow <ObligationRow
key={obligation.id} key={obligation.id}
obligation={obligation} obligation={obligation}

View File

@@ -111,19 +111,19 @@ function LinkedObligationsDialogContent(props: Omit<Props, "children">) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const obligations = useMemo( const obligations = useMemo(
() => data.obligations?.edges?.map((edge) => edge.node) ?? [], () => data.obligations?.edges?.map(edge => edge.node) ?? [],
[data.obligations], [data.obligations],
); );
const linkedIds = useMemo(() => { const linkedIds = useMemo(() => {
return new Set(props.linkedObligations?.map((o) => o.id) ?? []); return new Set(props.linkedObligations?.map(o => o.id) ?? []);
}, [props.linkedObligations]); }, [props.linkedObligations]);
const filteredObligations = useMemo(() => { const filteredObligations = useMemo(() => {
return obligations.filter( return obligations.filter(
(obligation) => obligation =>
obligation.area?.toLowerCase().includes(search.toLowerCase()) || obligation.area?.toLowerCase().includes(search.toLowerCase())
obligation.source?.toLowerCase().includes(search.toLowerCase()) || || obligation.source?.toLowerCase().includes(search.toLowerCase())
obligation.owner?.fullName || obligation.owner?.fullName
?.toLowerCase() ?.toLowerCase()
.includes(search.toLowerCase()), .includes(search.toLowerCase()),
); );
@@ -139,7 +139,7 @@ function LinkedObligationsDialogContent(props: Omit<Props, "children">) {
/> />
</div> </div>
<div className="divide-y divide-border-low"> <div className="divide-y divide-border-low">
{filteredObligations.map((obligation) => ( {filteredObligations.map(obligation => (
<ObligationRow <ObligationRow
key={obligation.id} key={obligation.id}
obligation={obligation} obligation={obligation}

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react"; import { useState } from "react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -32,14 +32,9 @@ export function DeleteOrganizationDialog({
const handleConfirm = () => { const handleConfirm = () => {
if (inputValue === organizationName) { if (inputValue === organizationName) {
onConfirm(); onConfirm();
}
};
useEffect(() => {
if (!isDeleting) {
setInputValue(""); setInputValue("");
} }
}, [isDeleting]); };
return ( return (
<Dialog <Dialog
@@ -52,7 +47,7 @@ export function DeleteOrganizationDialog({
<p className="text-txt-secondary text-sm"> <p className="text-txt-secondary text-sm">
{sprintf( {sprintf(
__("This will permanently delete the organization %s and all its data."), __("This will permanently delete the organization %s and all its data."),
organizationName organizationName,
)} )}
</p> </p>
@@ -62,12 +57,12 @@ export function DeleteOrganizationDialog({
<Field <Field
label={sprintf( label={sprintf(
__('To confirm deletion, type "%s" below:'), __("To confirm deletion, type \"%s\" below:"),
organizationName organizationName,
)} )}
type="text" type="text"
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={e => setInputValue(e.target.value)}
placeholder={organizationName} placeholder={organizationName}
disabled={isDeleting} disabled={isDeleting}
autoComplete="off" autoComplete="off"

View File

@@ -27,7 +27,7 @@ export function SlackConnections({
]; ];
const slackConnections = slackConnectionDefinitions.map((def) => { const slackConnections = slackConnectionDefinitions.map((def) => {
const connected = connectedSlackConnections.find((c) => c.id); const connected = connectedSlackConnections.find(c => c.id);
return { return {
...def, ...def,
createdAt: connected?.createdAt, createdAt: connected?.createdAt,
@@ -49,7 +49,7 @@ export function SlackConnections({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{slackConnections.map((slackConnection) => ( {slackConnections.map(slackConnection => (
<Card <Card
key={slackConnection.id} key={slackConnection.id}
padded padded
@@ -61,7 +61,8 @@ export function SlackConnections({
<div className="mr-auto"> <div className="mr-auto">
<h3 className="text-base font-semibold">{slackConnection.name}</h3> <h3 className="text-base font-semibold">{slackConnection.name}</h3>
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{slackConnection.createdAt ? ( {slackConnection.createdAt
? (
<> <>
{sprintf( {sprintf(
__("Connected on %s"), __("Connected on %s"),
@@ -74,18 +75,21 @@ export function SlackConnections({
</> </>
)} )}
</> </>
) : ( )
: (
slackConnection.description slackConnection.description
)} )}
</p> </p>
</div> </div>
{slackConnection.createdAt ? ( {slackConnection.createdAt
? (
<div> <div>
<Badge variant="success" size="md"> <Badge variant="success" size="md">
{__("Connected")} {__("Connected")}
</Badge> </Badge>
</div> </div>
) : ( )
: (
canUpdate && ( canUpdate && (
<Button variant="secondary" asChild> <Button variant="secondary" asChild>
<a href={getUrl(slackConnection.id)}>{__("Connect")}</a> <a href={getUrl(slackConnection.id)}>{__("Connect")}</a>

View File

@@ -103,7 +103,7 @@ export function LinkedRisksCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{props.risks.map((risk) => ( {props.risks.map(risk => (
<RiskRow <RiskRow
key={risk.id} key={risk.id}
risk={risk} risk={risk}

View File

@@ -74,24 +74,24 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [category, setCategory] = useState<string | null>(null); const [category, setCategory] = useState<string | null>(null);
const risks = useMemo( const risks = useMemo(
() => data.organization?.risks?.edges?.map((edge) => edge.node) ?? [], () => data.organization?.risks?.edges?.map(edge => edge.node) ?? [],
[data.organization?.risks], [data.organization?.risks],
); );
const linkedIds = useMemo(() => { const linkedIds = useMemo(() => {
return new Set(props.linkedRisks?.map((r) => r.id) ?? []); return new Set(props.linkedRisks?.map(r => r.id) ?? []);
}, [props.linkedRisks]); }, [props.linkedRisks]);
const filteredRisks = useMemo(() => { const filteredRisks = useMemo(() => {
return risks.filter( return risks.filter(
(risk) => risk =>
(category === null || risk.category === category) && (category === null || risk.category === category)
(risk.name.toLowerCase().includes(search.toLowerCase()) || && (risk.name.toLowerCase().includes(search.toLowerCase())
risk.description?.toLowerCase().includes(search.toLowerCase())), || risk.description?.toLowerCase().includes(search.toLowerCase())),
); );
}, [risks, search, category]); }, [risks, search, category]);
const categories = useMemo( const categories = useMemo(
() => Array.from(new Set(risks.map((r) => r.category))), () => Array.from(new Set(risks.map(r => r.category))),
[risks], [risks],
); );
@@ -109,7 +109,7 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
onValueChange={setCategory} onValueChange={setCategory}
className="max-w-[180px]" className="max-w-[180px]"
> >
{categories.map((category) => ( {categories.map(category => (
<Option key={category} value={category}> <Option key={category} value={category}>
{category} {category}
</Option> </Option>
@@ -117,7 +117,7 @@ function LinkedRisksDialogContent(props: Omit<Props, "children">) {
</Select> </Select>
</div> </div>
<div className="divide-y divide-border-low"> <div className="divide-y divide-border-low">
{filteredRisks.map((risk) => ( {filteredRisks.map(risk => (
<RiskRow <RiskRow
key={risk.id} key={risk.id}
risk={risk} risk={risk}
@@ -167,7 +167,9 @@ function RiskRow(props: RowProps) {
asChild asChild
> >
<span> <span>
<IconComponent size={16} /> {isLinked ? __("Unlink") : __("Link")} <IconComponent size={16} />
{" "}
{isLinked ? __("Unlink") : __("Link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -136,7 +136,7 @@ export function LinkedSnapshotsCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{snapshots.map((snapshot) => ( {snapshots.map(snapshot => (
<SnapshotRow <SnapshotRow
key={snapshot.id} key={snapshot.id}
snapshot={snapshot} snapshot={snapshot}

View File

@@ -101,15 +101,15 @@ function LinkedSnapshotsDialogContent(props: Omit<Props, "children">) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const snapshots = useMemo( const snapshots = useMemo(
() => data.snapshots?.edges?.map((edge) => edge.node) ?? [], () => data.snapshots?.edges?.map(edge => edge.node) ?? [],
[data.snapshots], [data.snapshots],
); );
const linkedIds = useMemo(() => { const linkedIds = useMemo(() => {
return new Set(props.linkedSnapshots?.map((s) => s.id) ?? []); return new Set(props.linkedSnapshots?.map(s => s.id) ?? []);
}, [props.linkedSnapshots]); }, [props.linkedSnapshots]);
const filteredSnapshots = useMemo(() => { const filteredSnapshots = useMemo(() => {
return snapshots.filter((snapshot) => return snapshots.filter(snapshot =>
snapshot.name.toLowerCase().includes(search.toLowerCase()), snapshot.name.toLowerCase().includes(search.toLowerCase()),
); );
}, [snapshots, search]); }, [snapshots, search]);
@@ -124,7 +124,7 @@ function LinkedSnapshotsDialogContent(props: Omit<Props, "children">) {
/> />
</div> </div>
<div className="divide-y divide-border-low"> <div className="divide-y divide-border-low">
{filteredSnapshots.map((snapshot) => ( {filteredSnapshots.map(snapshot => (
<SnapshotRow <SnapshotRow
key={snapshot.id} key={snapshot.id}
snapshot={snapshot} snapshot={snapshot}
@@ -187,7 +187,9 @@ function SnapshotRow(props: RowProps) {
asChild asChild
> >
<span> <span>
<IconComponent size={16} /> {isLinked ? __("Unlink") : __("Link")} <IconComponent size={16} />
{" "}
{isLinked ? __("Unlink") : __("Link")}
</span> </span>
</Button> </Button>
</button> </button>

View File

@@ -88,8 +88,8 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
: props.statesOfApplicability; : props.statesOfApplicability;
}, [props.statesOfApplicability, limit]); }, [props.statesOfApplicability, limit]);
const showMoreButton = const showMoreButton
limit !== null && props.statesOfApplicability.length > limit; = limit !== null && props.statesOfApplicability.length > limit;
const variant = props.variant ?? "table"; const variant = props.variant ?? "table";
const linkedData = linkedInfo; const linkedData = linkedInfo;
@@ -135,10 +135,10 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
onExtracted={(info) => { onExtracted={(info) => {
setLinkedInfo((prev) => { setLinkedInfo((prev) => {
const exists = prev.some( const exists = prev.some(
(p) => p =>
p.stateOfApplicabilityId === p.stateOfApplicabilityId
info.stateOfApplicabilityId && === info.stateOfApplicabilityId
p.controlId === info.controlId, && p.controlId === info.controlId,
); );
return exists ? prev : [...prev, info]; return exists ? prev : [...prev, info];
}); });
@@ -185,7 +185,7 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
</Td> </Td>
</Tr> </Tr>
)} )}
{statesOfApplicability.map((soa) => ( {statesOfApplicability.map(soa => (
<StateOfApplicabilityRow <StateOfApplicabilityRow
key={soa.id} key={soa.id}
stateOfApplicability={soa} stateOfApplicability={soa}
@@ -216,7 +216,7 @@ export function LinkedStatesOfApplicabilityCard<Params>(props: Props<Params>) {
> >
{sprintf( {sprintf(
__("Show %d more"), __("Show %d more"),
props.statesOfApplicability.length - limit!, props.statesOfApplicability.length - limit,
)} )}
</Button> </Button>
)} )}
@@ -231,17 +231,18 @@ function LinkedInfoExtractor(props: {
controlId: string; controlId: string;
}) => void; }) => void;
}) { }) {
const { fragment, onExtracted } = props;
const data = useFragment( const data = useFragment(
linkedStateOfApplicabilityFragment, linkedStateOfApplicabilityFragment,
props.fragment, fragment,
); );
useEffect(() => { useEffect(() => {
props.onExtracted({ onExtracted({
stateOfApplicabilityId: data.stateOfApplicabilityId, stateOfApplicabilityId: data.stateOfApplicabilityId,
controlId: data.controlId, controlId: data.controlId,
}); });
}, [data.stateOfApplicabilityId, data.controlId, props.onExtracted]); }, [data.stateOfApplicabilityId, data.controlId, onExtracted]);
return null; return null;
} }
@@ -281,8 +282,7 @@ function StateOfApplicabilityRow(props: {
props.onClick( props.onClick(
soa.stateOfApplicabilityId, soa.stateOfApplicabilityId,
soa.controlId, soa.controlId,
) )}
}
icon={IconTrashCan} icon={IconTrashCan}
> >
{__("Unlink")} {__("Unlink")}

View File

@@ -94,18 +94,18 @@ function LinkedStatesOfApplicabilityDialogContent(
const linkedSOAIds = new Set( const linkedSOAIds = new Set(
props.linkedStatesOfApplicability.map( props.linkedStatesOfApplicability.map(
(soa) => soa.stateOfApplicabilityId, soa => soa.stateOfApplicabilityId,
), ),
); );
const linkedSOAMap = new Map( const linkedSOAMap = new Map(
props.linkedStatesOfApplicability.map((soa) => [ props.linkedStatesOfApplicability.map(soa => [
soa.stateOfApplicabilityId, soa.stateOfApplicabilityId,
soa, soa,
]), ]),
); );
const statesOfApplicability = const statesOfApplicability
data.organization?.statesOfApplicability?.edges.map( = data.organization?.statesOfApplicability?.edges.map(
(edge) => edge.node, edge => edge.node,
) ?? []; ) ?? [];
const handleSelectSOA = (soa: { id: string; name: string }) => { const handleSelectSOA = (soa: { id: string; name: string }) => {
@@ -138,7 +138,8 @@ function LinkedStatesOfApplicabilityDialogContent(
return ( return (
<> <>
<DialogContent padded className="space-y-4"> <DialogContent padded className="space-y-4">
{statesOfApplicability.length === 0 ? ( {statesOfApplicability.length === 0
? (
<div className="flex flex-col items-center justify-center py-12 text-center"> <div className="flex flex-col items-center justify-center py-12 text-center">
<div className="text-txt-secondary text-base mb-2"> <div className="text-txt-secondary text-base mb-2">
{__("No states of applicability available")} {__("No states of applicability available")}
@@ -149,7 +150,9 @@ function LinkedStatesOfApplicabilityDialogContent(
)} )}
</div> </div>
</div> </div>
) : !selectedSOA ? ( )
: !selectedSOA
? (
<div className="space-y-2"> <div className="space-y-2">
<div className="text-sm font-medium mb-2"> <div className="text-sm font-medium mb-2">
{__("Select a state of applicability:")} {__("Select a state of applicability:")}
@@ -161,16 +164,16 @@ function LinkedStatesOfApplicabilityDialogContent(
key={soa.id} key={soa.id}
className={`border border-border-low rounded-lg p-3 flex items-center justify-between ${!isLinked ? "hover:bg-hover cursor-pointer" : ""}`} className={`border border-border-low rounded-lg p-3 flex items-center justify-between ${!isLinked ? "hover:bg-hover cursor-pointer" : ""}`}
onClick={() => onClick={() =>
!isLinked && handleSelectSOA(soa) !isLinked && handleSelectSOA(soa)}
}
> >
<div className="font-medium"> <div className="font-medium">
{soa.name} {soa.name}
</div> </div>
{isLinked ? ( {isLinked
? (
<div <div
className="flex items-center gap-2" className="flex items-center gap-2"
onClick={(e) => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<Badge variant="success"> <Badge variant="success">
{__("Linked")} {__("Linked")}
@@ -178,19 +181,20 @@ function LinkedStatesOfApplicabilityDialogContent(
<Button <Button
variant="danger" variant="danger"
onClick={() => onClick={() =>
handleUnlink(soa.id) handleUnlink(soa.id)}
}
disabled={props.disabled} disabled={props.disabled}
> >
{__("Unlink")} {__("Unlink")}
</Button> </Button>
</div> </div>
) : null} )
: null}
</div> </div>
); );
})} })}
</div> </div>
) : ( )
: (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -213,9 +217,8 @@ function LinkedStatesOfApplicabilityDialogContent(
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<Checkbox <Checkbox
checked={applicability} checked={applicability}
onChange={(checked) => onChange={checked =>
setApplicability(checked) setApplicability(checked)}
}
/> />
<span className="font-medium"> <span className="font-medium">
{__("Applicable")} {__("Applicable")}
@@ -229,9 +232,8 @@ function LinkedStatesOfApplicabilityDialogContent(
<Textarea <Textarea
placeholder={__("Add a justification...")} placeholder={__("Add a justification...")}
value={justification} value={justification}
onChange={(e) => onChange={e =>
setJustification(e.target.value) setJustification(e.target.value)}
}
rows={3} rows={3}
/> />
</div> </div>
@@ -240,7 +242,8 @@ function LinkedStatesOfApplicabilityDialogContent(
)} )}
</DialogContent> </DialogContent>
<DialogFooter exitLabel={__("Close")}> <DialogFooter exitLabel={__("Close")}>
{selectedSOA ? ( {selectedSOA
? (
<> <>
<Button <Button
variant="secondary" variant="secondary"
@@ -256,7 +259,8 @@ function LinkedStatesOfApplicabilityDialogContent(
{__("Link")} {__("Link")}
</Button> </Button>
</> </>
) : ( )
: (
<></> <></>
)} )}
</DialogFooter> </DialogFooter>

View File

@@ -81,7 +81,7 @@ export function EditableTable<
))} ))}
<CellHead /> <CellHead />
</Row> </Row>
{props.items.map((item) => ( {props.items.map(item => (
<EditableRow onUpdate={(k, v) => update(item.id, k, v)} key={item.id}> <EditableRow onUpdate={(k, v) => update(item.id, k, v)} key={item.id}>
{props.row({ {props.row({
item, item,
@@ -91,7 +91,8 @@ export function EditableTable<
<Cell>{props.action({ item })}</Cell> <Cell>{props.action({ item })}</Cell>
</EditableRow> </EditableRow>
))} ))}
{showAdd ? ( {showAdd
? (
<NewItemRow <NewItemRow
schema={props.schema} schema={props.schema}
defaultValue={props.defaultValue} defaultValue={props.defaultValue}
@@ -100,7 +101,8 @@ export function EditableTable<
mutation={props.createMutation} mutation={props.createMutation}
onSuccess={toggleAdd} onSuccess={toggleAdd}
/> />
) : ( )
: (
<RowButton onClick={toggleAdd} type="button">{props.addLabel}</RowButton> <RowButton onClick={toggleAdd} type="button">{props.addLabel}</RowButton>
)} )}
</SortableDataTable> </SortableDataTable>
@@ -144,7 +146,7 @@ function NewItemRow<T extends { id: string }, S extends z.ZodSchema>(props: {
disabled={!isOk || isMutating} disabled={!isOk || isMutating}
variant="tertiary" variant="tertiary"
className={clsx(isOk ? "text-txt-success" : "text-txt-secondary")} className={clsx(isOk ? "text-txt-success" : "text-txt-secondary")}
onClick={onSubmit} onClick={() => void onSubmit()}
> >
{isMutating ? <Spinner size={16} /> : <IconCheckmark1 size={16} />} {isMutating ? <Spinner size={16} /> : <IconCheckmark1 size={16} />}
</Button> </Button>

View File

@@ -79,7 +79,8 @@ export function GraphQLCell<Q extends OperationType, T extends NonNullable<unkno
value={value} value={value}
itemRenderer={props.itemRenderer} itemRenderer={props.itemRenderer}
/> />
</div>{" "} </div>
{" "}
{props.multiple && ( {props.multiple && (
<Command.Input <Command.Input
className={classNames.input()} className={classNames.input()}
@@ -88,11 +89,11 @@ export function GraphQLCell<Q extends OperationType, T extends NonNullable<unkno
)} )}
<Command.List> <Command.List>
<Suspense <Suspense
fallback={ fallback={(
<div className="py-2 px-3 flex items-center justify-center"> <div className="py-2 px-3 flex items-center justify-center">
<Spinner /> <Spinner />
</div> </div>
} )}
> >
<ItemList <ItemList
{...props} {...props}
@@ -121,8 +122,8 @@ function ItemList<Q extends OperationType, T>(
return ( return (
<> <>
{items {items
.filter((item) => !props.usedKeys.has(getKey(item) ?? "")) .filter(item => !props.usedKeys.has(getKey(item) ?? ""))
.map((item) => ( .map(item => (
<Command.Item <Command.Item
key={getKey(item)} key={getKey(item)}
className={props.className} className={props.className}

View File

@@ -18,9 +18,8 @@ export function PeopleCell(props: Props) {
organizationId: props.organizationId, organizationId: props.organizationId,
filter: { excludeContractEnded: true }, filter: { excludeContractEnded: true },
}} }}
items={(data) => items={data =>
data.organization?.peoples?.edges.map((edge) => edge.node) ?? [] data.organization?.peoples?.edges.map(edge => edge.node) ?? []}
}
itemRenderer={({ item }) => ( itemRenderer={({ item }) => (
<div className="flex gap-2 whitespace-nowrap items-center text-xs"> <div className="flex gap-2 whitespace-nowrap items-center text-xs">
<Avatar name={item.fullName} /> <Avatar name={item.fullName} />

View File

@@ -26,9 +26,8 @@ export function VendorsCell(props: Props) {
variables={{ variables={{
organizationId: props.organizationId, organizationId: props.organizationId,
}} }}
items={(data) => items={data =>
data.organization?.vendors?.edges?.map((edge) => edge.node) ?? [] data.organization?.vendors?.edges?.map(edge => edge.node) ?? []}
}
itemRenderer={({ item, onRemove }) => ( itemRenderer={({ item, onRemove }) => (
<VendorBadge vendor={item} onRemove={onRemove} /> <VendorBadge vendor={item} onRemove={onRemove} />
)} )}

View File

@@ -75,7 +75,7 @@ const createTaskSchema = z.object({
timeEstimate: z.string().optional().nullable(), timeEstimate: z.string().optional().nullable(),
assignedToId: z.string().optional().nullable(), assignedToId: z.string().optional().nullable(),
measureId: z.preprocess( measureId: z.preprocess(
(val) => (val === "" || val == null ? null : val), val => (val === "" || val == null ? null : val),
z.string().nullable().optional(), z.string().nullable().optional(),
), ),
deadline: z.string().optional().nullable(), deadline: z.string().optional().nullable(),
@@ -86,11 +86,11 @@ const updateTaskSchema = z.object({
description: z.string().optional().nullable(), description: z.string().optional().nullable(),
timeEstimate: z.string().optional().nullable(), timeEstimate: z.string().optional().nullable(),
assignedToId: z.preprocess( assignedToId: z.preprocess(
(val) => (val === "" || val == null ? null : val), val => (val === "" || val == null ? null : val),
z.string().nullable().optional(), z.string().nullable().optional(),
), ),
measureId: z.preprocess( measureId: z.preprocess(
(val) => (val === "" || val == null ? null : val), val => (val === "" || val == null ? null : val),
z.string().nullable().optional(), z.string().nullable().optional(),
), ),
deadline: z.string().optional().nullable(), deadline: z.string().optional().nullable(),
@@ -105,11 +105,12 @@ type Props = {
}; };
export default function TaskFormDialog(props: Props) { export default function TaskFormDialog(props: Props) {
const { children, connection, ref, task: taskKey, measureId } = props;
const { __ } = useTranslate(); const { __ } = useTranslate();
const ref = useDialogRef(); const newRef = useDialogRef();
const dialogRef = props.ref ?? ref; const dialogRef = ref ?? newRef;
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const task = useFragment(taskFragment, props.task); const task = useFragment(taskFragment, taskKey);
const relayEnv = useRelayEnvironment(); const relayEnv = useRelayEnvironment();
const [mutate] = useMutationWithToasts( const [mutate] = useMutationWithToasts(
task ? taskUpdateMutation : taskCreateMutation, task ? taskUpdateMutation : taskCreateMutation,
@@ -121,19 +122,19 @@ export default function TaskFormDialog(props: Props) {
const isUpdating = !!task; const isUpdating = !!task;
const { control, handleSubmit, register, formState, reset } = const { control, handleSubmit, register, formState, reset }
useFormWithSchema(isUpdating ? updateTaskSchema : createTaskSchema, { = useFormWithSchema(isUpdating ? updateTaskSchema : createTaskSchema, {
defaultValues: { defaultValues: {
name: task?.name ?? "", name: task?.name ?? "",
description: task?.description ?? "", description: task?.description ?? "",
timeEstimate: task?.timeEstimate ?? "", timeEstimate: task?.timeEstimate ?? "",
assignedToId: task?.assignedTo?.id ?? "", assignedToId: task?.assignedTo?.id ?? "",
measureId: task?.measure?.id ?? props.measureId ?? "", measureId: task?.measure?.id ?? measureId ?? "",
deadline: task?.deadline?.split("T")[0] ?? "", deadline: task?.deadline?.split("T")[0] ?? "",
}, },
}); });
const onSubmit = handleSubmit(async (data) => { const onSubmit = async (data: z.infer<typeof updateTaskSchema | typeof createTaskSchema>) => {
if (task) { if (task) {
await mutate({ await mutate({
variables: { variables: {
@@ -160,7 +161,7 @@ export default function TaskFormDialog(props: Props) {
assignedToId: data.assignedToId || null, assignedToId: data.assignedToId || null,
measureId: data.measureId || null, measureId: data.measureId || null,
}, },
connections: [props.connection!], connections: [connection!],
}, },
onCompleted: (_response, errors) => { onCompleted: (_response, errors) => {
if (!errors && data.measureId) { if (!errors && data.measureId) {
@@ -171,20 +172,20 @@ export default function TaskFormDialog(props: Props) {
reset(); reset();
} }
dialogRef.current?.close(); dialogRef.current?.close();
}); };
const showMeasure = !props.measureId; const showMeasure = !measureId;
return ( return (
<Dialog <Dialog
ref={dialogRef} ref={dialogRef}
trigger={props.children} trigger={children}
title={ title={(
<Breadcrumb <Breadcrumb
items={[__("Tasks"), isUpdating ? __("Edit Task") : __("New Task")]} items={[__("Tasks"), isUpdating ? __("Edit Task") : __("New Task")]}
/> />
} )}
> >
<form onSubmit={onSubmit}> <form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent className="grid grid-cols-[1fr_420px]"> <DialogContent className="grid grid-cols-[1fr_420px]">
<div className="py-8 px-10 space-y-4"> <div className="py-8 px-10 space-y-4">
<Input <Input
@@ -240,7 +241,7 @@ export default function TaskFormDialog(props: Props) {
<DurationPicker <DurationPicker
{...field} {...field}
value={value ?? null} value={value ?? null}
onValueChange={(value) => onChange(value)} onValueChange={value => onChange(value)}
/> />
)} )}
/> />

View File

@@ -67,12 +67,14 @@ export function TasksCard({ tasks, connectionId }: Props) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{tasks.length === 0 ? ( {tasks.length === 0
? (
<p className="text-center py-6 text-txt-secondary">{__("No tasks")}</p> <p className="text-center py-6 text-txt-secondary">{__("No tasks")}</p>
) : ( )
: (
<Card> <Card>
<Tabs className="px-6"> <Tabs className="px-6">
{hashes.map((h) => ( {hashes.map(h => (
<TabItem asChild active={hash === h.hash} key={h.hash}> <TabItem asChild active={hash === h.hash} key={h.hash}>
<Link to={`#${h.hash}`}> <Link to={`#${h.hash}`}>
{h.label} {h.label}
@@ -83,11 +85,11 @@ export function TasksCard({ tasks, connectionId }: Props) {
</Tabs> </Tabs>
<div className="divide-y divide-border-solid"> <div className="divide-y divide-border-solid">
{hash === "all" {hash === "all"
? // All tabs group the todo using the state // All tabs group the todo using the state
hashes ? hashes
.slice(0, 2) .slice(0, 2)
.filter((h) => tasksPerHash.get(h.hash)?.length) .filter(h => tasksPerHash.get(h.hash)?.length)
.map((h) => ( .map(h => (
<Fragment key={h.label}> <Fragment key={h.label}>
<h2 className="px-6 py-3 text-sm font-medium flex items-center gap-2 bg-subtle"> <h2 className="px-6 py-3 text-sm font-medium flex items-center gap-2 bg-subtle">
<TaskStateIcon state={h.state!} /> <TaskStateIcon state={h.state!} />
@@ -102,8 +104,8 @@ export function TasksCard({ tasks, connectionId }: Props) {
))} ))}
</Fragment> </Fragment>
)) ))
: // Todo and Done tab simply list todos // Todo and Done tab simply list todos
filteredTasks?.map(({ node: task }) => ( : filteredTasks?.map(({ node: task }) => (
<TaskRow <TaskRow
key={task.id} key={task.id}
fKey={task} fKey={task}
@@ -163,15 +165,15 @@ function TaskRow(props: TaskRowProps) {
const params = useParams<{ measureId?: string }>(); const params = useParams<{ measureId?: string }>();
const relayEnv = useRelayEnvironment(); const relayEnv = useRelayEnvironment();
const { canUpdate, canDelete, ...task } = const { canUpdate, canDelete, ...task }
useFragment<TasksCard_TaskRowFragment$key>( = useFragment<TasksCard_TaskRowFragment$key>(
fragment, fragment,
props.fKey as TasksCard_TaskRowFragment$key, props.fKey as TasksCard_TaskRowFragment$key,
); );
const [updateTask, isUpdating] = useMutation(taskUpdateMutation); const [updateTask, isUpdating] = useMutation(taskUpdateMutation);
const onToggle = () => { const onToggle = async () => {
promisifyMutation(updateTask)({ await promisifyMutation(updateTask)({
variables: { variables: {
input: { input: {
taskId: task.id, taskId: task.id,
@@ -217,7 +219,7 @@ function TaskRow(props: TaskRowProps) {
<div className="flex items-center gap-2 pt-[2px]"> <div className="flex items-center gap-2 pt-[2px]">
<PriorityLevel level={1} /> <PriorityLevel level={1} />
<button <button
onClick={onToggle} onClick={() => void onToggle()}
className="cursor-pointer -m-1 p-1 disabled:opacity-60" className="cursor-pointer -m-1 p-1 disabled:opacity-60"
disabled={isUpdating} disabled={isUpdating}
> >
@@ -227,7 +229,7 @@ function TaskRow(props: TaskRowProps) {
<div className="text-sm space-y-1 flex-1"> <div className="text-sm space-y-1 flex-1">
<h2 className="font-medium">{task.name}</h2> <h2 className="font-medium">{task.name}</h2>
{task.description && ( {task.description && (
<p className="text-txt-secondary whitespace-pre-wrap break-words"> <p className="text-txt-secondary whitespace-pre-wrap wrap-break-word">
{task.description} {task.description}
</p> </p>
)} )}

View File

@@ -60,7 +60,7 @@ export function DeleteTrustCenterReferenceDialog({
<p className="text-txt-secondary"> <p className="text-txt-secondary">
{sprintf( {sprintf(
__("Are you sure you want to delete the reference \"%s\"?"), __("Are you sure you want to delete the reference \"%s\"?"),
referenceName referenceName,
)} )}
</p> </p>
<p className="text-txt-secondary mt-2"> <p className="text-txt-secondary mt-2">
@@ -71,7 +71,7 @@ export function DeleteTrustCenterReferenceDialog({
<DialogFooter> <DialogFooter>
<Button <Button
variant="danger" variant="danger"
onClick={handleDelete} onClick={() => void handleDelete()}
disabled={isDeleting} disabled={isDeleting}
icon={isDeleting ? Spinner : IconTrashCan} icon={isDeleting ? Spinner : IconTrashCan}
> >

View File

@@ -14,7 +14,7 @@ import {
} from "@probo/ui"; } from "@probo/ui";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react"; import { useMemo, useState, useCallback } from "react";
import { import {
sprintf, sprintf,
getAuditStateVariant, getAuditStateVariant,
@@ -47,7 +47,7 @@ type Mutation<Params> = (p: {
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC"; trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC";
} & Params; } & Params;
}; };
}) => void; }) => Promise<void>;
type Props<Params> = { type Props<Params> = {
audits: TrustCenterAuditsCardFragment$key[]; audits: TrustCenterAuditsCardFragment$key[];
@@ -65,11 +65,11 @@ export function TrustCenterAuditsCard<Params>(props: Props<Params>) {
}, [props.audits, limit]); }, [props.audits, limit]);
const showMoreButton = limit !== null && props.audits.length > limit; const showMoreButton = limit !== null && props.audits.length > limit;
const onChangeVisibility = ( const onChangeVisibility = async (
auditId: string, auditId: string,
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC", trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC",
) => { ) => {
props.onChangeVisibility({ await props.onChangeVisibility({
variables: { variables: {
input: { input: {
id: auditId, id: auditId,
@@ -130,7 +130,7 @@ function AuditRow(props: {
onChangeVisibility: ( onChangeVisibility: (
auditId: string, auditId: string,
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC", trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC",
) => void; ) => Promise<void>;
disabled?: boolean; disabled?: boolean;
canUpdate: boolean; canUpdate: boolean;
}) { }) {
@@ -138,26 +138,16 @@ function AuditRow(props: {
const audit = useFragment(trustCenterAuditFragment, auditFragmentRef); const audit = useFragment(trustCenterAuditFragment, auditFragmentRef);
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const handleValueChange = useCallback( const handleValueChange = useCallback(
(value: string) => { async (value: string) => {
const stringValue = typeof value === "string" ? value : ""; const stringValue = typeof value === "string" ? value : "";
const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC"; const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC";
setOptimisticValue(typedValue); await onChangeVisibility(audit.id, typedValue);
onChangeVisibility(audit.id, typedValue);
}, },
[audit.id, onChangeVisibility], [audit.id, onChangeVisibility],
); );
useEffect(() => {
if (optimisticValue && audit.trustCenterVisibility === optimisticValue) {
setOptimisticValue(null);
}
}, [audit.trustCenterVisibility, optimisticValue]);
const currentValue = optimisticValue || audit.trustCenterVisibility;
const visibilityOptions = getTrustCenterVisibilityOptions(__); const visibilityOptions = getTrustCenterVisibilityOptions(__);
const validUntilFormatted = audit.validUntil const validUntilFormatted = audit.validUntil
@@ -179,12 +169,12 @@ function AuditRow(props: {
<Td noLink width={130} className="pr-0"> <Td noLink width={130} className="pr-0">
<Field <Field
type="select" type="select"
value={currentValue} value={audit.trustCenterVisibility}
onValueChange={handleValueChange} onValueChange={value => void handleValueChange(value)}
disabled={disabled || !canUpdate} disabled={disabled || !canUpdate}
className="w-[105px]" className="w-[105px]"
> >
{visibilityOptions.map((option) => ( {visibilityOptions.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<Badge variant={option.variant}>{option.label}</Badge> <Badge variant={option.variant}>{option.label}</Badge>

View File

@@ -17,7 +17,7 @@ import {
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import type { TrustCenterDocumentsCardFragment$key } from "/__generated__/core/TrustCenterDocumentsCardFragment.graphql"; import type { TrustCenterDocumentsCardFragment$key } from "/__generated__/core/TrustCenterDocumentsCardFragment.graphql";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react"; import { useMemo, useState, useCallback } from "react";
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { useOrganizationId } from "/hooks/useOrganizationId"; import { useOrganizationId } from "/hooks/useOrganizationId";
@@ -46,7 +46,7 @@ type Mutation<Params> = (p: {
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC"; trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC";
} & Params; } & Params;
}; };
}) => void; }) => Promise<void>;
type Props<Params> = { type Props<Params> = {
documents: TrustCenterDocumentsCardFragment$key[]; documents: TrustCenterDocumentsCardFragment$key[];
@@ -64,11 +64,11 @@ export function TrustCenterDocumentsCard<Params>(props: Props<Params>) {
}, [props.documents, limit]); }, [props.documents, limit]);
const showMoreButton = limit !== null && props.documents.length > limit; const showMoreButton = limit !== null && props.documents.length > limit;
const onChangeVisibility = ( const onChangeVisibility = async (
documentId: string, documentId: string,
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC", trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC",
) => { ) => {
props.onChangeVisibility({ await props.onChangeVisibility({
variables: { variables: {
input: { input: {
id: documentId, id: documentId,
@@ -128,38 +128,28 @@ function DocumentRow(props: {
onChangeVisibility: ( onChangeVisibility: (
documentId: string, documentId: string,
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC", trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC",
) => void; ) => Promise<void>;
disabled?: boolean; disabled?: boolean;
canUpdate: boolean; canUpdate: boolean;
}) { }) {
const { documentFragmentRef, onChangeVisibility, disabled, canUpdate } = const { documentFragmentRef, onChangeVisibility, disabled, canUpdate }
props; = props;
const document = useFragment( const document = useFragment(
trustCenterDocumentFragment, trustCenterDocumentFragment,
documentFragmentRef, documentFragmentRef,
); );
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const { __ } = useTranslate(); const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const handleValueChange = useCallback( const handleValueChange = useCallback(
(value: string) => { async (value: string) => {
const stringValue = typeof value === "string" ? value : ""; const stringValue = typeof value === "string" ? value : "";
const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC"; const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC";
setOptimisticValue(typedValue); await onChangeVisibility(document.id, typedValue);
onChangeVisibility(document.id, typedValue);
}, },
[document.id, onChangeVisibility], [document.id, onChangeVisibility],
); );
useEffect(() => {
if (optimisticValue && document.trustCenterVisibility === optimisticValue) {
setOptimisticValue(null);
}
}, [document.trustCenterVisibility, optimisticValue]);
const currentValue = optimisticValue || document.trustCenterVisibility;
const visibilityOptions = getTrustCenterVisibilityOptions(__); const visibilityOptions = getTrustCenterVisibilityOptions(__);
return ( return (
@@ -178,12 +168,12 @@ function DocumentRow(props: {
<Td noLink width={130} className="pr-0"> <Td noLink width={130} className="pr-0">
<Field <Field
type="select" type="select"
value={currentValue} value={document.trustCenterVisibility}
onValueChange={handleValueChange} onValueChange={value => void handleValueChange(value)}
disabled={disabled || !canUpdate} disabled={disabled || !canUpdate}
className="w-[105px]" className="w-[105px]"
> >
{visibilityOptions.map((option) => ( {visibilityOptions.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<Badge variant={option.variant}>{option.label}</Badge> <Badge variant={option.variant}>{option.label}</Badge>

View File

@@ -21,7 +21,7 @@ import type {
TrustCenterFilesCardFragment$data, TrustCenterFilesCardFragment$data,
} from "/__generated__/core/TrustCenterFilesCardFragment.graphql"; } from "/__generated__/core/TrustCenterFilesCardFragment.graphql";
import { useFragment } from "react-relay"; import { useFragment } from "react-relay";
import { useMemo, useState, useCallback, useEffect } from "react"; import { useMemo, useState, useCallback } from "react";
import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers"; import { sprintf, getTrustCenterVisibilityOptions } from "@probo/helpers";
import { formatDate } from "@probo/helpers"; import { formatDate } from "@probo/helpers";
@@ -46,7 +46,7 @@ type Mutation<Params> = (p: {
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC"; trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC";
} & Params; } & Params;
}; };
}) => void; }) => Promise<void>;
type Props<Params> = { type Props<Params> = {
files: TrustCenterFilesCardFragment$key[]; files: TrustCenterFilesCardFragment$key[];
@@ -66,11 +66,11 @@ export function TrustCenterFilesCard<Params>(props: Props<Params>) {
}, [props.files, limit]); }, [props.files, limit]);
const showMoreButton = limit !== null && props.files.length > limit; const showMoreButton = limit !== null && props.files.length > limit;
const onChangeVisibility = ( const onChangeVisibility = async (
fileId: string, fileId: string,
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC", trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC",
) => { ) => {
props.onChangeVisibility({ await props.onChangeVisibility({
variables: { variables: {
input: { input: {
id: fileId, id: fileId,
@@ -133,7 +133,7 @@ function FileRowWrapper(props: {
onChangeVisibility: ( onChangeVisibility: (
fileId: string, fileId: string,
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC", trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC",
) => void; ) => Promise<void>;
onEdit: (file: { id: string; name: string; category: string }) => void; onEdit: (file: { id: string; name: string; category: string }) => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
disabled?: boolean; disabled?: boolean;
@@ -157,35 +157,25 @@ function FileRow(props: {
onChangeVisibility: ( onChangeVisibility: (
fileId: string, fileId: string,
trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC", trustCenterVisibility: "NONE" | "PRIVATE" | "PUBLIC",
) => void; ) => Promise<void>;
onEdit: (file: { id: string; name: string; category: string }) => void; onEdit: (file: { id: string; name: string; category: string }) => void;
onDelete: (id: string) => void; onDelete: (id: string) => void;
disabled?: boolean; disabled?: boolean;
canUpdate: boolean; canUpdate: boolean;
}) { }) {
const { file, onChangeVisibility, onEdit, onDelete, disabled, canUpdate } = const { file, onChangeVisibility, onEdit, onDelete, disabled, canUpdate }
props; = props;
const { __ } = useTranslate(); const { __ } = useTranslate();
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
const handleValueChange = useCallback( const handleValueChange = useCallback(
(value: string) => { async (value: string) => {
const stringValue = typeof value === "string" ? value : ""; const stringValue = typeof value === "string" ? value : "";
const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC"; const typedValue = stringValue as "NONE" | "PRIVATE" | "PUBLIC";
setOptimisticValue(typedValue); await onChangeVisibility(file.id, typedValue);
onChangeVisibility(file.id, typedValue);
}, },
[file.id, onChangeVisibility], [file.id, onChangeVisibility],
); );
useEffect(() => {
if (optimisticValue && file.trustCenterVisibility === optimisticValue) {
setOptimisticValue(null);
}
}, [file.trustCenterVisibility, optimisticValue]);
const currentValue = optimisticValue || file.trustCenterVisibility;
const visibilityOptions = getTrustCenterVisibilityOptions(__); const visibilityOptions = getTrustCenterVisibilityOptions(__);
return ( return (
@@ -198,12 +188,12 @@ function FileRow(props: {
<Td noLink width={130} className="pr-0"> <Td noLink width={130} className="pr-0">
<Field <Field
type="select" type="select"
value={currentValue} value={file.trustCenterVisibility}
onValueChange={handleValueChange} onValueChange={value => void handleValueChange(value)}
disabled={disabled || !canUpdate} disabled={disabled || !canUpdate}
className="w-[105px]" className="w-[105px]"
> >
{visibilityOptions.map((option) => ( {visibilityOptions.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<Badge variant={option.variant}>{option.label}</Badge> <Badge variant={option.variant}>{option.label}</Badge>
@@ -218,8 +208,7 @@ function FileRow(props: {
variant="secondary" variant="secondary"
icon={IconArrowLink} icon={IconArrowLink}
onClick={() => onClick={() =>
window.open(file.fileUrl, "_blank", "noopener,noreferrer") window.open(file.fileUrl, "_blank", "noopener,noreferrer")}
}
title={__("Download")} title={__("Download")}
/> />
{file.canUpdate && ( {file.canUpdate && (
@@ -231,8 +220,7 @@ function FileRow(props: {
id: file.id, id: file.id,
name: file.name, name: file.name,
category: file.category, category: file.category,
}) })}
}
disabled={disabled} disabled={disabled}
title={__("Edit")} title={__("Edit")}
/> />

View File

@@ -15,7 +15,7 @@ import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { import {
useCreateTrustCenterReferenceMutation, useCreateTrustCenterReferenceMutation,
useUpdateTrustCenterReferenceMutation useUpdateTrustCenterReferenceMutation,
} from "/hooks/graph/TrustCenterReferenceGraph"; } from "/hooks/graph/TrustCenterReferenceGraph";
const referenceSchema = z.object({ const referenceSchema = z.object({
@@ -44,7 +44,7 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
function TrustCenterReferenceDialog({ children }, ref) { function TrustCenterReferenceDialog({ children }, ref) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const dialogRef = useDialogRef(); const dialogRef = useDialogRef();
const [mode, setMode] = useState<'create' | 'edit'>('create'); const [mode, setMode] = useState<"create" | "edit">("create");
const [trustCenterId, setTrustCenterId] = useState<string>(""); const [trustCenterId, setTrustCenterId] = useState<string>("");
const [connectionId, setConnectionId] = useState<string>(""); const [connectionId, setConnectionId] = useState<string>("");
const [editReference, setEditReference] = useState<Reference | null>(null); const [editReference, setEditReference] = useState<Reference | null>(null);
@@ -61,12 +61,12 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
description: "", description: "",
websiteUrl: "", websiteUrl: "",
}, },
} },
); );
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
openCreate: (tId: string, cId: string) => { openCreate: (tId: string, cId: string) => {
setMode('create'); setMode("create");
setTrustCenterId(tId); setTrustCenterId(tId);
setConnectionId(cId); setConnectionId(cId);
setEditReference(null); setEditReference(null);
@@ -79,7 +79,7 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
dialogRef.current?.open(); dialogRef.current?.open();
}, },
openEdit: (reference: Reference) => { openEdit: (reference: Reference) => {
setMode('edit'); setMode("edit");
setEditReference(reference); setEditReference(reference);
setUploadedFile(null); setUploadedFile(null);
reset({ reset({
@@ -99,8 +99,8 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
} }
}; };
const onSubmit = handleSubmit(async (data: ReferenceFormData) => { const onSubmit = async (data: ReferenceFormData) => {
if (mode === 'create') { if (mode === "create") {
if (!uploadedFile) { if (!uploadedFile) {
return; return;
} }
@@ -161,7 +161,7 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
}, },
}); });
} }
}); };
const handleClose = () => { const handleClose = () => {
reset(); reset();
@@ -169,12 +169,12 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
}; };
const isSubmitting = isCreating || isUpdating; const isSubmitting = isCreating || isUpdating;
const title = mode === 'create' ? __("Add Reference") : __("Edit Reference"); const title = mode === "create" ? __("Add Reference") : __("Edit Reference");
return ( return (
<> <>
{children && ( {children && (
<span onClick={() => mode === 'create' && dialogRef.current?.open()}> <span onClick={() => mode === "create" && dialogRef.current?.open()}>
{children} {children}
</span> </span>
)} )}
@@ -185,7 +185,7 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
className="max-w-2xl" className="max-w-2xl"
onClose={handleClose} onClose={handleClose}
> >
<form onSubmit={onSubmit}> <form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent padded className="space-y-6"> <DialogContent padded className="space-y-6">
<Field <Field
{...register("name")} {...register("name")}
@@ -213,7 +213,7 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
placeholder={__("https://example.com")} placeholder={__("https://example.com")}
/> />
{mode === 'edit' && ( {mode === "edit" && (
<Field <Field
{...register("rank", { valueAsNumber: true })} {...register("rank", { valueAsNumber: true })}
label={__("Rank")} label={__("Rank")}
@@ -239,18 +239,21 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
/> />
{uploadedFile && ( {uploadedFile && (
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg"> <div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
<p className="text-sm font-medium">{__("Selected file")}:</p> <p className="text-sm font-medium">
{__("Selected file")}
:
</p>
<p className="text-sm text-txt-secondary">{uploadedFile.name}</p> <p className="text-sm text-txt-secondary">{uploadedFile.name}</p>
</div> </div>
)} )}
{mode === 'edit' && !uploadedFile && ( {mode === "edit" && !uploadedFile && (
<div className="mt-2 p-3 bg-tertiary-subtle rounded-lg"> <div className="mt-2 p-3 bg-tertiary-subtle rounded-lg">
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__("Current logo will be kept if no new file is uploaded")} {__("Current logo will be kept if no new file is uploaded")}
</p> </p>
</div> </div>
)} )}
{mode === 'create' && !uploadedFile && ( {mode === "create" && !uploadedFile && (
<div className="mt-2 p-3 bg-warning-subtle rounded-lg"> <div className="mt-2 p-3 bg-warning-subtle rounded-lg">
<p className="text-sm"> <p className="text-sm">
{__("Logo is required for new references")} {__("Logo is required for new references")}
@@ -263,15 +266,15 @@ export const TrustCenterReferenceDialog = forwardRef<TrustCenterReferenceDialogR
<DialogFooter> <DialogFooter>
<Button <Button
type="submit" type="submit"
disabled={isSubmitting || (mode === 'create' && !uploadedFile)} disabled={isSubmitting || (mode === "create" && !uploadedFile)}
icon={isSubmitting ? Spinner : undefined} icon={isSubmitting ? Spinner : undefined}
> >
{mode === 'create' ? __("Add Reference") : __("Update Reference")} {mode === "create" ? __("Add Reference") : __("Update Reference")}
</Button> </Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Dialog> </Dialog>
</> </>
); );
} },
); );

View File

@@ -49,8 +49,8 @@ export function TrustCenterReferencesSection({
const [draggedIndex, setDraggedIndex] = useState<number | null>(null); const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null); const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
const [refetchKey, setRefetchKey] = useState(0); const [refetchKey, setRefetchKey] = useState(0);
const { node: trustCenterNode } = const { node: trustCenterNode }
useLazyLoadQuery<TrustCenterReferenceGraphQuery>( = useLazyLoadQuery<TrustCenterReferenceGraphQuery>(
trustCenterReferencesQuery, trustCenterReferencesQuery,
{ trustCenterId: trustCenterId || "" }, { trustCenterId: trustCenterId || "" },
{ fetchPolicy: "network-only", fetchKey: refetchKey }, { fetchPolicy: "network-only", fetchKey: refetchKey },
@@ -60,8 +60,8 @@ export function TrustCenterReferencesSection({
} }
const [updateRank] = useUpdateTrustCenterReferenceRankMutation(); const [updateRank] = useUpdateTrustCenterReferenceRankMutation();
const references = const references
trustCenterNode?.references?.edges?.map((edge) => edge.node) || []; = trustCenterNode?.references?.edges?.map(edge => edge.node) || [];
const referencesConnectionId = trustCenterNode?.references?.__id || ""; const referencesConnectionId = trustCenterNode?.references?.__id || "";
const handleCreate = () => { const handleCreate = () => {
@@ -89,7 +89,7 @@ export function TrustCenterReferencesSection({
} }
}; };
const handleDrop = (targetIndex: number) => { const handleDrop = async (targetIndex: number) => {
if (draggedIndex === null || draggedIndex === targetIndex) { if (draggedIndex === null || draggedIndex === targetIndex) {
setDraggedIndex(null); setDraggedIndex(null);
setDragOverIndex(null); setDragOverIndex(null);
@@ -99,7 +99,7 @@ export function TrustCenterReferencesSection({
const draggedRef = references[draggedIndex]; const draggedRef = references[draggedIndex];
const targetRank = references[targetIndex].rank; const targetRank = references[targetIndex].rank;
updateRank({ await updateRank({
variables: { variables: {
input: { input: {
id: draggedRef.id, id: draggedRef.id,
@@ -107,7 +107,7 @@ export function TrustCenterReferencesSection({
}, },
}, },
onCompleted: () => { onCompleted: () => {
setRefetchKey((prev) => prev + 1); setRefetchKey(prev => prev + 1);
}, },
}); });
@@ -158,8 +158,8 @@ export function TrustCenterReferencesSection({
connectionId={referencesConnectionId} connectionId={referencesConnectionId}
onVisitWebsite={() => handleVisitWebsite(reference.websiteUrl)} onVisitWebsite={() => handleVisitWebsite(reference.websiteUrl)}
onDragStart={() => handleDragStart(index)} onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)} onDragOver={e => handleDragOver(e, index)}
onDrop={() => handleDrop(index)} onDrop={() => void handleDrop(index)}
/> />
))} ))}
</Tbody> </Tbody>

View File

@@ -38,7 +38,7 @@ type Mutation<Params> = (p: {
showOnTrustCenter: boolean; showOnTrustCenter: boolean;
} & Params; } & Params;
}; };
}) => void; }) => Promise<void>;
type Props<Params> = { type Props<Params> = {
vendors: TrustCenterVendorsCardFragment$key[]; vendors: TrustCenterVendorsCardFragment$key[];
@@ -55,8 +55,8 @@ export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
}, [props.vendors, limit]); }, [props.vendors, limit]);
const showMoreButton = limit !== null && props.vendors.length > limit; const showMoreButton = limit !== null && props.vendors.length > limit;
const onToggleVisibility = (vendorId: string, showOnTrustCenter: boolean) => { const onToggleVisibility = async (vendorId: string, showOnTrustCenter: boolean) => {
props.onToggleVisibility({ await props.onToggleVisibility({
variables: { variables: {
input: { input: {
id: vendorId, id: vendorId,
@@ -112,7 +112,7 @@ export function TrustCenterVendorsCard<Params>(props: Props<Params>) {
function VendorRow(props: { function VendorRow(props: {
vendor: TrustCenterVendorsCardFragment$key; vendor: TrustCenterVendorsCardFragment$key;
onToggleVisibility: (vendorId: string, showOnTrustCenter: boolean) => void; onToggleVisibility: (vendorId: string, showOnTrustCenter: boolean) => Promise<void>;
disabled?: boolean; disabled?: boolean;
}) { }) {
const vendor = useFragment(trustCenterVendorFragment, props.vendor); const vendor = useFragment(trustCenterVendorFragment, props.vendor);
@@ -137,8 +137,7 @@ function VendorRow(props: {
<Button <Button
variant="secondary" variant="secondary"
onClick={() => onClick={() =>
props.onToggleVisibility(vendor.id, !vendor.showOnTrustCenter) void props.onToggleVisibility(vendor.id, !vendor.showOnTrustCenter)}
}
icon={vendor.showOnTrustCenter ? IconCrossLargeX : IconCheckmark1} icon={vendor.showOnTrustCenter ? IconCrossLargeX : IconCheckmark1}
disabled={props.disabled} disabled={props.disabled}
> >

View File

@@ -150,7 +150,7 @@ export const useDeleteAsset = (
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete "%s". This action cannot be undone.', "This will permanently delete \"%s\". This action cannot be undone.",
), ),
asset.name, asset.name,
), ),

View File

@@ -135,7 +135,7 @@ export const useDeleteAudit = (
await mutate({ await mutate({
variables: { variables: {
input: { input: {
auditId: audit.id!, auditId: audit.id,
}, },
connections: [connectionId], connections: [connectionId],
}, },

View File

@@ -5,8 +5,8 @@ import { useTranslate } from "@probo/i18n";
import { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation, sprintf } from "@probo/helpers";
import { useMutationWithToasts } from "../useMutationWithToasts"; import { useMutationWithToasts } from "../useMutationWithToasts";
export const ContinualImprovementsConnectionKey = export const ContinualImprovementsConnectionKey
"ContinualImprovementsPage_continualImprovements"; = "ContinualImprovementsPage_continualImprovements";
export const continualImprovementsQuery = graphql` export const continualImprovementsQuery = graphql`
query ContinualImprovementGraphListQuery( query ContinualImprovementGraphListQuery(

View File

@@ -144,7 +144,7 @@ export const useDeleteDatum = (
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete "%s". This action cannot be undone.', "This will permanently delete \"%s\". This action cannot be undone.",
), ),
datum.name, datum.name,
), ),

View File

@@ -59,7 +59,7 @@ export const useDeleteFrameworkMutation = (
return commitDelete({ return commitDelete({
variables: { variables: {
input: { input: {
frameworkId: framework.id!, frameworkId: framework.id,
}, },
connections: [connectionId], connections: [connectionId],
}, },
@@ -69,7 +69,7 @@ export const useDeleteFrameworkMutation = (
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete framework "%s". This action cannot be undone.', "This will permanently delete framework \"%s\". This action cannot be undone.",
), ),
framework.name, framework.name,
), ),

View File

@@ -5,8 +5,8 @@ import { useTranslate } from "@probo/i18n";
import { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation, sprintf } from "@probo/helpers";
import { useMutationWithToasts } from "../useMutationWithToasts"; import { useMutationWithToasts } from "../useMutationWithToasts";
export const NonconformitiesConnectionKey = export const NonconformitiesConnectionKey
"NonconformitiesPage_nonconformities"; = "NonconformitiesPage_nonconformities";
export const nonconformitiesQuery = graphql` export const nonconformitiesQuery = graphql`
query NonconformityGraphListQuery($organizationId: ID!, $snapshotId: ID) { query NonconformityGraphListQuery($organizationId: ID!, $snapshotId: ID) {

View File

@@ -58,7 +58,7 @@ export function usePeople(
{ fetchPolicy: "network-only" }, { fetchPolicy: "network-only" },
); );
return useMemo(() => { return useMemo(() => {
return data.organization?.peoples?.edges.map((edge) => edge.node) ?? []; return data.organization?.peoples?.edges.map(edge => edge.node) ?? [];
}, [data]); }, [data]);
} }
@@ -123,7 +123,7 @@ export function usePeopleQuery(
paginatedPeopleFragment, paginatedPeopleFragment,
data.organization as PeopleGraphPaginatedFragment$key, data.organization as PeopleGraphPaginatedFragment$key,
); );
const people = pagination.data.peoples?.edges.map((edge) => edge.node); const people = pagination.data.peoples?.edges.map(edge => edge.node);
return { return {
...pagination, ...pagination,
people, people,
@@ -179,7 +179,7 @@ export const useDeletePeople = (
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete "%s". This action cannot be undone.', "This will permanently delete \"%s\". This action cannot be undone.",
), ),
people.fullName, people.fullName,
), ),

View File

@@ -5,8 +5,8 @@ import { useTranslate } from "@probo/i18n";
import { promisifyMutation, sprintf } from "@probo/helpers"; import { promisifyMutation, sprintf } from "@probo/helpers";
import { useMutationWithToasts } from "../useMutationWithToasts"; import { useMutationWithToasts } from "../useMutationWithToasts";
export const ProcessingActivitiesConnectionKey = export const ProcessingActivitiesConnectionKey
"ProcessingActivitiesPage_processingActivities"; = "ProcessingActivitiesPage_processingActivities";
export type ProcessingActivityDPIAResidualRisk = "LOW" | "MEDIUM" | "HIGH"; export type ProcessingActivityDPIAResidualRisk = "LOW" | "MEDIUM" | "HIGH";
export const processingActivitiesQuery = graphql` export const processingActivitiesQuery = graphql`

View File

@@ -97,7 +97,7 @@ export function useRisksQuery(queryRef: PreloadedQuery<RiskGraphListQuery>) {
risksFragment, risksFragment,
data.organization as RiskGraphFragment$key, data.organization as RiskGraphFragment$key,
); );
const risks = pagination.data?.risks?.edges.map((edge) => edge.node); const risks = pagination.data?.risks?.edges.map(edge => edge.node);
return { return {
...pagination, ...pagination,

View File

@@ -81,7 +81,7 @@ export function useStateOfApplicabilityQuery(
paginatedStateOfApplicabilityFragment, paginatedStateOfApplicabilityFragment,
data.organization as StateOfApplicabilityGraphPaginatedFragment$key, data.organization as StateOfApplicabilityGraphPaginatedFragment$key,
); );
const statesOfApplicability = pagination.data.statesOfApplicability?.edges.map((edge) => edge.node); const statesOfApplicability = pagination.data.statesOfApplicability?.edges.map(edge => edge.node);
return { return {
...pagination, ...pagination,
statesOfApplicability, statesOfApplicability,
@@ -142,7 +142,7 @@ export const useDeleteStateOfApplicability = (
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete "%s". This action cannot be undone.', "This will permanently delete \"%s\". This action cannot be undone.",
), ),
stateOfApplicability.name, stateOfApplicability.name,
), ),

View File

@@ -1,4 +1,4 @@
import { graphql } from 'react-relay'; import { graphql } from "react-relay";
export const trustCenterByIdQuery = graphql` export const trustCenterByIdQuery = graphql`
query TrustCenterAccessTokenGraphQuery($trustCenterId: ID!) { query TrustCenterAccessTokenGraphQuery($trustCenterId: ID!) {

View File

@@ -28,7 +28,7 @@ export function useCreateTrustCenterFileMutation() {
{ {
successMessage: "File uploaded successfully", successMessage: "File uploaded successfully",
errorMessage: "Failed to upload file", errorMessage: "Failed to upload file",
} },
); );
} }
@@ -52,7 +52,7 @@ export function useUpdateTrustCenterFileMutation() {
{ {
successMessage: "File updated successfully", successMessage: "File updated successfully",
errorMessage: "Failed to update file", errorMessage: "Failed to update file",
} },
); );
} }
@@ -73,6 +73,6 @@ export function useDeleteTrustCenterFileMutation() {
{ {
successMessage: "File deleted successfully", successMessage: "File deleted successfully",
errorMessage: "Failed to delete file", errorMessage: "Failed to delete file",
} },
); );
} }

View File

@@ -79,7 +79,7 @@ export const useDeleteVendor = (
{ {
message: sprintf( message: sprintf(
__( __(
'This will permanently delete vendor "%s". This action cannot be undone.', "This will permanently delete vendor \"%s\". This action cannot be undone.",
), ),
vendor.name, vendor.name,
), ),
@@ -218,6 +218,6 @@ export function useVendors(organizationId: string) {
{ fetchPolicy: "network-only" }, { fetchPolicy: "network-only" },
); );
return useMemo(() => { return useMemo(() => {
return data.organization?.vendors?.edges.map((edge) => edge.node) ?? []; return data.organization?.vendors?.edges.map(edge => edge.node) ?? [];
}, [data]); }, [data]);
} }

View File

@@ -5,11 +5,11 @@ import { useQuery, type UseQueryOptions } from "@tanstack/react-query";
*/ */
export function useFetchQuery<T>( export function useFetchQuery<T>(
url: string, url: string,
options?: Omit<UseQueryOptions<T>, "queryKey" | "queryFn"> options?: Omit<UseQueryOptions<T>, "queryKey" | "queryFn">,
) { ) {
return useQuery<T>({ return useQuery<T>({
...options, ...options,
queryKey: [url], queryKey: [url],
queryFn: () => fetch(url).then((res) => res.json()), queryFn: () => fetch(url).then(res => res.json()),
}); });
} }

View File

@@ -15,7 +15,7 @@ export function useMutateField<Input extends Record<string, unknown>>(
const [mutate, isUpdating] = useMutation(mutation); const [mutate, isUpdating] = useMutation(mutation);
return { return {
update<T extends keyof Input>(id: string, fieldName: T, value: Input[T]) { update: <T extends keyof Input>(id: string, fieldName: T, value: Input[T]) => {
if (!id) { if (!id) {
return; return;
} }

View File

@@ -13,7 +13,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
baseOptions?: { baseOptions?: {
successMessage?: string | ((response: T["response"]) => string); successMessage?: string | ((response: T["response"]) => string);
errorMessage?: string; errorMessage?: string;
} },
) { ) {
const [mutate, isLoading] = useMutation<T>(query); const [mutate, isLoading] = useMutation<T>(query);
const { toast } = useToast(); const { toast } = useToast();
@@ -24,7 +24,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
onSuccess?: () => void; onSuccess?: () => void;
successMessage?: string | ((response: T["response"]) => string); successMessage?: string | ((response: T["response"]) => string);
errorMessage?: string; errorMessage?: string;
} },
) => { ) => {
const options = { ...baseOptions, ...queryOptions }; const options = { ...baseOptions, ...queryOptions };
return new Promise<void>((resolve, reject) => return new Promise<void>((resolve, reject) =>
@@ -39,7 +39,7 @@ export function useMutationWithToasts<T extends MutationParameters>(
description: formatError(errorTitle, error as GraphQLError[]), description: formatError(errorTitle, error as GraphQLError[]),
variant: "error", variant: "error",
}); });
reject(error); reject(new Error(errorTitle));
return; return;
} }
const successMessage = typeof options.successMessage === "function" const successMessage = typeof options.successMessage === "function"
@@ -49,8 +49,8 @@ export function useMutationWithToasts<T extends MutationParameters>(
toast({ toast({
title: __("Success"), title: __("Success"),
description: description:
successMessage ?? successMessage
__("Operation completed successfully"), ?? __("Operation completed successfully"),
variant: "success", variant: "success",
}); });
options.onSuccess?.(); options.onSuccess?.();
@@ -65,10 +65,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

@@ -9,13 +9,13 @@ export function useStateWithSchema<T extends ZodTypeAny>(
const [value, errors] = useMemo((): [z.infer<T>, Record<string, string>] => { const [value, errors] = useMemo((): [z.infer<T>, Record<string, string>] => {
try { try {
schema.parse(state); schema.parse(state);
return [schema.parse(state), {}]; return [schema.parse(state) as z.TypeOf<T>, {}];
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
return [ return [
state, state,
Object.fromEntries( Object.fromEntries(
error.issues.map((issue) => [ error.issues.map(issue => [
issue.path.join("."), issue.path.join("."),
issue.message, issue.message,
]) ?? [], ]) ?? [],
@@ -32,7 +32,7 @@ export function useStateWithSchema<T extends ZodTypeAny>(
errors, errors,
update: useCallback( update: useCallback(
<TKey extends keyof z.infer<T>>(key: TKey, value: z.infer<T>[TKey]) => { <TKey extends keyof z.infer<T>>(key: TKey, value: z.infer<T>[TKey]) => {
setState((prevState) => ({ ...prevState, [key]: value })); setState(prevState => ({ ...prevState, [key]: value }));
}, },
[], [],
), ),

View File

@@ -7,7 +7,7 @@ export function useVendorSearch() {
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const [vendors, setVendors] = useState<Vendor[]>([]); const [vendors, setVendors] = useState<Vendor[]>([]);
useEffect(() => { useEffect(() => {
import("@probo/vendors").then((module) => { void import("@probo/vendors").then((module) => {
const ms = new MiniSearch({ const ms = new MiniSearch({
fields: ["name"], fields: ["name"],
storeFields: Object.keys(module.default[0]), storeFields: Object.keys(module.default[0]),
@@ -16,7 +16,7 @@ export function useVendorSearch() {
prefix: true, prefix: true,
}, },
}); });
ms.addAll(module.default.map((v) => ({ ...v, id: v.name }))); ms.addAll(module.default.map(v => ({ ...v, id: v.name })));
// @ts-expect-error not enough types to handle this case // @ts-expect-error not enough types to handle this case
searchRef.current = ms.search.bind(ms); searchRef.current = ms.search.bind(ms);
}); });

View File

@@ -49,13 +49,15 @@ export function PublicTrustCenterLayout({
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16"> <div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
{organizationLogo ? ( {organizationLogo
? (
<img <img
src={organizationLogo} src={organizationLogo}
alt={organizationName} alt={organizationName}
className="h-8 w-8 rounded" className="h-8 w-8 rounded"
/> />
) : ( )
: (
<Logo className="h-8 w-8" /> <Logo className="h-8 w-8" />
)} )}
<div> <div>
@@ -85,7 +87,7 @@ export function PublicTrustCenterLayout({
<Button <Button
variant="tertiary" variant="tertiary"
icon={IconArrowBoxLeft} icon={IconArrowBoxLeft}
onClick={handleLogout} onClick={() => void handleLogout()}
title={__("Logout")} title={__("Logout")}
className="text-sm" className="text-sm"
/> />

View File

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

View File

@@ -34,8 +34,8 @@ export default function DocumentSigningRequestsPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [signing, setSigning] = useState(false); const [signing, setSigning] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [signingData, setSigningData] = const [signingData, setSigningData]
useState<DocumentSigningResponse | null>(null); = useState<DocumentSigningResponse | null>(null);
const [currentDocIndex, setCurrentDocIndex] = useState(0); const [currentDocIndex, setCurrentDocIndex] = useState(0);
const [showAllDocuments, setShowAllDocuments] = useState(false); const [showAllDocuments, setShowAllDocuments] = useState(false);
@@ -53,7 +53,7 @@ export default function DocumentSigningRequestsPage() {
useEffect(() => { useEffect(() => {
if (!token) { if (!token) {
setError( setError(
__("Missing signing token. Please check your URL and try again.") __("Missing signing token. Please check your URL and try again."),
); );
setLoading(false); setLoading(false);
return; return;
@@ -66,26 +66,26 @@ export default function DocumentSigningRequestsPage() {
{ {
method: "GET", method: "GET",
headers: { headers: {
Authorization: `Bearer ${token}`, "Authorization": `Bearer ${token}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
if (!response.ok) { if (!response.ok) {
throw new Error(__("Failed to fetch signing documents")); throw new Error(__("Failed to fetch signing documents"));
} }
const documents: Document[] = await response.json(); const documents = (await response.json()) as Document[];
const enhancedDocuments = documents.map((doc) => ({ const enhancedDocuments = documents.map(doc => ({
...doc, ...doc,
signed: false, signed: false,
})); }));
// Extract organization name from the first document // Extract organization name from the first document
const organizationName = const organizationName
documents.length > 0 = documents.length > 0
? documents[0].organization_name || "Organization" ? documents[0].organization_name || "Organization"
: "Organization"; : "Organization";
@@ -95,14 +95,14 @@ export default function DocumentSigningRequestsPage() {
}); });
} catch (err) { } catch (err) {
setError( setError(
err instanceof Error ? err.message : __("An unknown error occurred") err instanceof Error ? err.message : __("An unknown error occurred"),
); );
} finally { } finally {
setLoading(false); setLoading(false);
} }
} }
fetchDocuments(); void fetchDocuments();
}, [token, __]); }, [token, __]);
const handleSignDocument = async () => { const handleSignDocument = async () => {
@@ -117,10 +117,10 @@ export default function DocumentSigningRequestsPage() {
{ {
method: "POST", method: "POST",
headers: { headers: {
Authorization: `Bearer ${token}`, "Authorization": `Bearer ${token}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
} },
); );
if (!response.ok) { if (!response.ok) {
@@ -146,7 +146,7 @@ export default function DocumentSigningRequestsPage() {
} }
} catch (err) { } catch (err) {
setError( setError(
err instanceof Error ? err.message : __("Failed to sign document") err instanceof Error ? err.message : __("Failed to sign document"),
); );
} finally { } finally {
setSigning(false); setSigning(false);
@@ -161,7 +161,7 @@ export default function DocumentSigningRequestsPage() {
const getSignedCount = () => { const getSignedCount = () => {
if (!signingData) return 0; if (!signingData) return 0;
return signingData.documents.filter((doc) => doc.signed).length; return signingData.documents.filter(doc => doc.signed).length;
}; };
if (loading) { if (loading) {
@@ -217,7 +217,7 @@ export default function DocumentSigningRequestsPage() {
</h1> </h1>
<p className="text-txt-tertiary"> <p className="text-txt-tertiary">
{__( {__(
"There are no documents requiring your signature at this time." "There are no documents requiring your signature at this time.",
)} )}
</p> </p>
</Card> </Card>
@@ -247,16 +247,16 @@ export default function DocumentSigningRequestsPage() {
<h1 className="text-2xl font-semibold mb-6"> <h1 className="text-2xl font-semibold mb-6">
{sprintf( {sprintf(
__("%s requests your signature"), __("%s requests your signature"),
signingData.organizationName signingData.organizationName,
)} )}
</h1> </h1>
{allSigned ? ( {allSigned
? (
<p className="text-txt-secondary text-base"> <p className="text-txt-secondary text-base">
{__( {__("You have successfully signed all documents. You can now close this page.")}
"You have successfully signed all documents. You can now close this page."
)}
</p> </p>
) : ( )
: (
<> <>
<p className="text-txt-secondary text-base mb-4"> <p className="text-txt-secondary text-base mb-4">
{__("Please review and sign the following documents:")} {__("Please review and sign the following documents:")}
@@ -266,7 +266,7 @@ export default function DocumentSigningRequestsPage() {
{(() => { {(() => {
const renderDocumentItem = ( const renderDocumentItem = (
doc: Document, doc: Document,
index: number index: number,
) => ( ) => (
<div <div
key={doc.document_version_id} key={doc.document_version_id}
@@ -274,16 +274,18 @@ export default function DocumentSigningRequestsPage() {
"flex items-center gap-3 py-3 px-4 transition-colors", "flex items-center gap-3 py-3 px-4 transition-colors",
index === currentDocIndex index === currentDocIndex
? "bg-blue-50 border-l-4 border-blue-500" ? "bg-blue-50 border-l-4 border-blue-500"
: "bg-transparent hover:bg-level-1" : "bg-transparent hover:bg-level-1",
)} )}
> >
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-level-2 flex-shrink-0"> <div className="flex items-center justify-center w-8 h-8 rounded-full bg-level-2 flex-shrink-0">
{doc.signed ? ( {doc.signed
? (
<IconCircleCheck <IconCircleCheck
size={20} size={20}
className="text-txt-success" className="text-txt-success"
/> />
) : ( )
: (
<span className="text-sm font-semibold text-txt-tertiary"> <span className="text-sm font-semibold text-txt-tertiary">
{index + 1} {index + 1}
</span> </span>
@@ -295,7 +297,7 @@ export default function DocumentSigningRequestsPage() {
"text-sm font-medium truncate", "text-sm font-medium truncate",
doc.signed doc.signed
? "text-txt-tertiary" ? "text-txt-tertiary"
: "text-txt-primary" : "text-txt-primary",
)} )}
> >
{doc.title} {doc.title}
@@ -309,7 +311,7 @@ export default function DocumentSigningRequestsPage() {
? "bg-green-100 text-green-800" ? "bg-green-100 text-green-800"
: index === currentDocIndex : index === currentDocIndex
? "bg-blue-100 text-blue-800" ? "bg-blue-100 text-blue-800"
: "bg-gray-100 text-gray-700" : "bg-gray-100 text-gray-700",
)} )}
> >
{doc.signed {doc.signed
@@ -326,7 +328,7 @@ export default function DocumentSigningRequestsPage() {
if (totalDocs <= 4) { if (totalDocs <= 4) {
return signingData.documents.map((doc, index) => return signingData.documents.map((doc, index) =>
renderDocumentItem(doc, index) renderDocumentItem(doc, index),
); );
} }
@@ -334,7 +336,7 @@ export default function DocumentSigningRequestsPage() {
return ( return (
<> <>
{signingData.documents.map((doc, index) => {signingData.documents.map((doc, index) =>
renderDocumentItem(doc, index) renderDocumentItem(doc, index),
)} )}
<button <button
onClick={() => setShowAllDocuments(false)} onClick={() => setShowAllDocuments(false)}
@@ -374,14 +376,14 @@ export default function DocumentSigningRequestsPage() {
<span className="text-txt-tertiary">•••</span> <span className="text-txt-tertiary">•••</span>
{sprintf( {sprintf(
__("Show %s more documents"), __("Show %s more documents"),
hiddenBeforeCurrent hiddenBeforeCurrent,
)} )}
</button> </button>
)} )}
{/* Current document (if not first) */} {/* Current document (if not first) */}
{!currentIsFirst && {!currentIsFirst
renderDocumentItem(currentDoc, currentDocIndex)} && renderDocumentItem(currentDoc, currentDocIndex)}
{/* Show more button for documents AFTER current (upcoming documents) */} {/* Show more button for documents AFTER current (upcoming documents) */}
{hiddenAfterCurrent > 0 && ( {hiddenAfterCurrent > 0 && (
@@ -392,7 +394,7 @@ export default function DocumentSigningRequestsPage() {
<span className="text-txt-tertiary">•••</span> <span className="text-txt-tertiary">•••</span>
{sprintf( {sprintf(
__("Show %s more documents"), __("Show %s more documents"),
hiddenAfterCurrent hiddenAfterCurrent,
)} )}
</button> </button>
)} )}
@@ -416,7 +418,7 @@ export default function DocumentSigningRequestsPage() {
{!currentDoc.signed && !allSigned && ( {!currentDoc.signed && !allSigned && (
<> <>
<Button <Button
onClick={handleSignDocument} onClick={() => void handleSignDocument()}
className="h-10 w-full" className="h-10 w-full"
icon={signing ? Spinner : undefined} icon={signing ? Spinner : undefined}
disabled={signing} disabled={signing}
@@ -425,7 +427,7 @@ export default function DocumentSigningRequestsPage() {
</Button> </Button>
<p className="text-xs text-txt-tertiary mt-2"> <p className="text-xs text-txt-tertiary mt-2">
{__( {__(
"By clicking 'I acknowledge and agree', your digital signature will be recorded." "By clicking 'I acknowledge and agree', your digital signature will be recorded.",
)} )}
</p> </p>
</> </>
@@ -439,10 +441,12 @@ export default function DocumentSigningRequestsPage() {
href="https://www.getprobo.com/" href="https://www.getprobo.com/"
className={clsx( className={clsx(
"flex gap-1 text-sm font-medium text-txt-tertiary items-center w-max mx-auto", "flex gap-1 text-sm font-medium text-txt-tertiary items-center w-max mx-auto",
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

@@ -6,8 +6,8 @@ import type { APIKeysPageQuery } from "/__generated__/iam/APIKeysPageQuery.graph
import { IAMRelayProvider } from "/providers/IAMRelayProvider"; import { IAMRelayProvider } from "/providers/IAMRelayProvider";
function APIKeysPageLoaderInner() { function APIKeysPageLoaderInner() {
const [queryRef, loadQuery] = const [queryRef, loadQuery]
useQueryLoader<APIKeysPageQuery>(apiKeysPageQuery); = useQueryLoader<APIKeysPageQuery>(apiKeysPageQuery);
useEffect(() => { useEffect(() => {
loadQuery({}); loadQuery({});

View File

@@ -107,25 +107,25 @@ export function PersonalAPIKeyList(props: {
const connectionID = ConnectionHandler.getConnectionID( const connectionID = ConnectionHandler.getConnectionID(
viewer.id, viewer.id,
"PersonalAPIKeyListFragment_personalAPIKeys" "PersonalAPIKeyListFragment_personalAPIKeys",
); );
const { formState, handleSubmit, register, control, reset } = const { formState, handleSubmit, register, control, reset }
useFormWithSchema(createSchema, { = useFormWithSchema(createSchema, {
defaultValues: { defaultValues: {
name: new Date().toISOString().split("T")[0], name: new Date().toISOString().split("T")[0],
expiresIn: "1month", expiresIn: "1month",
}, },
}); });
const [createCommit, isCreating] = const [createCommit, isCreating]
useMutation<PersonalAPIKeyListCreateMutation>(createMutation); = useMutation<PersonalAPIKeyListCreateMutation>(createMutation);
const handleCreate = (data: CreateFormData) => { const handleCreate = (data: CreateFormData) => {
const expiresAt = computeExpiresAt(data.expiresIn); const expiresAt = computeExpiresAt(data.expiresIn);
const connectionID = ConnectionHandler.getConnectionID( const connectionID = ConnectionHandler.getConnectionID(
viewer.id, viewer.id,
"PersonalAPIKeyListFragment_personalAPIKeys" "PersonalAPIKeyListFragment_personalAPIKeys",
); );
createCommit({ createCommit({
@@ -170,7 +170,8 @@ export function PersonalAPIKeyList(props: {
</Button> </Button>
</div> </div>
{viewer.personalAPIKeys.edges.length === 0 ? ( {viewer.personalAPIKeys.edges.length === 0
? (
<Card padded> <Card padded>
<div className="text-center py-12"> <div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2"> <h3 className="text-lg font-medium text-gray-900 mb-2">
@@ -181,7 +182,8 @@ export function PersonalAPIKeyList(props: {
</p> </p>
</div> </div>
</Card> </Card>
) : ( )
: (
<Card padded> <Card padded>
<PersonalAPIKeysTable <PersonalAPIKeysTable
edges={viewer.personalAPIKeys.edges} edges={viewer.personalAPIKeys.edges}
@@ -196,7 +198,7 @@ export function PersonalAPIKeyList(props: {
title={<Breadcrumb items={[__("API Keys"), __("Create")]} />} title={<Breadcrumb items={[__("API Keys"), __("Create")]} />}
onClose={() => reset()} onClose={() => reset()}
> >
<form onSubmit={handleSubmit(handleCreate)}> <form onSubmit={e => void handleSubmit(handleCreate)(e)}>
<DialogContent padded className="space-y-5"> <DialogContent padded className="space-y-5">
<Field error={formState.errors.name?.message}> <Field error={formState.errors.name?.message}>
<Label>{__("Name")}</Label> <Label>{__("Name")}</Label>

View File

@@ -46,8 +46,8 @@ export function PersonalAPIKeyRow(props: {
const key = useFragment(personalAPIKeyRowFragment, fKey); const key = useFragment(personalAPIKeyRowFragment, fKey);
const expired = new Date(key.expiresAt) < now; const expired = new Date(key.expiresAt) < now;
const [revokeCommit, isRevoking] = const [revokeCommit, isRevoking]
useMutation<PersonalAPIKeyRow_revokeMutation>(revokeMutation); = useMutation<PersonalAPIKeyRow_revokeMutation>(revokeMutation);
const handleRevoke = () => { const handleRevoke = () => {
confirm( confirm(
@@ -64,11 +64,11 @@ export function PersonalAPIKeyRow(props: {
title: __("Error"), title: __("Error"),
description: formatError( description: formatError(
__("Failed to revoke API key."), __("Failed to revoke API key."),
errors as GraphQLError[] errors as GraphQLError[],
), ),
variant: "error", variant: "error",
}); });
reject(errors); reject(new Error(errors[0]?.message ?? __("Failed to revoke API key.")));
return; return;
} }
toast({ toast({
@@ -83,7 +83,7 @@ export function PersonalAPIKeyRow(props: {
title: __("Error"), title: __("Error"),
description: formatError( description: formatError(
__("Failed to revoke API key."), __("Failed to revoke API key."),
error error,
), ),
variant: "error", variant: "error",
}); });
@@ -95,11 +95,11 @@ export function PersonalAPIKeyRow(props: {
{ {
title: __("Revoke API Key"), title: __("Revoke API Key"),
message: __( message: __(
`Are you sure you want to revoke the API key "${key.name}"? This action cannot be undone.` `Are you sure you want to revoke the API key "${key.name}"? This action cannot be undone.`,
), ),
label: __("Revoke"), label: __("Revoke"),
variant: "danger", variant: "danger",
} },
); );
}; };

View File

@@ -34,14 +34,14 @@ export function PersonalAPIKeyTokenAction(props: {
title: __("Error"), title: __("Error"),
description: formatError( description: formatError(
__("Failed to load API key token."), __("Failed to load API key token."),
error error,
), ),
variant: "error", variant: "error",
}); });
dialogRef.current?.close(); dialogRef.current?.close();
} }
}, },
} },
); );
}; };

View File

@@ -39,7 +39,7 @@ export default function ForgotPasswordPage() {
sendInstructionsMutation, sendInstructionsMutation,
); );
const onSubmit = handleSubmit(async ({ email }) => { const onSubmit = handleSubmit(({ email }) => {
sendInstructions({ sendInstructions({
variables: { variables: {
input: { email }, input: { email },
@@ -74,7 +74,8 @@ export default function ForgotPasswordPage() {
}); });
}); });
return instructionsSent ? ( return instructionsSent
? (
<div className="space-y-6 w-full max-w-md mx-auto"> <div className="space-y-6 w-full max-w-md mx-auto">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Check your email")}</h1> <h1 className="text-3xl font-bold">{__("Check your email")}</h1>
@@ -85,7 +86,8 @@ export default function ForgotPasswordPage() {
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Didn't receive the email?")}{" "} {__("Didn't receive the email?")}
{" "}
<button <button
onClick={() => setInstructionsSent(false)} onClick={() => setInstructionsSent(false)}
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
@@ -97,7 +99,8 @@ export default function ForgotPasswordPage() {
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Remember your password?")}{" "} {__("Remember your password?")}
{" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"
@@ -107,7 +110,8 @@ export default function ForgotPasswordPage() {
</p> </p>
</div> </div>
</div> </div>
) : ( )
: (
<div className="space-y-6 w-full max-w-md mx-auto"> <div className="space-y-6 w-full max-w-md mx-auto">
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Forgot Password")}</h1> <h1 className="text-3xl font-bold">{__("Forgot Password")}</h1>
@@ -118,7 +122,7 @@ export default function ForgotPasswordPage() {
</p> </p>
</div> </div>
<form onSubmit={onSubmit} className="space-y-4"> <form onSubmit={e => void onSubmit(e)} className="space-y-4">
<Field <Field
label={__("Email")} label={__("Email")}
type="email" type="email"
@@ -141,7 +145,8 @@ export default function ForgotPasswordPage() {
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Remember your password?")}{" "} {__("Remember your password?")}
{" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"

View File

@@ -22,7 +22,7 @@ const schema = z
password: z.string().min(8), password: z.string().min(8),
confirmPassword: z.string().min(8), confirmPassword: z.string().min(8),
}) })
.refine((data) => data.password === data.confirmPassword, { .refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match", message: "Passwords don't match",
path: ["confirmPassword"], path: ["confirmPassword"],
}); });
@@ -47,7 +47,7 @@ export default function ResetPasswordPage() {
resetPasswordMutation, resetPasswordMutation,
); );
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit((data) => {
if (!token) { if (!token) {
toast({ toast({
title: __("Reset failed"), title: __("Reset failed"),
@@ -88,7 +88,7 @@ export default function ResetPasswordPage() {
description: __("Password reset successfully"), description: __("Password reset successfully"),
variant: "success", variant: "success",
}); });
navigate("/auth/login", { replace: true }); void navigate("/auth/login", { replace: true });
}, },
}); });
}); });
@@ -102,7 +102,7 @@ export default function ResetPasswordPage() {
</p> </p>
</div> </div>
<form onSubmit={onSubmit} className="space-y-4"> <form onSubmit={e => void onSubmit(e)} className="space-y-4">
<Field <Field
label={__("New Password")} label={__("New Password")}
type="password" type="password"
@@ -130,7 +130,8 @@ export default function ResetPasswordPage() {
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Remember your password?")}{" "} {__("Remember your password?")}
{" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"

View File

@@ -37,10 +37,10 @@ export default function VerifyEmailPage() {
}, },
}); });
const [verifyEmail] = const [verifyEmail]
useMutation<VerifyEmailPageMutation>(verifyEmailMutation); = useMutation<VerifyEmailPageMutation>(verifyEmailMutation);
const handleSubmit = form.handleSubmit(async (data) => { const handleSubmit = form.handleSubmit((data) => {
verifyEmail({ verifyEmail({
variables: { variables: {
input: { input: {
@@ -83,7 +83,8 @@ export default function VerifyEmailPage() {
</p> </p>
</div> </div>
{isConfirmed ? ( {isConfirmed
? (
<div className="space-y-4 text-center"> <div className="space-y-4 text-center">
<p className="text-green-600 dark:text-green-400"> <p className="text-green-600 dark:text-green-400">
{__("Your email has been confirmed successfully!")} {__("Your email has been confirmed successfully!")}
@@ -92,8 +93,9 @@ export default function VerifyEmailPage() {
{__("Proceed to Login")} {__("Proceed to Login")}
</Button> </Button>
</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

@@ -22,14 +22,14 @@ export default function PasswordSignInPage() {
const { toast } = useToast(); const { toast } = useToast();
const [signIn, isSigningIn] = const [signIn, isSigningIn]
useMutation<PasswordSignInPageMutation>(signInMutation); = useMutation<PasswordSignInPageMutation>(signInMutation);
const handlePasswordLogin: FormEventHandler<HTMLFormElement> = (e) => { const handlePasswordLogin: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
const emailValue = formData.get("email")?.toString(); const emailValue = formData.get("email") ? (formData.get("email") as string).toString() : "";
const passwordValue = formData.get("password")?.toString(); const passwordValue = formData.get("password") ? (formData.get("password") as string).toString() : "";
if (!emailValue || !passwordValue) return; if (!emailValue || !passwordValue) return;
@@ -104,14 +104,16 @@ export default function PasswordSignInPage() {
</Button> </Button>
<div className="text-center mt-6 text-sm text-txt-secondary"> <div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "} {__("Don't have an account ?")}
{" "}
<Link to="/auth/register" className="underline hover:text-txt-primary"> <Link to="/auth/register" className="underline hover:text-txt-primary">
{__("Register")} {__("Register")}
</Link> </Link>
</div> </div>
<div className="text-center text-sm text-txt-secondary"> <div className="text-center text-sm text-txt-secondary">
{__("Forgot password?")}{" "} {__("Forgot password?")}
{" "}
<Link <Link
to="/auth/forgot-password" to="/auth/forgot-password"
className="underline hover:text-txt-primary" className="underline hover:text-txt-primary"

View File

@@ -19,15 +19,15 @@ const ssoAvailabilityQuery = graphql`
export default function SSOSignInPage() { export default function SSOSignInPage() {
const { __ } = useTranslate(); const { __ } = useTranslate();
const [queryRef, loadQuery] = const [queryRef, loadQuery]
useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery); = useQueryLoader<SSOSignInPageQuery>(ssoAvailabilityQuery);
const [checking, setChecking] = useState(false); const [checking, setChecking] = useState(false);
const handleSSOCheck: FormEventHandler<HTMLFormElement> = (e) => { const handleSSOCheck: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault(); e.preventDefault();
setChecking(true); setChecking(true);
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
const email = formData.get("email")?.toString(); const email = formData.get("email") ? (formData.get("email") as string).toString() : "";
if (!email) return; if (!email) return;
@@ -66,7 +66,8 @@ export default function SSOSignInPage() {
</Button> </Button>
<div className="text-center mt-6 text-sm text-txt-secondary"> <div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "} {__("Don't have an account ?")}
{" "}
<Link <Link
to="/auth/register" to="/auth/register"
className="underline hover:text-txt-primary" className="underline hover:text-txt-primary"
@@ -112,7 +113,7 @@ function NavigateToSSOLoginURL(props: {
variant: "error", variant: "error",
}); });
navigate("/auth/login"); void navigate("/auth/login");
return; return;
} }

View File

@@ -37,14 +37,16 @@ export default function SignInPage() {
</Button> </Button>
<div className="text-center mt-6 text-sm text-txt-secondary"> <div className="text-center mt-6 text-sm text-txt-secondary">
{__("Don't have an account ?")}{" "} {__("Don't have an account ?")}
{" "}
<Link to="/auth/register" className="underline hover:text-txt-primary"> <Link to="/auth/register" className="underline hover:text-txt-primary">
{__("Register")} {__("Register")}
</Link> </Link>
</div> </div>
<div className="text-center text-sm text-txt-secondary"> <div className="text-center text-sm text-txt-secondary">
{__("Forgot password?")}{" "} {__("Forgot password?")}
{" "}
<Link <Link
to="/auth/forgot-password" to="/auth/forgot-password"
className="underline hover:text-txt-primary" className="underline hover:text-txt-primary"

View File

@@ -81,7 +81,7 @@ export default function SignUpFromInvitationPage() {
), ),
variant: "success", variant: "success",
}); });
navigate("/", { replace: true }); void navigate("/", { replace: true });
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
@@ -102,7 +102,7 @@ export default function SignUpFromInvitationPage() {
</p> </p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field <Field
label={__("Full Name")} label={__("Full Name")}
type="text" type="text"
@@ -130,7 +130,8 @@ export default function SignUpFromInvitationPage() {
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Already have an account?")}{" "} {__("Already have an account?")}
{" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"

View File

@@ -67,7 +67,7 @@ export default function SignUpPage() {
description: __("Account created successfully"), description: __("Account created successfully"),
variant: "success", variant: "success",
}); });
navigate("/", { replace: true }); void navigate("/", { replace: true });
}, },
onError: (e) => { onError: (e) => {
toast({ toast({
@@ -88,7 +88,7 @@ export default function SignUpPage() {
</p> </p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field <Field
label={__("Full Name")} label={__("Full Name")}
type="text" type="text"
@@ -125,7 +125,8 @@ export default function SignUpPage() {
<div className="text-center"> <div className="text-center">
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Already have an account?")}{" "} {__("Already have an account?")}
{" "}
<Link <Link
to="/auth/login" to="/auth/login"
className="underline text-txt-primary hover:text-txt-secondary" className="underline text-txt-primary hover:text-txt-secondary"

View File

@@ -118,11 +118,13 @@ export function MembershipsPage(props: {
onValueChange={setSearch} onValueChange={setSearch}
/> />
</div> </div>
{memberships.length === 0 ? ( {memberships.length === 0
? (
<div className="text-center text-txt-secondary py-4"> <div className="text-center text-txt-secondary py-4">
{__("No organizations found")} {__("No organizations found")}
</div> </div>
) : ( )
: (
memberships.map(({ node }) => ( memberships.map(({ node }) => (
<MembershipCard key={node.id} fKey={node} /> <MembershipCard key={node.id} fKey={node} />
)) ))

View File

@@ -6,8 +6,8 @@ import { MembershipsPage, membershipsPageQuery } from "./MembershipsPage";
import { IAMRelayProvider } from "/providers/IAMRelayProvider"; import { IAMRelayProvider } from "/providers/IAMRelayProvider";
function MembershipsPageLoader() { function MembershipsPageLoader() {
const [queryRef, loadQuery] = const [queryRef, loadQuery]
useQueryLoader<MembershipsPageQuery>(membershipsPageQuery); = useQueryLoader<MembershipsPageQuery>(membershipsPageQuery);
useEffect(() => { useEffect(() => {
loadQuery({}); loadQuery({});

View File

@@ -26,13 +26,13 @@ export function ViewerLayout(props: {
return ( return (
<Layout <Layout
headerTrailing={ headerTrailing={(
<div className="ml-auto"> <div className="ml-auto">
<Suspense fallback={<Skeleton className="w-32 h-8" />}> <Suspense fallback={<Skeleton className="w-32 h-8" />}>
<ViewerDropdown fKey={viewer} /> <ViewerDropdown fKey={viewer} />
</Suspense> </Suspense>
</div> </div>
} )}
> >
<Outlet /> <Outlet />
</Layout> </Layout>

View File

@@ -6,8 +6,8 @@ import { IAMRelayProvider } from "/providers/IAMRelayProvider";
import { ViewerLayoutLoading } from "./ViewerLayoutLoading"; import { ViewerLayoutLoading } from "./ViewerLayoutLoading";
function ViewerLayoutLoader() { function ViewerLayoutLoader() {
const [queryRef, loadQuery] = const [queryRef, loadQuery]
useQueryLoader<ViewerLayoutQuery>(viewerLayoutQuery); = useQueryLoader<ViewerLayoutQuery>(viewerLayoutQuery);
useEffect(() => { useEffect(() => {
loadQuery({}); loadQuery({});

View File

@@ -4,11 +4,11 @@ import { Outlet } from "react-router";
export function ViewerLayoutLoading() { export function ViewerLayoutLoading() {
return ( return (
<Layout <Layout
headerTrailing={ headerTrailing={(
<div className="ml-auto"> <div className="ml-auto">
<Skeleton className="w-32 h-8" /> <Skeleton className="w-32 h-8" />
</div> </div>
} )}
> >
<Outlet /> <Outlet />
</Layout> </Layout>

View File

@@ -48,16 +48,16 @@ interface InvitationCardProps {
} }
export function InvitationCard(props: InvitationCardProps) { export function InvitationCard(props: InvitationCardProps) {
const { pendingInvitationsConnectionId, membershipConnectionId, fKey } = const { pendingInvitationsConnectionId, membershipConnectionId, fKey }
props; = props;
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const invitation = useFragment<InvitationCardFragment$key>(fragment, fKey); const invitation = useFragment<InvitationCardFragment$key>(fragment, fKey);
const [acceptInvitation, isAccepting] = const [acceptInvitation, isAccepting]
useMutation<InvitationCardMutation>(acceptMutation); = useMutation<InvitationCardMutation>(acceptMutation);
const handleAccept = () => { const handleAccept = () => {
acceptInvitation({ acceptInvitation({
@@ -96,10 +96,14 @@ export function InvitationCard(props: InvitationCardProps) {
{invitation.organization.name} {invitation.organization.name}
</h3> </h3>
<p className="text-sm text-txt-secondary"> <p className="text-sm text-txt-secondary">
{__("Role")}: <span className="font-medium">{invitation.role}</span> {__("Role")}
:
<span className="font-medium">{invitation.role}</span>
</p> </p>
<p className="text-xs text-txt-tertiary"> <p className="text-xs text-txt-tertiary">
{__("Invited on")} {formatDate(invitation.createdAt)} {__("Invited on")}
{" "}
{formatDate(invitation.createdAt)}
</p> </p>
</div> </div>
<Button onClick={handleAccept} disabled={isAccepting}> <Button onClick={handleAccept} disabled={isAccepting}>

View File

@@ -72,11 +72,11 @@ export function MembershipCard(props: MembershipCardProps) {
fKey, fKey,
); );
const isAuthenticated = !!lastSession; const isAuthenticated = !!lastSession;
const isExpired = const isExpired
lastSession && parseDate(lastSession.expiresAt) < new Date(); = lastSession && parseDate(lastSession.expiresAt) < new Date();
const [assumeOrganizationSession] = const [assumeOrganizationSession]
useMutation<MembershipCard_assumeMutation>( = useMutation<MembershipCard_assumeMutation>(
assumeOrganizationSessionMutation, assumeOrganizationSessionMutation,
); );
@@ -96,13 +96,13 @@ export function MembershipCard(props: MembershipCardProps) {
switch (result.__typename) { switch (result.__typename) {
case "PasswordRequired": case "PasswordRequired":
navigate("auth/login"); void navigate("auth/login");
break; break;
case "SAMLAuthenticationRequired": case "SAMLAuthenticationRequired":
window.location.href = result.redirectUrl; window.location.href = result.redirectUrl;
break; break;
default: default:
navigate(`/organizations/${organization.id}`); void navigate(`/organizations/${organization.id}`);
} }
}, },
}); });
@@ -148,11 +148,13 @@ export function MembershipCard(props: MembershipCardProps) {
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{isAuthenticated ? ( {isAuthenticated
? (
<Link to={`/organizations/${organization.id}`}> <Link to={`/organizations/${organization.id}`}>
<Button variant="secondary">{__("Start")}</Button> <Button variant="secondary">{__("Start")}</Button>
</Link> </Link>
) : ( )
: (
<Button onClick={handleAssumeOrganizationSession}> <Button onClick={handleAssumeOrganizationSession}>
{__("Login")} {__("Login")}
</Button> </Button>

View File

@@ -35,8 +35,8 @@ export function ViewerDropdown(props: { fKey: ViewerDropdownFragment$key }) {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { toast } = useToast(); const { toast } = useToast();
const { canListAPIKeys, email, fullName } = const { canListAPIKeys, email, fullName }
useFragment<ViewerDropdownFragment$key>(fragment, fKey); = useFragment<ViewerDropdownFragment$key>(fragment, fKey);
const [signOut] = useMutation(signOutMutation); const [signOut] = useMutation(signOutMutation);
const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = (e) => { const handleLogout: React.MouseEventHandler<HTMLAnchorElement> = (e) => {

View File

@@ -31,13 +31,13 @@ function NewOrganizationPage() {
const { toast } = useToast(); const { toast } = useToast();
const { __ } = useTranslate(); const { __ } = useTranslate();
const [createOrganization, isCreating] = const [createOrganization, isCreating]
useMutation<NewOrganizationPageMutation>(createOrganizationMutation); = useMutation<NewOrganizationPageMutation>(createOrganizationMutation);
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => { const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
const name = formData.get("name")?.toString(); const name = formData.get("name") ? (formData.get("name") as string).toString() : "";
if (!name) { if (!name) {
toast({ toast({
title: __("Error"), title: __("Error"),
@@ -47,7 +47,7 @@ function NewOrganizationPage() {
return; return;
} }
createOrganization({ void createOrganization({
variables: { variables: {
input: { input: {
name, name,
@@ -64,7 +64,7 @@ function NewOrganizationPage() {
} }
const org = r.createOrganization!.organization; const org = r.createOrganization!.organization;
navigate(`/organizations/${org!.id}`); void navigate(`/organizations/${org!.id}`);
toast({ toast({
title: __("Success"), title: __("Success"),
description: __("Organization has been created successfully"), description: __("Organization has been created successfully"),
@@ -84,7 +84,7 @@ function NewOrganizationPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Link <Link
to={location.state?.from ?? "/"} to={(location.state as { from: string })?.from ?? "/"}
className="mb-4 inline-flex gap-2 items-center" className="mb-4 inline-flex gap-2 items-center"
> >
<IconChevronLeft size={16} /> <IconChevronLeft size={16} />
@@ -97,7 +97,7 @@ function NewOrganizationPage() {
)} )}
/> />
<Card padded asChild> <Card padded asChild>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<h2 className="text-xl font-semibold mb-1"> <h2 className="text-xl font-semibold mb-1">
{__("Organization Details")} {__("Organization Details")}
</h2> </h2>

View File

@@ -47,8 +47,8 @@ export function ViewerMembershipLayout(props: {
const { __ } = useTranslate(); const { __ } = useTranslate();
const { organization, viewer } = const { organization, viewer }
usePreloadedQuery<ViewerMembershipLayoutQuery>( = usePreloadedQuery<ViewerMembershipLayoutQuery>(
viewerMembershipLayoutQuery, viewerMembershipLayoutQuery,
queryRef, queryRef,
); );
@@ -58,7 +58,7 @@ export function ViewerMembershipLayout(props: {
return ( return (
<Layout <Layout
headerLeading={ headerLeading={(
<> <>
<MembershipsDropdown <MembershipsDropdown
organizationFKey={organization} organizationFKey={organization}
@@ -77,12 +77,12 @@ export function ViewerMembershipLayout(props: {
</Link> </Link>
)} )}
</> </>
} )}
headerTrailing={ headerTrailing={(
<Suspense fallback={<Skeleton className="w-32 h-8" />}> <Suspense fallback={<Skeleton className="w-32 h-8" />}>
<ViewerMembershipDropdown fKey={organization} /> <ViewerMembershipDropdown fKey={organization} />
</Suspense> </Suspense>
} )}
sidebar={!hideSidebar && <Sidebar fKey={organization} />} sidebar={!hideSidebar && <Sidebar fKey={organization} />}
> >
<CoreRelayProvider> <CoreRelayProvider>

View File

@@ -46,8 +46,8 @@ export function MembershipsDropdown(props: {
const { __ } = useTranslate(); const { __ } = useTranslate();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const currentOrganization = const currentOrganization
useFragment<MembershipsDropdown_organizationFragment$key>( = useFragment<MembershipsDropdown_organizationFragment$key>(
organizationFragment, organizationFragment,
organizationFKey, organizationFKey,
); );
@@ -72,7 +72,7 @@ export function MembershipsDropdown(props: {
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Dropdown <Dropdown
onOpenChange={handleOpenMenu} onOpenChange={handleOpenMenu}
toggle={ toggle={(
<Button <Button
className="-ml-3" className="-ml-3"
variant="tertiary" variant="tertiary"
@@ -80,7 +80,7 @@ export function MembershipsDropdown(props: {
> >
{currentOrganization.name} {currentOrganization.name}
</Button> </Button>
} )}
> >
<div className="px-3 py-2"> <div className="px-3 py-2">
<Input <Input
@@ -97,11 +97,11 @@ export function MembershipsDropdown(props: {
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400"> <div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
{queryRef && ( {queryRef && (
<Suspense <Suspense
fallback={ fallback={(
<div className="px-3 py-2 text-gray-500"> <div className="px-3 py-2 text-gray-500">
{__("Loading organizations...")} {__("Loading organizations...")}
</div> </div>
} )}
> >
<MembershipsDropdownMenu search={search} queryRef={queryRef} /> <MembershipsDropdownMenu search={search} queryRef={queryRef} />
</Suspense> </Suspense>

View File

@@ -63,21 +63,21 @@ export function MembershipsDropdownMenuItem(props: {
const navigate = useNavigate(); const navigate = useNavigate();
const { id, lastSession, organization } = const { id, lastSession, organization }
useFragment<MembershipsDropdownMenuItemFragment$key>(fragment, fKey); = useFragment<MembershipsDropdownMenuItemFragment$key>(fragment, fKey);
const isAuthenticated = !!lastSession; const isAuthenticated = !!lastSession;
const isExpired = const isExpired
lastSession && parseDate(lastSession.expiresAt) < new Date(); = lastSession && parseDate(lastSession.expiresAt) < new Date();
const [assumeOrganizationSession] = const [assumeOrganizationSession]
useMutation<MembershipsDropdownMenuItem_assumeMutation>( = useMutation<MembershipsDropdownMenuItem_assumeMutation>(
assumeOrganizationSessionMutation, assumeOrganizationSessionMutation,
); );
const handleAssumeOrganizationSession = useCallback(() => { const handleAssumeOrganizationSession = useCallback(() => {
if (isAuthenticated) { if (isAuthenticated) {
navigate(`/organizations/${organization.id}`); void navigate(`/organizations/${organization.id}`);
} else { } else {
assumeOrganizationSession({ assumeOrganizationSession({
variables: { variables: {
@@ -94,13 +94,13 @@ export function MembershipsDropdownMenuItem(props: {
switch (result.__typename) { switch (result.__typename) {
case "PasswordRequired": case "PasswordRequired":
navigate("/auth/login"); void navigate("/auth/login");
break; break;
case "SAMLAuthenticationRequired": case "SAMLAuthenticationRequired":
window.location.href = result.redirectUrl; window.location.href = result.redirectUrl;
break; break;
default: default:
navigate(`/organizations/${organization.id}`); void navigate(`/organizations/${organization.id}`);
} }
}, },
}); });

View File

@@ -49,8 +49,8 @@ export function GeneralSettingsPage(props: {
throw new Error("Relay node is not an organization"); throw new Error("Relay node is not an organization");
} }
const [deleteOrganization, isDeletingOrganization] = const [deleteOrganization, isDeletingOrganization]
useMutationWithToasts<GeneralSettingsPage_deleteMutation>( = useMutationWithToasts<GeneralSettingsPage_deleteMutation>(
deleteOrganizationMutation, deleteOrganizationMutation,
{ {
successMessage: __("Organization deleted successfully."), successMessage: __("Organization deleted successfully."),
@@ -67,7 +67,7 @@ export function GeneralSettingsPage(props: {
connections: [], connections: [],
}, },
onSuccess: () => { onSuccess: () => {
navigate("/", { replace: true }); void navigate("/", { replace: true });
}, },
}); });
}; };
@@ -87,7 +87,8 @@ export function GeneralSettingsPage(props: {
{__("Delete Organization")} {__("Delete Organization")}
</h3> </h3>
<p className="text-sm text-txt-tertiary"> <p className="text-sm text-txt-tertiary">
{__("Permanently delete this organization and all its data.")}{" "} {__("Permanently delete this organization and all its data.")}
{" "}
<span className="text-red-600 font-medium"> <span className="text-red-600 font-medium">
{__("This action cannot be undone.")} {__("This action cannot be undone.")}
</span> </span>
@@ -95,7 +96,7 @@ export function GeneralSettingsPage(props: {
</div> </div>
<DeleteOrganizationDialog <DeleteOrganizationDialog
organizationName={organization.name} organizationName={organization.name}
onConfirm={handleDeleteOrganization} onConfirm={() => void handleDeleteOrganization()}
isDeleting={isDeletingOrganization} isDeleting={isDeletingOrganization}
> >
<Button <Button

View File

@@ -7,8 +7,8 @@ import type { MembersPageQuery } from "/__generated__/iam/MembersPageQuery.graph
function MembersPageLoader() { function MembersPageLoader() {
const organizationId = useOrganizationId(); const organizationId = useOrganizationId();
const [queryRef, loadQuery] = const [queryRef, loadQuery]
useQueryLoader<MembersPageQuery>(membersPageQuery); = useQueryLoader<MembersPageQuery>(membersPageQuery);
useEffect(() => { useEffect(() => {
loadQuery({ loadQuery({

View File

@@ -39,8 +39,8 @@ export function SAMLSettingsPage(props: {
const formDialogRef = useDialogRef(); const formDialogRef = useDialogRef();
const domainDialogRef = useDialogRef(); const domainDialogRef = useDialogRef();
const [isEditing, setIsEditing] = useState<boolean>(); const [isEditing, setIsEditing] = useState<boolean>();
const [domainVerificationToken, setDomainVerificationToken] = const [domainVerificationToken, setDomainVerificationToken]
useState<string>(); = useState<string>();
const { __ } = useTranslate(); const { __ } = useTranslate();
@@ -48,8 +48,8 @@ export function SAMLSettingsPage(props: {
if (organization.__typename !== "Organization") { if (organization.__typename !== "Organization") {
throw new Error("invalid node type"); throw new Error("invalid node type");
} }
const [formQueryRef, loadFormQuery] = const [formQueryRef, loadFormQuery]
useQueryLoader<EditSAMLConfigurationFormQuery>(samlConfigurationFormQuery); = useQueryLoader<EditSAMLConfigurationFormQuery>(samlConfigurationFormQuery);
const handleOpenFormDialog = (samlConfigurationId?: string) => { const handleOpenFormDialog = (samlConfigurationId?: string) => {
setIsEditing(!!samlConfigurationId); setIsEditing(!!samlConfigurationId);
@@ -96,7 +96,8 @@ export function SAMLSettingsPage(props: {
onClose={handleCloseFormDialog} onClose={handleCloseFormDialog}
title={<Breadcrumb items={[__("SAML Settings"), __("Configure")]} />} title={<Breadcrumb items={[__("SAML Settings"), __("Configure")]} />}
> >
{isEditing ? ( {isEditing
? (
<Suspense> <Suspense>
{formQueryRef && ( {formQueryRef && (
<EditSAMLConfigurationForm <EditSAMLConfigurationForm
@@ -105,7 +106,8 @@ export function SAMLSettingsPage(props: {
/> />
)} )}
</Suspense> </Suspense>
) : ( )
: (
<NewSAMLConfigurationForm onCreate={handleCloseFormDialog} /> <NewSAMLConfigurationForm onCreate={handleCloseFormDialog} />
)} )}
</Dialog> </Dialog>

Some files were not shown because too many files have changed in this diff Show More