Fix apps/console lint issues
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
@@ -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>(
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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!) {
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }));
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<TranslatorProvider>
|
<TranslatorProvider>
|
||||||
<App />
|
<App />
|
||||||
</TranslatorProvider>
|
</TranslatorProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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({});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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} />
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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({});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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({});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user