Add policies version history
Signed-off-by: Bryan Frimin <bryan@getprobo.com> Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
committed by
Sacha Al Himdani
parent
abe0ce1dde
commit
2e6741942b
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"relay": "npx relay-compiler",
|
||||
"relay": "find src -type d -name \"__generated\" -exec rm -rf {} + && npx relay-compiler",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"check": "tsc --noEmit -p tsconfig.app.json",
|
||||
|
||||
@@ -20,7 +20,6 @@ export function ControlledField({
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
{/* @ts-expect-error field is too dynamic */}
|
||||
<Field
|
||||
{...props}
|
||||
{...field}
|
||||
|
||||
@@ -12,6 +12,8 @@ export const policiesQuery = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const PoliciesConnectionKey = "PoliciesPageFragment_policies";
|
||||
|
||||
const deletePolicyMutation = graphql`
|
||||
mutation PolicyGraphDeleteMutation(
|
||||
$input: DeletePolicyInput!
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<88a40781a78a282ebde97d4cb73bb996>>
|
||||
* @generated SignedSource<<bfeead50dec41ac2f5fac7c6d70f47fb>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -39,12 +39,33 @@ v1 = [
|
||||
}
|
||||
],
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
},
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
},
|
||||
v5 = [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 20
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
@@ -93,14 +114,8 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
@@ -119,26 +134,14 @@ return {
|
||||
"name": "owner",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
}
|
||||
(v3/*: any*/),
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"args": (v5/*: any*/),
|
||||
"concreteType": "PolicyVersionConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "versions",
|
||||
@@ -160,7 +163,7 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -226,7 +229,7 @@ return {
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
@@ -242,15 +245,89 @@ return {
|
||||
}
|
||||
],
|
||||
"storageKey": "signatures(first:100)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "changelog",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "People",
|
||||
"kind": "LinkedField",
|
||||
"name": "publishedBy",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v4/*: any*/),
|
||||
(v3/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": "versions(first:1)"
|
||||
"storageKey": "versions(first:20)"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v5/*: any*/),
|
||||
"filters": null,
|
||||
"handle": "connection",
|
||||
"key": "PolicyPage_versions",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "versions"
|
||||
}
|
||||
],
|
||||
"type": "Policy",
|
||||
@@ -262,12 +339,12 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "061b3441def7dfcf73881e57077e1738",
|
||||
"cacheID": "e3a62b161b9fce47237c98f90a5d6d2e",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "PolicyGraphNodeQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query PolicyGraphNodeQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n ... on Policy {\n ...PolicyPagePolicyFragment\n }\n id\n }\n}\n\nfragment PolicyPagePolicyFragment on Policy {\n id\n title\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
|
||||
"text": "query PolicyGraphNodeQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n ... on Policy {\n ...PolicyPagePolicyFragment\n }\n id\n }\n}\n\nfragment PolicyPagePolicyFragment on Policy {\n id\n title\n owner {\n id\n fullName\n }\n versions(first: 20) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n ...PolicyVersionHistoryDialogFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment PolicyVersionHistoryDialogFragment on PolicyVersion {\n id\n version\n status\n content\n changelog\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -28,7 +28,8 @@ export function useMutationWithToasts<T extends MutationParameters>(
|
||||
const options = { ...baseOptions, ...queryOptions };
|
||||
mutate({
|
||||
...options,
|
||||
onCompleted: (_, error) => {
|
||||
onCompleted: (response, error) => {
|
||||
options.onCompleted?.(response, error);
|
||||
if (error) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useConfirmDialogRef,
|
||||
} from "@probo/ui";
|
||||
import {
|
||||
measuresQuery,
|
||||
@@ -156,6 +157,7 @@ export default function MeasuresPage(props: Props) {
|
||||
key={category}
|
||||
category={category}
|
||||
measures={measuresPerCategory[category]}
|
||||
connectionId={connectionId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -165,6 +167,7 @@ export default function MeasuresPage(props: Props) {
|
||||
type CategoryProps = {
|
||||
category: string;
|
||||
measures: NodeOf<MeasuresPageFragment$data["measures"]>[];
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
function Category(props: CategoryProps) {
|
||||
@@ -210,7 +213,11 @@ function Category(props: CategoryProps) {
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{measures.map((measure) => (
|
||||
<MeasureRow key={measure.id} measure={measure} />
|
||||
<MeasureRow
|
||||
key={measure.id}
|
||||
measure={measure}
|
||||
connectionId={props.connectionId}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
@@ -232,6 +239,7 @@ function Category(props: CategoryProps) {
|
||||
|
||||
type MeasureRowProps = {
|
||||
measure: NodeOf<MeasuresPageFragment$data["measures"]>;
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
function MeasureRow(props: MeasureRowProps) {
|
||||
@@ -239,42 +247,50 @@ function MeasureRow(props: MeasureRowProps) {
|
||||
const [deleteMeasure, isDeleting] = useDeleteMeasureMutation();
|
||||
|
||||
const onDelete = () => {
|
||||
deleteMeasure({
|
||||
variables: {
|
||||
input: { measureId: props.measure.id },
|
||||
connections: ["MeasuresListQuery_measures"],
|
||||
},
|
||||
return new Promise<void>((resolve) => {
|
||||
deleteMeasure({
|
||||
variables: {
|
||||
input: { measureId: props.measure.id },
|
||||
connections: [props.connectionId],
|
||||
},
|
||||
onCompleted: () => resolve(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const confirmRef = useConfirmDialogRef();
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>{props.measure.name}</Td>
|
||||
<Td width={120}>
|
||||
<MeasureBadge state={props.measure.state} />
|
||||
</Td>
|
||||
<Td width={50} className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem icon={IconPencil}>{__("Edit")}</DropdownItem>
|
||||
<ConfirmDialog
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the measure "%s". This action cannot be undone.'
|
||||
),
|
||||
props.measure.name
|
||||
)}
|
||||
onConfirm={() => onDelete()}
|
||||
>
|
||||
<>
|
||||
<ConfirmDialog
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the measure "%s". This action cannot be undone.'
|
||||
),
|
||||
props.measure.name
|
||||
)}
|
||||
onConfirm={onDelete}
|
||||
ref={confirmRef}
|
||||
/>
|
||||
<Tr>
|
||||
<Td>{props.measure.name}</Td>
|
||||
<Td width={120}>
|
||||
<MeasureBadge state={props.measure.state} />
|
||||
</Td>
|
||||
<Td width={50} className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem icon={IconPencil}>{__("Edit")}</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={() => confirmRef.current?.open()}
|
||||
disabled={isDeleting}
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ConfirmDialog>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ import {
|
||||
Td,
|
||||
Avatar,
|
||||
Badge,
|
||||
ActionDropdown,
|
||||
DropdownItem,
|
||||
IconPencil,
|
||||
ConfirmDialog,
|
||||
IconTrashCan,
|
||||
Button,
|
||||
@@ -31,7 +28,7 @@ import {
|
||||
import type { PoliciesPageListFragment$key } from "./__generated__/PoliciesPageListFragment.graphql";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import FormPolicyDialog from "./FormPolicyDialog";
|
||||
import { CreatePolicyDialog } from "./dialogs/CreatePolicyDialog";
|
||||
import type { PoliciesPageRowFragment$key } from "./__generated__/PoliciesPageRowFragment.graphql";
|
||||
|
||||
const policiesFragment = graphql`
|
||||
@@ -80,7 +77,7 @@ export default function PoliciesPage(props: Props) {
|
||||
title={__("Policies")}
|
||||
description={__("Manage your organization’s policies")}
|
||||
>
|
||||
<FormPolicyDialog
|
||||
<CreatePolicyDialog
|
||||
connection={connectionId}
|
||||
trigger={<Button icon={IconPlusLarge}>{__("New policy")}</Button>}
|
||||
/>
|
||||
@@ -160,45 +157,47 @@ function PolicyRow({
|
||||
const signedCount = signatures.filter(
|
||||
(signature) => signature.state === "SIGNED"
|
||||
).length;
|
||||
const [deletePolicy, isDeleting] = useDeletePolicyMutation();
|
||||
const [deletePolicy] = useDeletePolicyMutation();
|
||||
|
||||
const handleDelete = () => {
|
||||
deletePolicy({
|
||||
variables: {
|
||||
input: { policyId: policy.id },
|
||||
connections: [connectionId],
|
||||
},
|
||||
return new Promise<void>((resolve) => {
|
||||
deletePolicy({
|
||||
variables: {
|
||||
input: { policyId: policy.id },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted: () => resolve(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
||||
<Td>{policy.title}</Td>
|
||||
<Td>
|
||||
<Badge variant={isDraft ? "neutral" : "success"}>
|
||||
{isDraft ? __("Draft") : __("Published")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar name={policy.owner.fullName} />
|
||||
{policy.owner.fullName}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{dateFormat(policy.updatedAt, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
weekday: "short",
|
||||
})}
|
||||
</Td>
|
||||
<Td>
|
||||
{signedCount}/{signatures.length}
|
||||
</Td>
|
||||
<Td noLink width={50} className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem icon={IconPencil}>{__("Edit")}</DropdownItem>
|
||||
<>
|
||||
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
|
||||
<Td>{policy.title}</Td>
|
||||
<Td>
|
||||
<Badge variant={isDraft ? "neutral" : "success"}>
|
||||
{isDraft ? __("Draft") : __("Published")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar name={policy.owner.fullName} />
|
||||
{policy.owner.fullName}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{dateFormat(policy.updatedAt, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
weekday: "short",
|
||||
})}
|
||||
</Td>
|
||||
<Td>
|
||||
{signedCount}/{signatures.length}
|
||||
</Td>
|
||||
<Td noLink width={50} className="text-end">
|
||||
<ConfirmDialog
|
||||
message={sprintf(
|
||||
__(
|
||||
@@ -208,16 +207,10 @@ function PolicyRow({
|
||||
)}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
<DropdownItem
|
||||
disabled={isDeleting}
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
<Button icon={IconTrashCan} variant="danger" />
|
||||
</ConfirmDialog>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { PreloadedQuery } from "react-relay";
|
||||
import type { PolicyGraphNodeQuery } from "/hooks/graph/__generated__/PolicyGraphNodeQuery.graphql";
|
||||
import {
|
||||
ConnectionHandler,
|
||||
graphql,
|
||||
loadQuery,
|
||||
useFragment,
|
||||
usePreloadedQuery,
|
||||
} from "react-relay";
|
||||
import { policyNodeQuery } from "/hooks/graph/PolicyGraph";
|
||||
import {
|
||||
PoliciesConnectionKey,
|
||||
policyNodeQuery,
|
||||
useDeletePolicyMutation,
|
||||
} from "/hooks/graph/PolicyGraph";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import type { PolicyPagePolicyFragment$key } from "./__generated__/PolicyPagePolicyFragment.graphql";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
@@ -19,10 +24,22 @@ import {
|
||||
Drawer,
|
||||
Badge,
|
||||
Avatar,
|
||||
DropdownItem,
|
||||
ConfirmDialog,
|
||||
ActionDropdown,
|
||||
IconTrashCan,
|
||||
IconPencil,
|
||||
useConfirmDialogRef,
|
||||
IconClock,
|
||||
} from "@probo/ui";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { Button } from "@probo/ui";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import { sprintf } from "@probo/helpers";
|
||||
import { useNavigate } from "react-router";
|
||||
import UpdateVersionDialog from "./dialogs/UpdateVersionDialog";
|
||||
import { useRef } from "react";
|
||||
import { PolicyVersionHistoryDialog } from "./dialogs/PolicyVersionHistoryDialog";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<PolicyGraphNodeQuery>;
|
||||
@@ -36,7 +53,8 @@ const policyFragment = graphql`
|
||||
id
|
||||
fullName
|
||||
}
|
||||
versions(first: 1) {
|
||||
versions(first: 20) @connection(key: "PolicyPage_versions") {
|
||||
__id
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
@@ -53,6 +71,7 @@ const policyFragment = graphql`
|
||||
}
|
||||
}
|
||||
}
|
||||
...PolicyVersionHistoryDialogFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +96,7 @@ export default function PolicyPage(props: Props) {
|
||||
);
|
||||
const { __, dateFormat } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const navigate = useNavigate();
|
||||
const lastVersion = policy.versions.edges[0].node;
|
||||
const isDraft = lastVersion.status === "DRAFT";
|
||||
const [publishPolicyVersion, isPublishing] = useMutationWithToasts(
|
||||
@@ -86,6 +106,10 @@ export default function PolicyPage(props: Props) {
|
||||
errorMessage: __("Failed to publish policy. Please try again."),
|
||||
}
|
||||
);
|
||||
const [deletePolicy, isDeleting] = useDeletePolicyMutation();
|
||||
const versionConnectionId = policy.versions.__id;
|
||||
const versions = policy.versions.edges.map((edge) => edge.node);
|
||||
|
||||
usePageTitle(policy.title);
|
||||
|
||||
const handlePublish = () => {
|
||||
@@ -105,8 +129,46 @@ export default function PolicyPage(props: Props) {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const connectionId = ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
PoliciesConnectionKey
|
||||
);
|
||||
deletePolicy({
|
||||
variables: {
|
||||
input: { policyId: policy.id },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onSuccess() {
|
||||
navigate(`/organizations/${organizationId}/policies`);
|
||||
},
|
||||
onError: () => resolve(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateDialogRef = useRef<{ open: () => void }>(null);
|
||||
const confirmRef = useConfirmDialogRef();
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateVersionDialog
|
||||
ref={updateDialogRef}
|
||||
policy={policy}
|
||||
connectionId={versionConnectionId}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
ref={confirmRef}
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the policy "%s". This action cannot be undone.'
|
||||
),
|
||||
policy.title
|
||||
)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Breadcrumb
|
||||
@@ -130,6 +192,28 @@ export default function PolicyPage(props: Props) {
|
||||
{__("Publish")}
|
||||
</Button>
|
||||
)}
|
||||
<PolicyVersionHistoryDialog policy={policy}>
|
||||
<Button icon={IconClock} variant="secondary">
|
||||
{__("Version history")}
|
||||
</Button>
|
||||
</PolicyVersionHistoryDialog>
|
||||
|
||||
<ActionDropdown variant="secondary">
|
||||
<DropdownItem
|
||||
onClick={() => updateDialogRef.current?.open()}
|
||||
icon={IconPencil}
|
||||
>
|
||||
{isDraft ? __("Edit draft policy") : __("Create new draft")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
disabled={isDeleting}
|
||||
onClick={() => confirmRef.current?.open()}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader title={policy.title} />
|
||||
@@ -147,11 +231,11 @@ export default function PolicyPage(props: Props) {
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Status")}>
|
||||
<Badge
|
||||
variant={lastVersion.status === "DRAFT" ? "highlight" : "success"}
|
||||
variant={isDraft ? "highlight" : "success"}
|
||||
size="md"
|
||||
className="gap-2"
|
||||
>
|
||||
{lastVersion.status === "DRAFT" ? __("Draft") : __("Published")}
|
||||
{isDraft ? __("Draft") : __("Published")}
|
||||
</Badge>
|
||||
</PropertyRow>
|
||||
<PropertyRow label={__("Version")}>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @generated SignedSource<<0c65ab6f2531f65b9687e3ca13074774>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type PolicyStatus = "DRAFT" | "PUBLISHED" | "%future added value";
|
||||
export type CreateDraftPolicyVersionInput = {
|
||||
policyID: string;
|
||||
};
|
||||
export type PolicyPageCreateDraftPolicyVersionMutation$variables = {
|
||||
input: CreateDraftPolicyVersionInput;
|
||||
};
|
||||
export type PolicyPageCreateDraftPolicyVersionMutation$data = {
|
||||
readonly createDraftPolicyVersion: {
|
||||
readonly policyVersionEdge: {
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly status: PolicyStatus;
|
||||
readonly version: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
export type PolicyPageCreateDraftPolicyVersionMutation = {
|
||||
response: PolicyPageCreateDraftPolicyVersionMutation$data;
|
||||
variables: PolicyPageCreateDraftPolicyVersionMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "CreateDraftPolicyVersionPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createDraftPolicyVersion",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "policyVersionEdge",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersion",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "version",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "PolicyPageCreateDraftPolicyVersionMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "PolicyPageCreateDraftPolicyVersionMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "197f53d67c5393a8163be2a8ce356cd0",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "PolicyPageCreateDraftPolicyVersionMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation PolicyPageCreateDraftPolicyVersionMutation(\n $input: CreateDraftPolicyVersionInput!\n) {\n createDraftPolicyVersion(input: $input) {\n policyVersionEdge {\n node {\n id\n version\n status\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "7fcf72158c1dc58ae8f9d7efaa6d93a9";
|
||||
|
||||
export default node;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<c59210c06978720ee4b52c41f0337ecf>>
|
||||
* @generated SignedSource<<769555856fa8bff973c4685249abbb2f>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -20,6 +20,7 @@ export type PolicyPagePolicyFragment$data = {
|
||||
};
|
||||
readonly title: string;
|
||||
readonly versions: {
|
||||
readonly __id: string;
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly content: string;
|
||||
@@ -36,6 +37,7 @@ export type PolicyPagePolicyFragment$data = {
|
||||
readonly status: PolicyStatus;
|
||||
readonly updatedAt: any;
|
||||
readonly version: number;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"PolicyVersionHistoryDialogFragment">;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
@@ -57,7 +59,18 @@ var v0 = {
|
||||
return {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"metadata": {
|
||||
"connection": [
|
||||
{
|
||||
"count": null,
|
||||
"cursor": null,
|
||||
"direction": "forward",
|
||||
"path": [
|
||||
"versions"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "PolicyPagePolicyFragment",
|
||||
"selections": [
|
||||
(v0/*: any*/),
|
||||
@@ -88,17 +101,11 @@ return {
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"alias": "versions",
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "versions",
|
||||
"name": "__PolicyPage_versions_connection",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
@@ -199,15 +206,71 @@ return {
|
||||
}
|
||||
],
|
||||
"storageKey": "signatures(first:100)"
|
||||
},
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "PolicyVersionHistoryDialogFragment"
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "cursor",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PageInfo",
|
||||
"kind": "LinkedField",
|
||||
"name": "pageInfo",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "endCursor",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "hasNextPage",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"kind": "ClientExtension",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__id",
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": "versions(first:1)"
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Policy",
|
||||
@@ -215,6 +278,6 @@ return {
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "6b70d6d05d6e1081afae758389d070b2";
|
||||
(node as any).hash = "684db4b721c50638ee5ec1f9f4ebdd16";
|
||||
|
||||
export default node;
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
/**
|
||||
* @generated SignedSource<<f3471e799b803d8bd39ad2a76603716e>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type PolicyPagePolicyQuery$variables = {
|
||||
id: string;
|
||||
};
|
||||
export type PolicyPagePolicyQuery$data = {
|
||||
readonly node: {
|
||||
readonly " $fragmentSpreads": FragmentRefs<"PolicyPagePolicyFragment">;
|
||||
};
|
||||
};
|
||||
export type PolicyPagePolicyQuery = {
|
||||
response: PolicyPagePolicyQuery$data;
|
||||
variables: PolicyPagePolicyQuery$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "id"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "id",
|
||||
"variableName": "id"
|
||||
}
|
||||
],
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "PolicyPagePolicyQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "PolicyPagePolicyFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "PolicyPagePolicyQuery",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "title",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "People",
|
||||
"kind": "LinkedField",
|
||||
"name": "owner",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "PolicyVersionConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "versions",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersion",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 100
|
||||
}
|
||||
],
|
||||
"concreteType": "PolicyVersionSignatureConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "signatures",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionSignatureEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionSignature",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "state",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "signatures(first:100)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "versions(first:1)"
|
||||
}
|
||||
],
|
||||
"type": "Policy",
|
||||
"abstractKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "a7dd624a52cc7f0193b7670476c9de0d",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "PolicyPagePolicyQuery",
|
||||
"operationKind": "query",
|
||||
"text": "query PolicyPagePolicyQuery(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PolicyPagePolicyFragment\n id\n }\n}\n\nfragment PolicyPagePolicyFragment on Policy {\n id\n title\n description\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "a1317341ce16c9f3cfd8f6a6a620c077";
|
||||
|
||||
export default node;
|
||||
@@ -1,249 +0,0 @@
|
||||
/**
|
||||
* @generated SignedSource<<9647dfc253447fe799b8178349c656d0>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type PolicyPagePolicyRefetch$variables = {
|
||||
id: string;
|
||||
};
|
||||
export type PolicyPagePolicyRefetch$data = {
|
||||
readonly node: {
|
||||
readonly " $fragmentSpreads": FragmentRefs<"PolicyPagePolicyFragment">;
|
||||
};
|
||||
};
|
||||
export type PolicyPagePolicyRefetch = {
|
||||
response: PolicyPagePolicyRefetch$data;
|
||||
variables: PolicyPagePolicyRefetch$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "id"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "id",
|
||||
"variableName": "id"
|
||||
}
|
||||
],
|
||||
v2 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "PolicyPagePolicyRefetch",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"args": null,
|
||||
"kind": "FragmentSpread",
|
||||
"name": "PolicyPagePolicyFragment"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Query",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "PolicyPagePolicyRefetch",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v1/*: any*/),
|
||||
"concreteType": null,
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "__typename",
|
||||
"storageKey": null
|
||||
},
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"kind": "InlineFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "title",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "description",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "People",
|
||||
"kind": "LinkedField",
|
||||
"name": "owner",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"concreteType": "PolicyVersionConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "versions",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersion",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 100
|
||||
}
|
||||
],
|
||||
"concreteType": "PolicyVersionSignatureConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "signatures",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionSignatureEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionSignature",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v2/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "state",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "signatures(first:100)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "versions(first:1)"
|
||||
}
|
||||
],
|
||||
"type": "Policy",
|
||||
"abstractKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "6eec3f5bb581dbce013634d36d9b43d9",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "PolicyPagePolicyRefetch",
|
||||
"operationKind": "query",
|
||||
"text": "query PolicyPagePolicyRefetch(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PolicyPagePolicyFragment\n id\n }\n}\n\nfragment PolicyPagePolicyFragment on Policy {\n id\n title\n description\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "e553858c09ded278d1d30aaad4f7ede9";
|
||||
|
||||
export default node;
|
||||
@@ -10,24 +10,23 @@ import {
|
||||
Label,
|
||||
PropertyRow,
|
||||
Textarea,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { usePolicyForm } from "../../../hooks/forms/usePolicyForm";
|
||||
import { usePolicyForm } from "../../../../hooks/forms/usePolicyForm";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
import type { FormPolicyDialogMutation } from "./__generated__/FormPolicyDialogMutation.graphql";
|
||||
import { PeopleSelect } from "/components/form/PeopleSelect";
|
||||
import type { CreatePolicyDialogMutation } from "./__generated__/CreatePolicyDialogMutation.graphql";
|
||||
|
||||
type Props = {
|
||||
trigger?: ReactNode;
|
||||
open?: boolean;
|
||||
onSuccess?: () => void;
|
||||
connection: string;
|
||||
};
|
||||
|
||||
const createPolicyMutation = graphql`
|
||||
mutation FormPolicyDialogMutation(
|
||||
mutation CreatePolicyDialogMutation(
|
||||
$input: CreatePolicyInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
@@ -44,19 +43,14 @@ const createPolicyMutation = graphql`
|
||||
/**
|
||||
* Dialog to create or update a policy
|
||||
*/
|
||||
export default function FormPolicyDialog({
|
||||
trigger,
|
||||
onSuccess,
|
||||
open,
|
||||
connection,
|
||||
}: Props) {
|
||||
export function CreatePolicyDialog({ trigger, connection }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
|
||||
const { control, handleSubmit, register, formState, reset } = usePolicyForm();
|
||||
const errors = formState.errors ?? {};
|
||||
const [createPolicy, isLoading] =
|
||||
useMutationWithToasts<FormPolicyDialogMutation>(createPolicyMutation);
|
||||
useMutationWithToasts<CreatePolicyDialogMutation>(createPolicyMutation);
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
createPolicy({
|
||||
@@ -70,19 +64,17 @@ export default function FormPolicyDialog({
|
||||
successMessage: __("Policy created successfully."),
|
||||
errorMessage: __("Failed to create policy. Please try again."),
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
dialogRef.current?.close();
|
||||
reset();
|
||||
onSuccess?.();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const [isOpen, setOpen] = useState(!!open);
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={setOpen}
|
||||
ref={dialogRef}
|
||||
trigger={trigger}
|
||||
title={<Breadcrumb items={[__("Policies"), __("New Policy")]} />}
|
||||
>
|
||||
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
Avatar,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
Markdown,
|
||||
} from "@probo/ui";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import type {
|
||||
PolicyPagePolicyFragment$data,
|
||||
PolicyPagePolicyFragment$key,
|
||||
} from "../__generated__/PolicyPagePolicyFragment.graphql";
|
||||
import { useFragment } from "react-relay";
|
||||
import type {
|
||||
PolicyVersionHistoryDialogFragment$data,
|
||||
PolicyVersionHistoryDialogFragment$key,
|
||||
} from "./__generated__/PolicyVersionHistoryDialogFragment.graphql";
|
||||
import clsx from "clsx";
|
||||
import type { NodeOf } from "/types";
|
||||
|
||||
const historyFragment = graphql`
|
||||
fragment PolicyVersionHistoryDialogFragment on PolicyVersion {
|
||||
id
|
||||
version
|
||||
status
|
||||
content
|
||||
changelog
|
||||
publishedAt
|
||||
updatedAt
|
||||
publishedBy {
|
||||
fullName
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
policy: PolicyPagePolicyFragment$data;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
type Version = NodeOf<PolicyPagePolicyFragment$data["versions"]>;
|
||||
|
||||
export function PolicyVersionHistoryDialog(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const versions = props.policy.versions.edges.map((edge) => edge.node);
|
||||
const [selectedVersion, setSelectedVersion] = useState<Version>(versions[0]);
|
||||
return (
|
||||
<Dialog defaultOpen trigger={props.children}>
|
||||
<DialogContent className="flex" scrollableChildren>
|
||||
<aside className="p-6 overflow-y-auto w-60 flex-none">
|
||||
<DialogTitle className="text-base text-txt-primary font-medium mb-4">
|
||||
{__("Version History")}
|
||||
</DialogTitle>
|
||||
{versions.map((version) => (
|
||||
<VersionItem
|
||||
key={version.id}
|
||||
version={version}
|
||||
active={selectedVersion === version}
|
||||
onSelect={setSelectedVersion}
|
||||
/>
|
||||
))}
|
||||
</aside>
|
||||
<main className="flex-1 px-12 py-8">
|
||||
<Markdown content={selectedVersion.content} />
|
||||
</main>
|
||||
</DialogContent>
|
||||
<DialogFooter></DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function VersionItem(props: {
|
||||
version: Version;
|
||||
active?: boolean;
|
||||
onSelect: (v: Version) => void;
|
||||
}) {
|
||||
const version = useFragment<PolicyVersionHistoryDialogFragment$key>(
|
||||
historyFragment,
|
||||
props.version
|
||||
);
|
||||
const { dateFormat } = useTranslate();
|
||||
return (
|
||||
<button
|
||||
onClick={() => props.onSelect(props.version)}
|
||||
className={clsx(
|
||||
"flex items-center gap-1 py-2 px-[10px] w-full hover:bg-tertiary-hover cursor-pointer rounded",
|
||||
props.active && "bg-tertiary-pressed"
|
||||
)}
|
||||
>
|
||||
<Avatar name={version.publishedBy?.fullName ?? ""} size="l" />
|
||||
<div className="space-y-[2px]">
|
||||
<div className="text-sm text-txt-primary">
|
||||
{version.publishedBy?.fullName ?? "Anonymous"}
|
||||
</div>
|
||||
<div className="text-xs text-txt-secondary">
|
||||
{dateFormat(version.publishedAt, {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Spinner,
|
||||
Textarea,
|
||||
useDialogRef,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useState, type RefObject } from "react";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useMutation } from "react-relay";
|
||||
import type { PolicyPagePolicyFragment$data } from "../__generated__/PolicyPagePolicyFragment.graphql";
|
||||
import type { UpdateVersionDialogCreateMutation } from "./__generated__/UpdateVersionDialogCreateMutation.graphql";
|
||||
import type { UpdateVersionDialogUpdateMutation } from "./__generated__/UpdateVersionDialogUpdateMutation.graphql";
|
||||
import { z } from "zod";
|
||||
import { useFormWithSchema } from "/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||
|
||||
const createDraftPolicy = graphql`
|
||||
mutation UpdateVersionDialogCreateMutation(
|
||||
$input: CreateDraftPolicyVersionInput!
|
||||
$connections: [ID!]!
|
||||
) {
|
||||
createDraftPolicyVersion(input: $input) {
|
||||
policyVersionEdge @prependEdge(connections: $connections) {
|
||||
node {
|
||||
id
|
||||
content
|
||||
status
|
||||
publishedAt
|
||||
version
|
||||
updatedAt
|
||||
signatures(first: 100) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UpdatePolicyMutation = graphql`
|
||||
mutation UpdateVersionDialogUpdateMutation(
|
||||
$input: UpdatePolicyVersionInput!
|
||||
) {
|
||||
updatePolicyVersion(input: $input) {
|
||||
policyVersion {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
policy: PolicyPagePolicyFragment$data;
|
||||
connectionId: string;
|
||||
ref: RefObject<{ open: () => void } | null>;
|
||||
};
|
||||
|
||||
const versionSchema = z.object({
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
export default function UpdateVersionDialog({
|
||||
policy,
|
||||
connectionId,
|
||||
ref,
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const dialogRef = useDialogRef();
|
||||
|
||||
const version = policy.versions.edges[0].node;
|
||||
const isDraft = version?.status === "DRAFT";
|
||||
const [createDraftPolicyVersion, isCreatingDraft] =
|
||||
useMutation<UpdateVersionDialogCreateMutation>(createDraftPolicy);
|
||||
const [updatePolicyVersion, isUpdating] =
|
||||
useMutationWithToasts<UpdateVersionDialogUpdateMutation>(
|
||||
UpdatePolicyMutation,
|
||||
{
|
||||
successMessage: __("Policy updated successfully."),
|
||||
errorMessage: __("Failed to update policy. Please try again."),
|
||||
}
|
||||
);
|
||||
const { handleSubmit, register } = useFormWithSchema(versionSchema, {
|
||||
defaultValues: {
|
||||
content: version.content,
|
||||
},
|
||||
});
|
||||
|
||||
ref.current = {
|
||||
open: () => {
|
||||
dialogRef.current?.open();
|
||||
if (!isDraft) {
|
||||
createDraftPolicyVersion({
|
||||
variables: {
|
||||
input: {
|
||||
policyID: policy.id,
|
||||
},
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors) {
|
||||
toast({
|
||||
variant: "error",
|
||||
title: __("Error creating draft"),
|
||||
description:
|
||||
errors[0]?.message || __("An unknown error occurred"),
|
||||
});
|
||||
dialogRef.current?.close();
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
updatePolicyVersion({
|
||||
variables: {
|
||||
input: {
|
||||
policyVersionId: version.id,
|
||||
content: data.content,
|
||||
},
|
||||
},
|
||||
onSuccess: () => {
|
||||
dialogRef.current?.close();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ref={dialogRef}
|
||||
title={<Breadcrumb items={[__("Policies"), __("Edit policy")]} />}
|
||||
>
|
||||
{isCreatingDraft && <Spinner centered />}
|
||||
<form onSubmit={onSubmit}>
|
||||
<DialogContent>
|
||||
<Textarea
|
||||
id="content"
|
||||
variant="ghost"
|
||||
autogrow
|
||||
required
|
||||
placeholder={__("Add description")}
|
||||
aria-label={__("Description")}
|
||||
className="p-6"
|
||||
{...register("content")}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button disabled={isUpdating} type="submit">
|
||||
{__("Update policy")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<418554f5765131f61a3dfed83db0da76>>
|
||||
* @generated SignedSource<<0471409a1735c49fc0b05b5e63e97853>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
@@ -16,11 +16,11 @@ export type CreatePolicyInput = {
|
||||
ownerId: string;
|
||||
title: string;
|
||||
};
|
||||
export type FormPolicyDialogMutation$variables = {
|
||||
export type CreatePolicyDialogMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: CreatePolicyInput;
|
||||
};
|
||||
export type FormPolicyDialogMutation$data = {
|
||||
export type CreatePolicyDialogMutation$data = {
|
||||
readonly createPolicy: {
|
||||
readonly policyEdge: {
|
||||
readonly node: {
|
||||
@@ -29,9 +29,9 @@ export type FormPolicyDialogMutation$data = {
|
||||
};
|
||||
};
|
||||
};
|
||||
export type FormPolicyDialogMutation = {
|
||||
response: FormPolicyDialogMutation$data;
|
||||
variables: FormPolicyDialogMutation$variables;
|
||||
export type CreatePolicyDialogMutation = {
|
||||
response: CreatePolicyDialogMutation$data;
|
||||
variables: CreatePolicyDialogMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
@@ -67,7 +67,7 @@ return {
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "FormPolicyDialogMutation",
|
||||
"name": "CreatePolicyDialogMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -118,7 +118,7 @@ return {
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "FormPolicyDialogMutation",
|
||||
"name": "CreatePolicyDialogMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
@@ -307,16 +307,16 @@ return {
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "5967e987ab20d7823e7456a22aec54f2",
|
||||
"cacheID": "7ca1b2811e06da7ba97064950fb8f632",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "FormPolicyDialogMutation",
|
||||
"name": "CreatePolicyDialogMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation FormPolicyDialogMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n ...PoliciesPageRowFragment\n id\n }\n }\n }\n}\n\nfragment PoliciesPageRowFragment on Policy {\n id\n title\n description\n updatedAt\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
|
||||
"text": "mutation CreatePolicyDialogMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n ...PoliciesPageRowFragment\n id\n }\n }\n }\n}\n\nfragment PoliciesPageRowFragment on Policy {\n id\n title\n description\n updatedAt\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "c2612f85b9170d81cb9fb4908f752069";
|
||||
(node as any).hash = "6c749af4def68b742bc5eb80dd120c8a";
|
||||
|
||||
export default node;
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @generated SignedSource<<929e2bdd729e8b9654b27cd9e9dbccb2>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ReaderFragment } from 'relay-runtime';
|
||||
export type PolicyStatus = "DRAFT" | "PUBLISHED" | "%future added value";
|
||||
import { FragmentRefs } from "relay-runtime";
|
||||
export type PolicyVersionHistoryDialogFragment$data = {
|
||||
readonly changelog: string;
|
||||
readonly content: string;
|
||||
readonly id: string;
|
||||
readonly publishedAt: any | null | undefined;
|
||||
readonly publishedBy: {
|
||||
readonly fullName: string;
|
||||
} | null | undefined;
|
||||
readonly status: PolicyStatus;
|
||||
readonly updatedAt: any;
|
||||
readonly version: number;
|
||||
readonly " $fragmentType": "PolicyVersionHistoryDialogFragment";
|
||||
};
|
||||
export type PolicyVersionHistoryDialogFragment$key = {
|
||||
readonly " $data"?: PolicyVersionHistoryDialogFragment$data;
|
||||
readonly " $fragmentSpreads": FragmentRefs<"PolicyVersionHistoryDialogFragment">;
|
||||
};
|
||||
|
||||
const node: ReaderFragment = {
|
||||
"argumentDefinitions": [],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "PolicyVersionHistoryDialogFragment",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "version",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "content",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "changelog",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "publishedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "People",
|
||||
"kind": "LinkedField",
|
||||
"name": "publishedBy",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "fullName",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "PolicyVersion",
|
||||
"abstractKey": null
|
||||
};
|
||||
|
||||
(node as any).hash = "63ce12a876b10889d9ff2e4ee20af968";
|
||||
|
||||
export default node;
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* @generated SignedSource<<e90085131e94bcac0f94ecf5b1329712>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type PolicyStatus = "DRAFT" | "PUBLISHED" | "%future added value";
|
||||
export type PolicyVersionSignatureState = "REQUESTED" | "SIGNED" | "%future added value";
|
||||
export type CreateDraftPolicyVersionInput = {
|
||||
policyID: string;
|
||||
};
|
||||
export type UpdateVersionDialogCreateMutation$variables = {
|
||||
connections: ReadonlyArray<string>;
|
||||
input: CreateDraftPolicyVersionInput;
|
||||
};
|
||||
export type UpdateVersionDialogCreateMutation$data = {
|
||||
readonly createDraftPolicyVersion: {
|
||||
readonly policyVersionEdge: {
|
||||
readonly node: {
|
||||
readonly content: string;
|
||||
readonly id: string;
|
||||
readonly publishedAt: any | null | undefined;
|
||||
readonly signatures: {
|
||||
readonly edges: ReadonlyArray<{
|
||||
readonly node: {
|
||||
readonly id: string;
|
||||
readonly state: PolicyVersionSignatureState;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
readonly status: PolicyStatus;
|
||||
readonly updatedAt: any;
|
||||
readonly version: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
export type UpdateVersionDialogCreateMutation = {
|
||||
response: UpdateVersionDialogCreateMutation$data;
|
||||
variables: UpdateVersionDialogCreateMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "connections"
|
||||
},
|
||||
v1 = {
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
},
|
||||
v2 = [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
v3 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
v4 = {
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "policyVersionEdge",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersion",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "content",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "status",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "publishedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "version",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "updatedAt",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Literal",
|
||||
"name": "first",
|
||||
"value": 100
|
||||
}
|
||||
],
|
||||
"concreteType": "PolicyVersionSignatureConnection",
|
||||
"kind": "LinkedField",
|
||||
"name": "signatures",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionSignatureEdge",
|
||||
"kind": "LinkedField",
|
||||
"name": "edges",
|
||||
"plural": true,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersionSignature",
|
||||
"kind": "LinkedField",
|
||||
"name": "node",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v3/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "state",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": "signatures(first:100)"
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
};
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": [
|
||||
(v0/*: any*/),
|
||||
(v1/*: any*/)
|
||||
],
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "UpdateVersionDialogCreateMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "CreateDraftPolicyVersionPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createDraftPolicyVersion",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v4/*: any*/)
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": [
|
||||
(v1/*: any*/),
|
||||
(v0/*: any*/)
|
||||
],
|
||||
"kind": "Operation",
|
||||
"name": "UpdateVersionDialogCreateMutation",
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": (v2/*: any*/),
|
||||
"concreteType": "CreateDraftPolicyVersionPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "createDraftPolicyVersion",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
(v4/*: any*/),
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"filters": null,
|
||||
"handle": "prependEdge",
|
||||
"key": "",
|
||||
"kind": "LinkedHandle",
|
||||
"name": "policyVersionEdge",
|
||||
"handleArgs": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "connections",
|
||||
"variableName": "connections"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "c5b0f449a70a4043368de55bba176329",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "UpdateVersionDialogCreateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation UpdateVersionDialogCreateMutation(\n $input: CreateDraftPolicyVersionInput!\n) {\n createDraftPolicyVersion(input: $input) {\n policyVersionEdge {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "6a451392a86776d33c376bfdcea7613c";
|
||||
|
||||
export default node;
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @generated SignedSource<<6cbec0c0b675969ecdebebf74c7465ab>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type UpdatePolicyVersionInput = {
|
||||
content: string;
|
||||
policyVersionId: string;
|
||||
};
|
||||
export type UpdateVersionDialogUpdateMutation$variables = {
|
||||
input: UpdatePolicyVersionInput;
|
||||
};
|
||||
export type UpdateVersionDialogUpdateMutation$data = {
|
||||
readonly updatePolicyVersion: {
|
||||
readonly policyVersion: {
|
||||
readonly content: string;
|
||||
readonly id: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
export type UpdateVersionDialogUpdateMutation = {
|
||||
response: UpdateVersionDialogUpdateMutation$data;
|
||||
variables: UpdateVersionDialogUpdateMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "UpdatePolicyVersionPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "updatePolicyVersion",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"concreteType": "PolicyVersion",
|
||||
"kind": "LinkedField",
|
||||
"name": "policyVersion",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "id",
|
||||
"storageKey": null
|
||||
},
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "content",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "UpdateVersionDialogUpdateMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "UpdateVersionDialogUpdateMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "374557bf1319583b7d590f5bea438e63",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "UpdateVersionDialogUpdateMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation UpdateVersionDialogUpdateMutation(\n $input: UpdatePolicyVersionInput!\n) {\n updatePolicyVersion(input: $input) {\n policyVersion {\n id\n content\n }\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "d863d893bfa969b02768096e024275a4";
|
||||
|
||||
export default node;
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
PropertyRow,
|
||||
IconPlusLarge,
|
||||
Textarea,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import { useFetchQuery } from "/hooks/useFetchQuery";
|
||||
@@ -37,10 +38,9 @@ import { getRiskImpacts, getRiskLikelihoods } from "@probo/helpers";
|
||||
|
||||
type Props = {
|
||||
trigger?: ReactNode;
|
||||
open?: boolean;
|
||||
risk?: RiskKey;
|
||||
onSuccess?: () => void;
|
||||
connection?: string;
|
||||
ref?: ReturnType<typeof useDialogRef>;
|
||||
};
|
||||
|
||||
type RiskTemplate = {
|
||||
@@ -80,12 +80,12 @@ const updateRiskMutation = graphql`
|
||||
export default function FormRiskDialog({
|
||||
trigger,
|
||||
risk,
|
||||
onSuccess,
|
||||
open,
|
||||
connection,
|
||||
ref: refProps,
|
||||
}: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const ref = refProps ?? useDialogRef();
|
||||
|
||||
const { control, handleSubmit, setValue, register, watch, formState, reset } =
|
||||
useRiskForm(risk);
|
||||
@@ -113,8 +113,7 @@ export default function FormRiskDialog({
|
||||
successMessage: __("Risk updated successfully."),
|
||||
errorMessage: __("Failed to update risk. Please try again."),
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
ref?.current?.close();
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -130,20 +129,17 @@ export default function FormRiskDialog({
|
||||
successMessage: __("Risk created successfully."),
|
||||
errorMessage: __("Failed to create risk. Please try again."),
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
ref?.current?.close();
|
||||
reset();
|
||||
onSuccess?.();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const [showNote, toggleNote] = useToggle(false);
|
||||
const [isOpen, setOpen] = useState(!!open);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={setOpen}
|
||||
ref={ref}
|
||||
trigger={trigger}
|
||||
title={
|
||||
<Breadcrumb
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
PropertyRow,
|
||||
TabLink,
|
||||
Tabs,
|
||||
useConfirmDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { Outlet, useNavigate, useParams } from "react-router";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
@@ -54,17 +55,22 @@ export default function RiskDetailPage(props: Props) {
|
||||
organizationId,
|
||||
RisksConnectionKey
|
||||
);
|
||||
deleteRisk({
|
||||
variables: {
|
||||
input: { riskId },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onSuccess() {
|
||||
navigate(`/organizations/${organizationId}/risks`);
|
||||
},
|
||||
return new Promise<void>((resolve) => {
|
||||
deleteRisk({
|
||||
variables: {
|
||||
input: { riskId },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onSuccess() {
|
||||
navigate(`/organizations/${organizationId}/risks`);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const confirmRef = useConfirmDialogRef();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -90,23 +96,28 @@ export default function RiskDetailPage(props: Props) {
|
||||
risk={{ id: riskId, ...risk }}
|
||||
/>
|
||||
<ActionDropdown variant="secondary">
|
||||
<ConfirmDialog
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the risk "%s". This action cannot be undone.'
|
||||
),
|
||||
risk.name
|
||||
)}
|
||||
onConfirm={() => onDelete(riskId)}
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={() => confirmRef.current?.open()}
|
||||
>
|
||||
<DropdownItem variant="danger" icon={IconTrashCan}>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ConfirmDialog>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
ref={confirmRef}
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the risk "%s". This action cannot be undone.'
|
||||
),
|
||||
risk.name
|
||||
)}
|
||||
onConfirm={() => onDelete(riskId)}
|
||||
/>
|
||||
|
||||
<PageHeader title={risk.name} />
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useConfirmDialogRef,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import FormRiskDialog from "./FormRiskDialog";
|
||||
@@ -21,11 +23,12 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { useState } from "react";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
import { getTreatment, sprintf } from "@probo/helpers";
|
||||
import type { ItemOf } from "/types";
|
||||
import type { ItemOf, NodeOf } from "/types";
|
||||
import { useDeleteRiskMutation, useRisksQuery } from "/hooks/graph/RiskGraph";
|
||||
import { SortableTable, SortableTh } from "/components/SortableTable";
|
||||
import type { PreloadedQuery } from "react-relay";
|
||||
import type { RiskGraphListQuery } from "/hooks/graph/__generated__/RiskGraphListQuery.graphql";
|
||||
import type { RiskGraphFragment$data } from "/hooks/graph/__generated__/RiskGraphFragment.graphql";
|
||||
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<RiskGraphListQuery>;
|
||||
@@ -33,23 +36,10 @@ type Props = {
|
||||
|
||||
export default function RisksPage(props: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const [editedRisk, setEditedRisk] = useState<ItemOf<typeof risks> | null>(
|
||||
null
|
||||
);
|
||||
const organizationId = useOrganizationId();
|
||||
|
||||
const [deleteRisk] = useDeleteRiskMutation();
|
||||
const { connectionId, risks, refetch } = useRisksQuery(props.queryRef);
|
||||
|
||||
const onDelete = (riskId: string) => {
|
||||
deleteRisk({
|
||||
variables: {
|
||||
input: { riskId },
|
||||
connections: [connectionId],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
usePageTitle(__("Risks"));
|
||||
|
||||
return (
|
||||
@@ -60,14 +50,6 @@ export default function RisksPage(props: Props) {
|
||||
trigger={<Button icon={IconPlusLarge}>{__("New Risk")}</Button>}
|
||||
/>
|
||||
</PageHeader>
|
||||
{editedRisk && (
|
||||
<FormRiskDialog
|
||||
open
|
||||
risk={editedRisk}
|
||||
connection={connectionId}
|
||||
onSuccess={() => setEditedRisk(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<RisksChart
|
||||
@@ -98,46 +80,88 @@ export default function RisksPage(props: Props) {
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{risks?.map((risk) => (
|
||||
<Tr
|
||||
<RiskRow
|
||||
risk={risk}
|
||||
key={risk.id}
|
||||
to={`/organizations/${organizationId}/risks/${risk.id}`}
|
||||
>
|
||||
<Td>{risk.name}</Td>
|
||||
<Td>{risk.category}</Td>
|
||||
<Td>{getTreatment(__, risk.treatment)}</Td>
|
||||
<Td>
|
||||
<SeverityBadge score={risk.inherentRiskScore} />
|
||||
</Td>
|
||||
<Td>
|
||||
<SeverityBadge score={risk.residualRiskScore} />
|
||||
</Td>
|
||||
<Td noLink className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
icon={IconPencil}
|
||||
onClick={() => setEditedRisk(risk)}
|
||||
>
|
||||
{__("Edit")}
|
||||
</DropdownItem>
|
||||
<ConfirmDialog
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the risk "%s". This action cannot be undone.'
|
||||
),
|
||||
risk.name
|
||||
)}
|
||||
onConfirm={() => onDelete(risk.id)}
|
||||
>
|
||||
<DropdownItem variant="danger" icon={IconTrashCan}>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ConfirmDialog>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
connectionId={connectionId}
|
||||
organizationId={organizationId}
|
||||
/>
|
||||
))}
|
||||
</Tbody>
|
||||
</SortableTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type RowProps = {
|
||||
risk: NodeOf<RiskGraphFragment$data["risks"]>;
|
||||
connectionId: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
function RiskRow(props: RowProps) {
|
||||
const { __ } = useTranslate();
|
||||
const { risk, connectionId, organizationId } = props;
|
||||
const [deleteRisk] = useDeleteRiskMutation();
|
||||
const onDelete = (riskId: string) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
deleteRisk({
|
||||
variables: {
|
||||
input: { riskId },
|
||||
connections: [connectionId],
|
||||
},
|
||||
onCompleted: () => resolve(),
|
||||
});
|
||||
});
|
||||
};
|
||||
const confirmRef = useConfirmDialogRef();
|
||||
const formDialogRef = useDialogRef();
|
||||
return (
|
||||
<>
|
||||
<ConfirmDialog
|
||||
ref={confirmRef}
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the risk "%s". This action cannot be undone.'
|
||||
),
|
||||
risk.name
|
||||
)}
|
||||
onConfirm={() => onDelete(risk.id)}
|
||||
/>
|
||||
<FormRiskDialog
|
||||
ref={formDialogRef}
|
||||
risk={risk}
|
||||
connection={connectionId}
|
||||
/>
|
||||
<Tr to={`/organizations/${organizationId}/risks/${risk.id}`}>
|
||||
<Td>{risk.name}</Td>
|
||||
<Td>{risk.category}</Td>
|
||||
<Td>{getTreatment(__, risk.treatment)}</Td>
|
||||
<Td>
|
||||
<SeverityBadge score={risk.inherentRiskScore} />
|
||||
</Td>
|
||||
<Td>
|
||||
<SeverityBadge score={risk.residualRiskScore} />
|
||||
</Td>
|
||||
<Td noLink className="text-end">
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
icon={IconPencil}
|
||||
onClick={() => confirmRef.current?.open()}
|
||||
>
|
||||
{__("Edit")}
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onClick={() => confirmRef.current?.open()}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
DropdownItem,
|
||||
IconTrashCan,
|
||||
ConfirmDialog,
|
||||
useConfirmDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
@@ -134,66 +135,77 @@ function VendorRow({
|
||||
const [deleteVendor] = useDeleteVendorMutation();
|
||||
|
||||
const onDelete = (vendorId: string) => {
|
||||
deleteVendor({
|
||||
variables: {
|
||||
input: {
|
||||
vendorId,
|
||||
return new Promise<void>((resolve) => {
|
||||
deleteVendor({
|
||||
variables: {
|
||||
input: {
|
||||
vendorId,
|
||||
},
|
||||
connections: [
|
||||
ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
"VendorsPage_vendors"
|
||||
),
|
||||
],
|
||||
},
|
||||
connections: [
|
||||
ConnectionHandler.getConnectionID(
|
||||
organizationId,
|
||||
"VendorsPage_vendors"
|
||||
),
|
||||
],
|
||||
},
|
||||
onCompleted: () => resolve(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const confirmRef = useConfirmDialogRef();
|
||||
|
||||
return (
|
||||
<Tr to={`/organizations/${organizationId}/vendors/${vendor.id}`}>
|
||||
<Td>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} />
|
||||
<div>{vendor.name}</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{latestAssessment?.assessedAt
|
||||
? dateFormat(latestAssessment.assessedAt, {
|
||||
day: "2-digit",
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
})
|
||||
: __("Not assessed")}
|
||||
</Td>
|
||||
<Td>
|
||||
<RiskBadge level={latestAssessment?.dataSensitivity ?? "NONE"} />
|
||||
</Td>
|
||||
<Td>
|
||||
<RiskBadge level={latestAssessment?.businessImpact ?? "NONE"} />
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant={isExpired ? "danger" : "warning"}>
|
||||
{isExpired ? __("Late") : __("In progress")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td noLink>
|
||||
<ActionDropdown>
|
||||
<ConfirmDialog
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the vendor "%s". This action cannot be undone.'
|
||||
),
|
||||
vendor.name
|
||||
)}
|
||||
onConfirm={() => onDelete(vendor.id)}
|
||||
>
|
||||
<DropdownItem variant="danger" icon={IconTrashCan}>
|
||||
<>
|
||||
<ConfirmDialog
|
||||
ref={confirmRef}
|
||||
message={sprintf(
|
||||
__(
|
||||
'This will permanently delete the vendor "%s". This action cannot be undone.'
|
||||
),
|
||||
vendor.name
|
||||
)}
|
||||
onConfirm={() => onDelete(vendor.id)}
|
||||
/>
|
||||
<Tr to={`/organizations/${organizationId}/vendors/${vendor.id}`}>
|
||||
<Td>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Avatar name={vendor.name} src={faviconUrl(vendor.websiteUrl)} />
|
||||
<div>{vendor.name}</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
{latestAssessment?.assessedAt
|
||||
? dateFormat(latestAssessment.assessedAt, {
|
||||
day: "2-digit",
|
||||
weekday: "short",
|
||||
month: "short",
|
||||
})
|
||||
: __("Not assessed")}
|
||||
</Td>
|
||||
<Td>
|
||||
<RiskBadge level={latestAssessment?.dataSensitivity ?? "NONE"} />
|
||||
</Td>
|
||||
<Td>
|
||||
<RiskBadge level={latestAssessment?.businessImpact ?? "NONE"} />
|
||||
</Td>
|
||||
<Td>
|
||||
<Badge variant={isExpired ? "danger" : "warning"}>
|
||||
{isExpired ? __("Late") : __("In progress")}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td noLink>
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
onClick={() => confirmRef.current?.open()}
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ConfirmDialog>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
</ActionDropdown>
|
||||
</Td>
|
||||
</Tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@
|
||||
"lint": "eslint .",
|
||||
"dev": "storybook dev -p 6006 --no-open",
|
||||
"build": "storybook build",
|
||||
"check": "tsc --noEmit -p tsconfig.app.json"
|
||||
"check": "tsc --noEmit -p tsconfig.app.json",
|
||||
"icons": "bun run src/Atoms/Icons/generator.ts"
|
||||
},
|
||||
"prettier": "@probo/prettier",
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.17",
|
||||
"@probo/helpers": "1.0.0",
|
||||
"@probo/i18n": "1.0.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-portal": "^1.1.9",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
|
||||
@@ -8,7 +8,7 @@ type Props = {
|
||||
|
||||
export function Avatar({ name, src, size = "m" }: Props) {
|
||||
const className = clsx(
|
||||
"bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center",
|
||||
"bg-txt-success text-txt-invert rounded-full font-semibold flex items-center justify-center flex-none",
|
||||
size === "s" && "size-5 text-xss",
|
||||
size === "m" && "size-6 text-xxs",
|
||||
size === "l" && "size-8 text-sm",
|
||||
|
||||
@@ -62,6 +62,7 @@ export const Button = (props: Props) => {
|
||||
iconAfter: IconAfterComponent,
|
||||
children,
|
||||
onClick,
|
||||
variant,
|
||||
...componentProps
|
||||
} = props;
|
||||
return (
|
||||
@@ -69,7 +70,7 @@ export const Button = (props: Props) => {
|
||||
<Component
|
||||
{...componentProps}
|
||||
onClick={onClick}
|
||||
className={button({ ...props, empty: !children })}
|
||||
className={button({ ...props, variant, empty: !children })}
|
||||
>
|
||||
{IconComponent && <IconComponent size={16} className="flex-none" />}
|
||||
{children}
|
||||
|
||||
8
packages/ui/src/Atoms/Icons/IconClock.tsx
Normal file
8
packages/ui/src/Atoms/Icons/IconClock.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import type {IconProps} from "./type.ts";
|
||||
|
||||
export function IconClock({size = 24, className}: IconProps) {
|
||||
return <svg width={size} height={size} viewBox="0 0 16 16" fill="currentColor" className={className} xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3335 8.00065C13.3335 5.05513 10.9457 2.66732 8.00016 2.66732C5.05465 2.66732 2.66683 5.05513 2.66683 8.00065C2.66683 10.9462 5.05464 13.334 8.00016 13.334C10.9457 13.334 13.3335 10.9462 13.3335 8.00065ZM7.3335 5.33398C7.3335 4.96579 7.63197 4.66732 8.00016 4.66732C8.36835 4.66732 8.66683 4.96579 8.66683 5.33398V7.72461L10.1382 9.19596L10.1838 9.24675C10.3973 9.50859 10.3823 9.8946 10.1382 10.1387C9.89411 10.3827 9.50811 10.3978 9.24626 10.1842L9.19548 10.1387L7.52881 8.47201C7.40378 8.34698 7.3335 8.17746 7.3335 8.00065V5.33398ZM14.6668 8.00065C14.6668 11.6826 11.6821 14.6673 8.00016 14.6673C4.31827 14.6673 1.3335 11.6826 1.3335 8.00065C1.3335 4.31875 4.31827 1.33398 8.00016 1.33398C11.6821 1.33398 14.6668 4.31876 14.6668 8.00065Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
}
|
||||
3
packages/ui/src/Atoms/Icons/clock.svg
Normal file
3
packages/ui/src/Atoms/Icons/clock.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3335 8.00065C13.3335 5.05513 10.9457 2.66732 8.00016 2.66732C5.05465 2.66732 2.66683 5.05513 2.66683 8.00065C2.66683 10.9462 5.05464 13.334 8.00016 13.334C10.9457 13.334 13.3335 10.9462 13.3335 8.00065ZM7.3335 5.33398C7.3335 4.96579 7.63197 4.66732 8.00016 4.66732C8.36835 4.66732 8.66683 4.96579 8.66683 5.33398V7.72461L10.1382 9.19596L10.1838 9.24675C10.3973 9.50859 10.3823 9.8946 10.1382 10.1387C9.89411 10.3827 9.50811 10.3978 9.24626 10.1842L9.19548 10.1387L7.52881 8.47201C7.40378 8.34698 7.3335 8.17746 7.3335 8.00065V5.33398ZM14.6668 8.00065C14.6668 11.6826 11.6821 14.6673 8.00016 14.6673C4.31827 14.6673 1.3335 11.6826 1.3335 8.00065C1.3335 4.31875 4.31827 1.33398 8.00016 1.33398C11.6821 1.33398 14.6668 4.31876 14.6668 8.00065Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 873 B |
@@ -12,6 +12,7 @@ export { IconCrossLargeX } from "./IconCrossLargeX.tsx";
|
||||
export { IconUpload } from "./IconUpload.tsx";
|
||||
export { IconListStack } from "./IconListStack.tsx";
|
||||
export { IconCircleCheck1 } from "./IconCircleCheck1.tsx";
|
||||
export { IconClock } from "./IconClock.tsx";
|
||||
export { IconImport2 } from "./IconImport2.tsx";
|
||||
export { IconChevronTriangleDownSmall } from "./IconChevronTriangleDownSmall.tsx";
|
||||
export { IconCheckmark1 } from "./IconCheckmark1.tsx";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
useRef,
|
||||
type FormEventHandler,
|
||||
type TextareaHTMLAttributes,
|
||||
type RefObject,
|
||||
type RefCallback,
|
||||
useLayoutEffect,
|
||||
} from "react";
|
||||
import { input } from "../Input/Input";
|
||||
import clsx from "clsx";
|
||||
@@ -10,26 +10,35 @@ import clsx from "clsx";
|
||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||
variant?: "bordered" | "ghost" | "title";
|
||||
autogrow?: boolean;
|
||||
ref?: RefObject<HTMLTextAreaElement>;
|
||||
ref?: RefCallback<HTMLTextAreaElement>;
|
||||
};
|
||||
|
||||
export function Textarea(props: Props) {
|
||||
const textbox = useRef<HTMLTextAreaElement>(null);
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
const { autogrow, variant, ref: propsRef, ...restProps } = props;
|
||||
const ref = propsRef || textbox;
|
||||
|
||||
const adjustHeight: FormEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
props.onInput?.(e);
|
||||
const adjustHeight = () => {
|
||||
if (!autogrow || !ref.current) return;
|
||||
ref.current.style.height = "inherit";
|
||||
const paddingY = 2;
|
||||
ref.current.style.height = `${ref.current.scrollHeight + paddingY * 2}px`;
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
adjustHeight();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
{...restProps}
|
||||
ref={ref}
|
||||
onInput={adjustHeight}
|
||||
ref={(node) => {
|
||||
ref.current = node;
|
||||
propsRef?.(node);
|
||||
}}
|
||||
onInput={(e) => {
|
||||
adjustHeight();
|
||||
props.onInput?.(e);
|
||||
}}
|
||||
className={input({
|
||||
...props,
|
||||
className: clsx("min-h-20", props.className),
|
||||
|
||||
@@ -19,7 +19,13 @@ export const Default: Story = {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
message="Are you sure you want to delete this risk?"
|
||||
onConfirm={() => {}}
|
||||
onConfirm={() => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant="danger">Delete this</Button>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Dialog, DialogContent, DialogFooter } from "./Dialog";
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
Root,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
Cancel,
|
||||
} from "@radix-ui/react-alert-dialog";
|
||||
import {
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type MouseEvent,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { Button } from "../../Atoms/Button/Button";
|
||||
import { Root as Portal } from "@radix-ui/react-portal";
|
||||
import { dialog } from "./Dialog";
|
||||
|
||||
type Props = {
|
||||
children?: ReactNode;
|
||||
@@ -18,7 +25,12 @@ type Props = {
|
||||
message: string;
|
||||
variant?: ComponentProps<typeof Button>["variant"];
|
||||
label?: string;
|
||||
onConfirm?: () => void;
|
||||
onConfirm?: () => Promise<void>;
|
||||
ref?: RefObject<{ open: () => void } | null>;
|
||||
};
|
||||
|
||||
export const useConfirmDialogRef = () => {
|
||||
return useRef<{ open: () => void } | null>(null);
|
||||
};
|
||||
|
||||
export function ConfirmDialog(props: Props) {
|
||||
@@ -27,45 +39,52 @@ export function ConfirmDialog(props: Props) {
|
||||
const variant = props.variant ?? "danger";
|
||||
const label = variant === "danger" ? __("Delete") : props.label;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {
|
||||
overlay,
|
||||
content,
|
||||
header,
|
||||
footer,
|
||||
title: titleClassname,
|
||||
} = dialog();
|
||||
const onConfirm = () => {
|
||||
setOpen(false);
|
||||
props.onConfirm?.();
|
||||
document.body.click();
|
||||
};
|
||||
const children = Children.map(props.children, (child) => {
|
||||
if (!isValidElement(child)) {
|
||||
throw new Error("Cannot use non element children");
|
||||
}
|
||||
return cloneElement(child, {
|
||||
// @ts-expect-error We inject a handler on an unknown element
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
},
|
||||
setLoading(true);
|
||||
props.onConfirm?.().then(() => {
|
||||
setLoading(false);
|
||||
setOpen(false);
|
||||
});
|
||||
});
|
||||
};
|
||||
if (props.ref) {
|
||||
props.ref.current = {
|
||||
open: () => setOpen(true),
|
||||
};
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Root open={open} onOpenChange={setOpen}>
|
||||
{props.children && <Trigger asChild children={props.children} />}
|
||||
<Portal>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={title}
|
||||
className="max-w-[500px]"
|
||||
>
|
||||
<DialogContent>
|
||||
<p className="text-sm text-txt-secondary p-4">
|
||||
{props.message}
|
||||
</p>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button variant={variant} onClick={onConfirm}>
|
||||
<Overlay className={overlay()} />
|
||||
<Content className={content({ className: "max-w-[500px]" })}>
|
||||
<header className={header()}>
|
||||
<Title children={title} className={titleClassname()} />
|
||||
</header>
|
||||
<Description className="p-6" children={props.message} />
|
||||
<footer className={footer()}>
|
||||
<Cancel asChild>
|
||||
<Button disabled={loading} variant="tertiary">
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
</Cancel>
|
||||
<Button
|
||||
disabled={loading}
|
||||
variant={variant}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</footer>
|
||||
</Content>
|
||||
</Portal>
|
||||
</>
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,63 +8,100 @@ import {
|
||||
Close,
|
||||
} from "@radix-ui/react-dialog";
|
||||
import { IconCrossLargeX } from "../../Atoms/Icons";
|
||||
import { type HTMLAttributes, type ReactNode } from "react";
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type CSSProperties,
|
||||
type HTMLAttributes,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { Button } from "../../Atoms/Button/Button";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import clsx from "clsx";
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const dialog = tv({
|
||||
slots: {
|
||||
overlay:
|
||||
"fixed inset-0 z-50 bg-dialog/40 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
content:
|
||||
"text-txt-secondary text-sm fixed inset-0 m-auto z-50 w-full h-max bg-level-2 rounded-2xl max-w-5xl w-[95%] duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-5 data-[state=open]:slide-in-from-top-5",
|
||||
header: "flex justify-between items-center p-3 border-b border-b-border-low",
|
||||
title: "text-sm font-medium text-txt-primary",
|
||||
footer: "flex justify-end items-center p-3 border-t border-t-border-low gap-2",
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
trigger?: ReactNode;
|
||||
title: ReactNode;
|
||||
title?: ReactNode;
|
||||
children?: ReactNode;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
ref?: RefObject<{ open: () => void; close: () => void } | null>;
|
||||
};
|
||||
|
||||
export const useDialogRef = () => {
|
||||
return useRef<{ open: () => void; close: () => void } | null>(null);
|
||||
};
|
||||
|
||||
export function Dialog({
|
||||
trigger,
|
||||
title,
|
||||
children,
|
||||
onOpenChange,
|
||||
open,
|
||||
className,
|
||||
ref,
|
||||
defaultOpen,
|
||||
}: Props) {
|
||||
const { overlay, content, header, title: titleClassname } = dialog();
|
||||
const [open, setOpen] = useState(!!defaultOpen);
|
||||
|
||||
if (ref) {
|
||||
ref.current = {
|
||||
open() {
|
||||
setOpen(true);
|
||||
},
|
||||
close() {
|
||||
setOpen(false);
|
||||
},
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Root open={open} onOpenChange={onOpenChange}>
|
||||
<Root open={open} onOpenChange={setOpen}>
|
||||
{trigger && <Trigger asChild>{trigger}</Trigger>}
|
||||
<Portal>
|
||||
<Overlay
|
||||
className={clsx(
|
||||
"fixed inset-0 z-50 bg-dialog/40",
|
||||
`duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0`,
|
||||
)}
|
||||
/>
|
||||
<Overlay className={overlay()} />
|
||||
<Content
|
||||
aria-describedby={undefined}
|
||||
className={clsx(
|
||||
"fixed inset-0 m-auto z-50 w-full h-max bg-level-2 rounded-2xl max-w-5xl w-[95%]",
|
||||
className,
|
||||
`duration-200
|
||||
data-[state=open]:animate-in data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
|
||||
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
|
||||
data-[state=closed]:slide-out-to-top-5
|
||||
data-[state=open]:slide-in-from-top-5`,
|
||||
)}
|
||||
className={content({ className })}
|
||||
>
|
||||
<div className="flex justify-between items-center p-3 border-b border-b-border-low">
|
||||
<Title className="text-sm font-medium text-txt-primary">
|
||||
{title}
|
||||
</Title>
|
||||
{title ? (
|
||||
<div className={header()}>
|
||||
<Title className={titleClassname()}> {title}</Title>
|
||||
<Close asChild>
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="tertiary"
|
||||
icon={IconCrossLargeX}
|
||||
/>
|
||||
</Close>
|
||||
</div>
|
||||
) : (
|
||||
<Close asChild>
|
||||
<Button
|
||||
tabIndex={-1}
|
||||
variant="tertiary"
|
||||
className="absolute top-4 right-4"
|
||||
icon={IconCrossLargeX}
|
||||
/>
|
||||
</Close>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</Content>
|
||||
</Portal>
|
||||
@@ -80,8 +117,9 @@ export function DialogFooter({
|
||||
exitLabel?: string;
|
||||
}) {
|
||||
const { __ } = useTranslate();
|
||||
const { footer } = dialog();
|
||||
return (
|
||||
<footer className="flex justify-end items-center p-3 border-t border-t-border-low gap-2">
|
||||
<footer className={footer()}>
|
||||
<Close asChild>
|
||||
<Button variant="secondary">{exitLabel ?? __("Cancel")}</Button>
|
||||
</Close>
|
||||
@@ -91,19 +129,51 @@ export function DialogFooter({
|
||||
}
|
||||
|
||||
export function DialogContent(
|
||||
props: HTMLAttributes<HTMLDivElement> & { padded?: boolean },
|
||||
props: HTMLAttributes<HTMLDivElement> & {
|
||||
padded?: boolean;
|
||||
scrollableChildren?: boolean;
|
||||
},
|
||||
) {
|
||||
let children = props.children;
|
||||
if (props.scrollableChildren) {
|
||||
children = Children.map(props.children, (c) => {
|
||||
if (
|
||||
isValidElement<{
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}>(c)
|
||||
) {
|
||||
return cloneElement(c, {
|
||||
...c.props,
|
||||
className: clsx(c.props.className, "overflow-y-auto"),
|
||||
style: {
|
||||
maxHeight: "var(--maxHeight)",
|
||||
...c.props.style,
|
||||
},
|
||||
});
|
||||
}
|
||||
return c;
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
children={children}
|
||||
className={clsx(
|
||||
"overflow-y-auto",
|
||||
props.className,
|
||||
props.padded && "p-6",
|
||||
)}
|
||||
style={{
|
||||
maxHeight: "min(640px, calc(100vh - 140px))",
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--maxHeight": "min(640px, calc(100vh - 140px))",
|
||||
maxHeight: "var(--maxHeight)",
|
||||
} as CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogTitle(props: ComponentProps<typeof Title>) {
|
||||
return <Title {...props} />;
|
||||
}
|
||||
|
||||
@@ -40,10 +40,19 @@ export {
|
||||
} from "./Molecules/UserDropdown/UserDropdown";
|
||||
export { PageHeader } from "./Molecules/PageHeader/PageHeader";
|
||||
export { Skeleton } from "./Atoms/Skeleton/Skeleton";
|
||||
export { Dialog, DialogContent, DialogFooter } from "./Molecules/Dialog/Dialog";
|
||||
export {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
useDialogRef,
|
||||
} from "./Molecules/Dialog/Dialog";
|
||||
export { RiskBadge } from "./Molecules/Badge/RiskBadge";
|
||||
export { SeverityBadge } from "./Molecules/Badge/SeverityBadge.tsx";
|
||||
export { ConfirmDialog } from "./Molecules/Dialog/ConfirmDialog.tsx";
|
||||
export {
|
||||
ConfirmDialog,
|
||||
useConfirmDialogRef,
|
||||
} from "./Molecules/Dialog/ConfirmDialog.tsx";
|
||||
export { RisksChart } from "./Molecules/Risks/RisksChart";
|
||||
export { RiskOverview } from "./Molecules/Risks/RiskOverview";
|
||||
export { Combobox, ComboboxItem } from "./Molecules/Combobox/Combobox";
|
||||
|
||||
@@ -244,7 +244,6 @@
|
||||
*:focus-visible {
|
||||
box-shadow: var(--shadow-focus);
|
||||
outline: none;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
||||
29
packages/ui/tailwind.config.js
Normal file
29
packages/ui/tailwind.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
typography: () => ({
|
||||
neutral: {
|
||||
css: {
|
||||
"--tw-prose-body": "var(--color-txt-secondary)",
|
||||
"--tw-prose-headings": "var(--color-txt-primary)",
|
||||
"--tw-prose-lead": "var(--color-txt-secondary)",
|
||||
"--tw-prose-links": "var(--color-txt-secondary)",
|
||||
"--tw-prose-bold": "var(--color-txt-secondary)",
|
||||
"--tw-prose-counters": "var(--color-txt-secondary)",
|
||||
"--tw-prose-bullets": "var(--color-txt-secondary)",
|
||||
"--tw-prose-hr": "var(--color-txt-secondary)",
|
||||
"--tw-prose-quotes": "var(--color-txt-secondary)",
|
||||
"--tw-prose-quote-borders": "var(--color-txt-primary)",
|
||||
"--tw-prose-captions": "var(--color-txt-secondary)",
|
||||
"--tw-prose-code": "var(--color-txt-secondary)",
|
||||
"--tw-prose-pre-code": "var(--color-txt-secondary)",
|
||||
"--tw-prose-pre-bg": "var(--color-txt-secondary)",
|
||||
"--tw-prose-th-borders": "var(--color-txt-secondary)",
|
||||
"--tw-prose-td-borders": "var(--color-txt-secondary)",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user