Refatcor policies management

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-04-28 16:03:42 -07:00
parent c208131b85
commit a7cd94956a
59 changed files with 10112 additions and 2303 deletions

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 0C6.36819 0 6.66667 0.298477 6.66667 0.666667V6.05719L7.86193 4.86193C8.12228 4.60158 8.54439 4.60158 8.80474 4.86193C9.06509 5.12228 9.06509 5.54439 8.80474 5.80474L6.4714 8.13807C6.21106 8.39842 5.78895 8.39842 5.5286 8.13807L3.19526 5.80474C2.93491 5.54439 2.93491 5.12228 3.19526 4.86193C3.45561 4.60158 3.87772 4.60158 4.13807 4.86193L5.33333 6.05719V0.666667C5.33333 0.298477 5.63181 0 6 0ZM0.666667 7.33333C1.03486 7.33333 1.33333 7.63181 1.33333 8V9.33333C1.33333 10.0697 1.93028 10.6667 2.66667 10.6667H9.33333C10.0697 10.6667 10.6667 10.0697 10.6667 9.33333V8C10.6667 7.63181 10.9651 7.33333 11.3333 7.33333C11.7015 7.33333 12 7.63181 12 8V9.33333C12 10.8061 10.8061 12 9.33333 12H2.66667C1.19392 12 0 10.8061 0 9.33333V8C0 7.63181 0.298477 7.33333 0.666667 7.33333Z" fill="#141E12"/>
</svg>

After

Width:  |  Height:  |  Size: 950 B

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.99966 1.39993C3.90702 1.39993 1.39993 3.90702 1.39993 6.99966C1.39993 10.0923 3.90701 12.5994 6.99966 12.5994C10.0923 12.5994 12.5994 10.0923 12.5994 6.99966C12.5994 3.90701 10.0923 1.39993 6.99966 1.39993ZM0 6.99966C0 3.13386 3.13386 0 6.99966 0C10.8655 0 13.9993 3.13386 13.9993 6.99966C13.9993 10.8655 10.8655 13.9993 6.99966 13.9993C3.13386 13.9993 0 10.8655 0 6.99966ZM7.69962 3.49983V6.70972L9.73947 8.74957L8.74957 9.73947L6.29969 7.28959V3.49983H7.69962Z" fill="#141E12"/>
</svg>

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -0,0 +1,5 @@
<svg width="14" height="3" viewBox="0 0 14 3" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.59993 1.39998C5.59993 0.626783 6.22671 0 6.99991 0C7.77311 0 8.3999 0.626783 8.3999 1.39998C8.3999 2.17318 7.77311 2.79997 6.99991 2.79997C6.22671 2.79997 5.59993 2.17318 5.59993 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1999 1.39998C11.1999 0.626783 11.8266 0 12.5998 0C13.373 0 13.9998 0.626783 13.9998 1.39998C13.9998 2.17318 13.373 2.79997 12.5998 2.79997C11.8266 2.79997 11.1999 2.17318 11.1999 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1.39998C0 0.626774 0.626806 0 1.39998 0C2.17316 0 2.79997 0.626774 2.79997 1.39998C2.79997 2.17319 2.17316 2.79997 1.39998 2.79997C0.626806 2.79997 0 2.17319 0 1.39998Z" fill="#141E12"/>
</svg>

After

Width:  |  Height:  |  Size: 857 B

View File

@@ -0,0 +1,5 @@
<svg width="14" height="3" viewBox="0 0 14 3" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.59993 1.39998C5.59993 0.626783 6.22671 0 6.99991 0C7.77311 0 8.3999 0.626783 8.3999 1.39998C8.3999 2.17318 7.77311 2.79997 6.99991 2.79997C6.22671 2.79997 5.59993 2.17318 5.59993 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1999 1.39998C11.1999 0.626783 11.8266 0 12.5998 0C13.373 0 13.9998 0.626783 13.9998 1.39998C13.9998 2.17318 13.373 2.79997 12.5998 2.79997C11.8266 2.79997 11.1999 2.17318 11.1999 1.39998Z" fill="#141E12"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1.39998C0 0.626774 0.626806 0 1.39998 0C2.17316 0 2.79997 0.626774 2.79997 1.39998C2.79997 2.17319 2.17316 2.79997 1.39998 2.79997C0.626806 2.79997 0 2.17319 0 1.39998Z" fill="#141E12"/>
</svg>

After

Width:  |  Height:  |  Size: 857 B

View File

@@ -290,7 +290,7 @@ function BreadcrumbPolicyOverview() {
policy: node(id: $policyId) {
id
... on Policy {
name
title
}
}
}
@@ -306,7 +306,7 @@ function BreadcrumbPolicyOverview() {
<BreadcrumbNavLink
to={`/organizations/${organizationId}/policies/${policyId}`}
>
{data.policy?.name}
{data.policy?.title}
</BreadcrumbNavLink>
</BreadcrumbItem>
<Outlet />

View File

@@ -19,9 +19,8 @@ import { NewPeoplePage } from "./people/NewPeoplePage";
import { PeopleListPage } from "./people/PeopleListPage";
import { PeoplePage } from "./people/PeoplePage";
import { EditPolicyPage } from "./policies/EditPolicyPage";
import { NewPolicyPage } from "./policies/NewPolicyPage";
import { PolicyListPage } from "./policies/PolicyListPage";
import { PolicyPage } from "./policies/PolicyPage";
import { ShowPolicyPage } from "./policies/ShowPolicyPage";
import { EditRiskPage } from "./risks/EditRiskPage";
import { NewRiskPage } from "./risks/NewRiskPage";
import { ListRiskPage } from "./risks/ListRiskPage";
@@ -54,9 +53,8 @@ export function OrganizationsRoutes() {
<Route path="mesures/:mesureId/edit" element={<EditMesurePage />} />
<Route path="vendors/:vendorId" element={<VendorPage />} />
<Route path="policies" element={<PolicyListPage />} />
<Route path="policies/new" element={<NewPolicyPage />} />
<Route path="policies/:policyId" element={<PolicyPage />} />
<Route path="policies/:policyId/edit" element={<EditPolicyPage />} />
<Route path="policies/:policyId" element={<ShowPolicyPage />} />
<Route path="policies/:policyId/versions/:versionId/edit" element={<EditPolicyPage />} />
<Route path="risks" element={<ListRiskPage />} />
<Route path="risks/new" element={<NewRiskPage />} />
<Route path="risks/:riskId" element={<ShowRiskView />} />

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5f81eaad8c66c579401d9c73180f8f91>>
* @generated SignedSource<<ca410ce7df7231383e55d134d37916bd>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,7 +15,7 @@ export type OrganizationBreadcrumbBreadcrumbPolicyOverviewQuery$variables = {
export type OrganizationBreadcrumbBreadcrumbPolicyOverviewQuery$data = {
readonly policy: {
readonly id: string;
readonly name?: string;
readonly title?: string;
};
};
export type OrganizationBreadcrumbBreadcrumbPolicyOverviewQuery = {
@@ -52,7 +52,7 @@ v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "title",
"storageKey": null
}
],
@@ -112,16 +112,16 @@ return {
]
},
"params": {
"cacheID": "13d6ac9cf6f904f8a03f141b6b6e2564",
"cacheID": "bc0b04bbe75c94660de230461532773a",
"id": null,
"metadata": {},
"name": "OrganizationBreadcrumbBreadcrumbPolicyOverviewQuery",
"operationKind": "query",
"text": "query OrganizationBreadcrumbBreadcrumbPolicyOverviewQuery(\n $policyId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n }\n }\n}\n"
"text": "query OrganizationBreadcrumbBreadcrumbPolicyOverviewQuery(\n $policyId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n title\n }\n }\n}\n"
}
};
})();
(node as any).hash = "6ed45dd2d1fa369329edde6b77eec92d";
(node as any).hash = "ced380d1e6eb1c31ecc5f2dc9096133b";
export default node;

View File

@@ -106,10 +106,15 @@ const linkedPoliciesQuery = graphql`
edges {
node {
id
name
content
status
reviewDate
title
description
currentPublishedVersion
createdAt
updatedAt
owner {
id
fullName
}
}
}
}
@@ -128,10 +133,15 @@ const organizationPoliciesQuery = graphql`
edges {
node {
id
name
content
status
reviewDate
title
description
currentPublishedVersion
createdAt
updatedAt
owner {
id
fullName
}
}
}
}
@@ -582,7 +592,7 @@ export function Control({
return policies.filter((policy) => {
return (
!policySearchQuery ||
policy.name.toLowerCase().includes(policySearchQuery.toLowerCase()) ||
policy.name?.toLowerCase().includes(policySearchQuery.toLowerCase()) ||
(policy.content &&
policy.content
.toLowerCase()
@@ -1066,10 +1076,7 @@ export function Control({
<thead className="sticky top-0 bg-white">
<tr className="border-b text-left text-sm text-secondary bg-invert-bg">
<th className="py-3 px-4 font-medium">Name</th>
<th className="py-3 px-4 font-medium">Status</th>
<th className="py-3 px-4 font-medium">
Review Date
</th>
<th className="py-3 px-4 font-medium">Review Date</th>
<th className="py-3 px-4 font-medium text-right">
Actions
</th>
@@ -1093,17 +1100,6 @@ export function Control({
</div>
)}
</td>
<td className="py-3 px-4">
<div
className={`px-2 py-0.5 rounded-full text-xs ${
policy.status === "ACTIVE"
? "bg-success-bg text-success"
: "bg-secondary-bg text-secondary"
} inline-block`}
>
{policy.status}
</div>
</td>
<td className="py-3 px-4">
{policy.reviewDate
? new Date(
@@ -1137,7 +1133,7 @@ export function Control({
handleLinkPolicy(policy.id)
}
disabled={isLinkingPolicy}
className="text-xs h-7 text-info border-info-b hover:bg-h-info-bg"
className="text-xs h-7 text-info border-info-b hover:bg-h-info-bg"
>
{isLinkingPolicy ? (
<Loader2 className="w-4 h-4 animate-spin" />
@@ -1193,7 +1189,6 @@ export function Control({
<thead>
<tr className="border-b text-left text-sm text-secondary bg-invert-bg">
<th className="py-3 px-4 font-medium">Name</th>
<th className="py-3 px-4 font-medium">Status</th>
<th className="py-3 px-4 font-medium">Review Date</th>
<th className="py-3 px-4 font-medium text-right">
Actions
@@ -1214,17 +1209,6 @@ export function Control({
</div>
)}
</td>
<td className="py-3 px-4">
<div
className={`px-2 py-0.5 rounded-full text-xs ${
policy.status === "ACTIVE"
? "bg-success-bg text-success"
: "bg-secondary-bg text-secondary"
} inline-block`}
>
{policy.status}
</div>
</td>
<td className="py-3 px-4">
{policy.reviewDate
? new Date(policy.reviewDate).toLocaleDateString()

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9bddb18bf52f14881a85995793bdc1ca>>
* @generated SignedSource<<85bdd7f83672d1a88247d958f4b7dfaf>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type ControlLinkedPoliciesQuery$variables = {
controlId: string;
};
@@ -19,11 +18,16 @@ export type ControlLinkedPoliciesQuery$data = {
readonly policies?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly content: string;
readonly createdAt: string;
readonly currentPublishedVersion: number | null | undefined;
readonly description: string;
readonly id: string;
readonly name: string;
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly title: string;
readonly updatedAt: string;
};
}>;
};
@@ -85,28 +89,54 @@ v4 = [
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"name": "currentPublishedVersion",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"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
},
(v3/*: any*/)
@@ -244,7 +274,7 @@ return {
]
},
"params": {
"cacheID": "67f9bf1d5f255f5406e932abde540e08",
"cacheID": "24c5ce380772f28348df8cf089a58a8e",
"id": null,
"metadata": {
"connection": [
@@ -261,11 +291,11 @@ return {
},
"name": "ControlLinkedPoliciesQuery",
"operationKind": "query",
"text": "query ControlLinkedPoliciesQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n status\n reviewDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query ControlLinkedPoliciesQuery(\n $controlId: ID!\n) {\n control: node(id: $controlId) {\n __typename\n id\n ... on Control {\n policies(first: 100) {\n edges {\n node {\n id\n title\n description\n currentPublishedVersion\n createdAt\n updatedAt\n owner {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "3904c2344754177177fa7c16129dc894";
(node as any).hash = "d4d16cac2a05ea1d38fde55d04f21c01";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<cfe85b9fdabaf24f9a7f575eca8a6417>>
* @generated SignedSource<<1f9de20276a5c0b1291c5e25dc11990b>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type ControlOrganizationPoliciesQuery$variables = {
organizationId: string;
};
@@ -19,11 +18,16 @@ export type ControlOrganizationPoliciesQuery$data = {
readonly policies?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly content: string;
readonly createdAt: string;
readonly currentPublishedVersion: number | null | undefined;
readonly description: string;
readonly id: string;
readonly name: string;
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly title: string;
readonly updatedAt: string;
};
}>;
};
@@ -85,28 +89,54 @@ v4 = [
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"name": "currentPublishedVersion",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"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
},
(v3/*: any*/)
@@ -244,7 +274,7 @@ return {
]
},
"params": {
"cacheID": "3a3c91f3e18e8a4b66011aa583e262e4",
"cacheID": "94e32c7758c8df264b99a44150de687d",
"id": null,
"metadata": {
"connection": [
@@ -261,11 +291,11 @@ return {
},
"name": "ControlOrganizationPoliciesQuery",
"operationKind": "query",
"text": "query ControlOrganizationPoliciesQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n status\n reviewDate\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query ControlOrganizationPoliciesQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n title\n description\n currentPublishedVersion\n createdAt\n updatedAt\n owner {\n id\n fullName\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "f4699d729e6df6192efd201047689204";
(node as any).hash = "47aaf55ffb65d3220df2b0fe2688bc12";
export default node;

View File

@@ -10,27 +10,30 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { toast } from "@/hooks/use-toast";
import { Calendar, User } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Suspense } from "react";
import PolicyEditor from "@/components/PolicyEditor";
import PeopleSelector from "@/components/PeopleSelector";
import type { EditPolicyViewQuery } from "./__generated__/EditPolicyViewQuery.graphql";
import type { EditPolicyViewMutation as EditPolicyViewMutationType } from "./__generated__/EditPolicyViewMutation.graphql";
import { PageTemplate } from "@/components/PageTemplate";
import { EditPolicyViewSkeleton } from "./EditPolicyPage";
import { User } from "lucide-react";
const editPolicyViewQuery = graphql`
query EditPolicyViewQuery($policyId: ID!, $organizationId: ID!) {
query EditPolicyViewQuery($policyId: ID!, $organizationId: ID!, $policyVersionId: ID!) {
policyVersion: node(id: $policyVersionId) {
id
... on PolicyVersion {
content
}
}
policy: node(id: $policyId) {
id
... on Policy {
name
content
status
reviewDate
title
owner {
id
fullName
@@ -44,18 +47,11 @@ const editPolicyViewQuery = graphql`
`;
const UpdatePolicyMutation = graphql`
mutation EditPolicyViewMutation($input: UpdatePolicyInput!) {
updatePolicy(input: $input) {
policy {
mutation EditPolicyViewMutation($input: UpdatePolicyVersionInput!) {
updatePolicyVersion(input: $input) {
policyVersion {
id
name
content
status
reviewDate
owner {
id
fullName
}
}
}
}
@@ -67,56 +63,28 @@ function EditPolicyViewContent({
queryRef: PreloadedQuery<EditPolicyViewQuery>;
}) {
const navigate = useNavigate();
const { organizationId, policyId } = useParams();
const { organizationId, policyId, versionId } = useParams();
const data = usePreloadedQuery<EditPolicyViewQuery>(
editPolicyViewQuery,
queryRef
);
console.log("EditPolicyView data:", data.policy);
const [content, setContent] = useState(data.policyVersion.content || "");
const [name, setName] = useState(data.policy.name);
const [content, setContent] = useState(data.policy.content || "");
const [status, setStatus] = useState(data.policy.status);
const [reviewDate, setReviewDate] = useState(data.policy.reviewDate || "");
const [ownerId, setOwnerId] = useState<string | null>(
data.policy.owner?.id || null
);
const [isSubmitting, setIsSubmitting] = useState(false);
console.log(
"EditPolicyView state - content:",
content
? content.substring(0, 50) + (content.length > 50 ? "..." : "")
: "empty"
);
const [commitMutation] =
const [updatePolicy, isSubmitting] =
useMutation<EditPolicyViewMutationType>(UpdatePolicyMutation);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Convert reviewDate string to ISO format for the API
let reviewDateValue = null;
if (reviewDate) {
reviewDateValue = new Date(reviewDate).toISOString();
}
commitMutation({
updatePolicy({
variables: {
input: {
id: data.policy.id,
name,
policyVersionId: data.policyVersion.id,
content,
status,
reviewDate: reviewDateValue,
ownerId,
},
},
onCompleted: (response, errors) => {
setIsSubmitting(false);
onCompleted: (_, errors) => {
if (errors) {
console.error("Error updating policy:", errors);
toast({
@@ -134,7 +102,6 @@ function EditPolicyViewContent({
navigate(`/organizations/${organizationId}/policies/${policyId}`);
},
onError: (error) => {
setIsSubmitting(false);
console.error("Error updating policy:", error);
toast({
title: "Error",
@@ -153,77 +120,20 @@ function EditPolicyViewContent({
<CardTitle>Policy Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Policy Name</Label>
<Input
id="name"
placeholder="Enter policy name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="content">Policy Content</Label>
<div className="min-h-[300px]">
<PolicyEditor
initialContent={content}
onChange={(html) => setContent(html)}
<div className="min-h-[500px]">
<Textarea
id="content"
placeholder="Enter policy description"
value={content}
onChange={(e) => setContent(e.target.value)}
required
rows={20}
className="min-h-[500px] resize-y"
/>
</div>
</div>
<div className="space-y-2">
<Label>Status</Label>
<RadioGroup
value={status}
onValueChange={(value) =>
setStatus(value as "DRAFT" | "ACTIVE")
}
className="flex space-x-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="DRAFT" id="draft" />
<Label htmlFor="draft" className="cursor-pointer">
Draft
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="ACTIVE" id="active" />
<Label htmlFor="active" className="cursor-pointer">
Active
</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label htmlFor="owner" className="flex items-center gap-2">
<User className="h-4 w-4" />
Policy Owner
</Label>
<PeopleSelector
organizationRef={data.organization}
selectedPersonId={ownerId}
onSelect={setOwnerId}
placeholder="Select policy owner"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="reviewDate" className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Review Date
</Label>
<Input
id="reviewDate"
type="date"
value={reviewDate}
onChange={(e) => setReviewDate(e.target.value)}
/>
</div>
</CardContent>
</Card>
@@ -250,15 +160,17 @@ function EditPolicyViewContent({
}
export default function EditPolicyView() {
const { organizationId, policyId } = useParams();
const { organizationId, policyId, versionId } = useParams();
const [queryRef, loadQuery] =
useQueryLoader<EditPolicyViewQuery>(editPolicyViewQuery);
useEffect(() => {
if (organizationId && policyId) {
loadQuery({ organizationId, policyId });
}
}, [organizationId, policyId, loadQuery]);
loadQuery({
organizationId: organizationId!,
policyId: policyId!,
policyVersionId: versionId!,
});
}, [organizationId, policyId, versionId]);
if (!queryRef) {
return <EditPolicyViewSkeleton />;
@@ -266,7 +178,7 @@ export default function EditPolicyView() {
return (
<Suspense fallback={<EditPolicyViewSkeleton />}>
{queryRef && <EditPolicyViewContent queryRef={queryRef} />}
<EditPolicyViewContent queryRef={queryRef}/>
</Suspense>
);
}

View File

@@ -1,30 +0,0 @@
import { PageTemplateSkeleton } from "@/components/PageTemplate";
import { Suspense } from "react";
import { useLocation } from "react-router";
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
import { lazy } from "@probo/react-lazy";
const NewPolicyView = lazy(() => import("./NewPolicyView"));
export function NewPolicyViewSkeleton() {
return (
<PageTemplateSkeleton
title="New Policy"
description="Add a new policy for your organization"
>
<div className="bg-subtle-bg animate-pulse rounded-lg h-[600px]" />
</PageTemplateSkeleton>
);
}
export function NewPolicyPage() {
const location = useLocation();
return (
<Suspense key={location.pathname} fallback={<NewPolicyViewSkeleton />}>
<ErrorBoundaryWithLocation>
<NewPolicyView />
</ErrorBoundaryWithLocation>
</Suspense>
);
}

View File

@@ -1,281 +0,0 @@
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router";
import {
ConnectionHandler,
graphql,
useMutation,
useQueryLoader,
usePreloadedQuery,
PreloadedQuery,
} from "react-relay";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { Calendar, User } from "lucide-react";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import PolicyEditor from "@/components/PolicyEditor";
import PeopleSelector from "@/components/PeopleSelector";
import { Suspense } from "react";
import type { NewPolicyViewMutation } from "./__generated__/NewPolicyViewMutation.graphql";
import type { NewPolicyViewQuery } from "./__generated__/NewPolicyViewQuery.graphql";
import { PageTemplate } from "@/components/PageTemplate";
import { NewPolicyViewSkeleton } from "./NewPolicyPage";
const newPolicyQuery = graphql`
query NewPolicyViewQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
...PeopleSelector_organization
}
}
`;
const CreatePolicyMutation = graphql`
mutation NewPolicyViewMutation(
$input: CreatePolicyInput!
$connections: [ID!]!
) {
createPolicy(input: $input) {
policyEdge @prependEdge(connections: $connections) {
node {
id
name
content
status
reviewDate
owner {
id
fullName
}
}
}
}
}
`;
function CreatePolicyForm({
queryRef,
}: {
queryRef: PreloadedQuery<NewPolicyViewQuery>;
}) {
const navigate = useNavigate();
const { organizationId } = useParams();
const data = usePreloadedQuery<NewPolicyViewQuery>(newPolicyQuery, queryRef);
const [name, setName] = useState("");
const [content, setContent] = useState("");
const [status, setStatus] = useState<"DRAFT" | "ACTIVE">("DRAFT");
const [reviewDate, setReviewDate] = useState("");
const [ownerId, setOwnerId] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { toast } = useToast();
console.log(
"NewPolicyView state - content:",
content
? content.substring(0, 50) + (content.length > 50 ? "..." : "")
: "empty"
);
const [createPolicy] =
useMutation<NewPolicyViewMutation>(CreatePolicyMutation);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!ownerId) {
toast({
title: "Error",
description: "Please select an owner for the policy.",
variant: "destructive",
});
return;
}
setIsSubmitting(true);
// Convert reviewDate string to ISO format for the API
let reviewDateValue = null;
if (reviewDate) {
reviewDateValue = new Date(reviewDate).toISOString();
}
const input = {
organizationId: organizationId!,
name,
content,
status,
reviewDate: reviewDateValue,
ownerId,
};
createPolicy({
variables: {
input,
connections: [
ConnectionHandler.getConnectionID(
organizationId!,
"PolicyListView_policies"
),
],
},
onCompleted: (response, errors) => {
setIsSubmitting(false);
if (errors) {
console.error("Error creating policy:", errors);
toast({
title: "Error",
description: "Failed to create policy. Please try again.",
variant: "destructive",
});
return;
}
toast({
title: "Success",
description: "Policy created successfully!",
});
navigate(
`/organizations/${organizationId}/policies/${response.createPolicy.policyEdge.node.id}`
);
},
onError: (error) => {
setIsSubmitting(false);
console.error("Error creating policy:", error);
toast({
title: "Error",
description: "Failed to create policy. Please try again.",
variant: "destructive",
});
},
});
};
return (
<PageTemplate
title="Create Policy"
description="Create a new policy for your organization"
>
<form onSubmit={handleSubmit}>
<div className="grid gap-6">
<Card>
<CardHeader>
<CardTitle>Policy Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Policy Name</Label>
<Input
id="name"
placeholder="Enter policy name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="content">Policy Content</Label>
<div className="min-h-[300px]">
<PolicyEditor
initialContent={content}
onChange={(html) => setContent(html)}
/>
</div>
</div>
<div className="space-y-2">
<Label>Status</Label>
<RadioGroup
value={status}
onValueChange={(value: "DRAFT" | "ACTIVE") =>
setStatus(value)
}
className="flex space-x-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="DRAFT" id="draft" />
<Label htmlFor="draft" className="cursor-pointer">
Draft
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="ACTIVE" id="active" />
<Label htmlFor="active" className="cursor-pointer">
Active
</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label htmlFor="owner" className="flex items-center gap-2">
<User className="h-4 w-4" />
Policy Owner
</Label>
<PeopleSelector
organizationRef={data.organization}
selectedPersonId={ownerId}
onSelect={setOwnerId}
placeholder="Select policy owner"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="reviewDate" className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
Review Date
</Label>
<Input
id="reviewDate"
type="date"
value={reviewDate}
onChange={(e) => setReviewDate(e.target.value)}
/>
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={() =>
navigate(`/organizations/${organizationId}/policies`)
}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Creating..." : "Create Policy"}
</Button>
</div>
</div>
</form>
</PageTemplate>
);
}
export default function NewPolicyView() {
const { organizationId } = useParams();
const [queryRef, loadQuery] =
useQueryLoader<NewPolicyViewQuery>(newPolicyQuery);
useEffect(() => {
if (organizationId) {
loadQuery({ organizationId });
}
}, [organizationId, loadQuery]);
if (!queryRef) {
return <NewPolicyViewSkeleton />;
}
return (
<Suspense fallback={<NewPolicyViewSkeleton />}>
{<CreatePolicyForm queryRef={queryRef} />}
</Suspense>
);
}

View File

@@ -15,6 +15,7 @@ import {
MoreHorizontal,
Trash2,
Eye,
X,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import {
@@ -24,24 +25,59 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useToast } from "@/hooks/use-toast";
import type { PolicyListViewQuery as PolicyListViewQueryType } from "./__generated__/PolicyListViewQuery.graphql";
import { format } from "date-fns";
import type { PolicyListViewQuery, PolicyListViewQuery$data } from "./__generated__/PolicyListViewQuery.graphql";
import type { PolicyListViewDeleteMutation } from "./__generated__/PolicyListViewDeleteMutation.graphql";
import type { PolicyListViewCreateMutation } from "./__generated__/PolicyListViewCreateMutation.graphql";
import { PageTemplate } from "@/components/PageTemplate";
import { PolicyListViewSkeleton } from "./PolicyListPage";
import {
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Avatar } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import PeopleSelector from "@/components/PeopleSelector";
import type { PeopleSelector_organization$key } from "@/components/__generated__/PeopleSelector_organization.graphql";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
const PolicyListViewQuery = graphql`
const policyListViewQuery = graphql`
query PolicyListViewQuery($organizationId: ID!) {
viewer {
user {
id
}
}
organization: node(id: $organizationId) {
... on Organization {
policies(first: 100) @connection(key: "PolicyListView_policies") {
...PeopleSelector_organization
policies(first: 50, orderBy: {field: TITLE, direction: ASC}) @connection(key: "PolicyListView_policies") {
edges {
node {
id
name
content
title
description
currentPublishedVersion
createdAt
updatedAt
status
owner {
id
fullName
}
versions(first: 1) {
edges {
node {
id
status
updatedAt
}
}
}
}
}
}
@@ -61,29 +97,47 @@ const DeletePolicyMutation = graphql`
}
`;
const createPolicyMutation = graphql`
mutation PolicyListViewCreateMutation(
$input: CreatePolicyInput!
$connections: [ID!]!
) {
createPolicy(input: $input) {
policyEdge @prependEdge(connections: $connections) {
node {
id
title
description
createdAt
updatedAt
owner {
id
fullName
}
}
}
}
}
`;
function PolicyTableRow({
policy,
organizationId,
}: {
policy: {
id: string;
name: string;
content?: string;
status?: string;
updatedAt: string;
};
policy: NonNullable<PolicyListViewQuery$data["organization"]["policies"]>["edges"][0]["node"];
organizationId: string;
}) {
const navigate = useNavigate();
const { toast } = useToast();
const [isDeleting, setIsDeleting] = useState(false);
const [commitDeleteMutation] = useMutation<PolicyListViewDeleteMutation>(DeletePolicyMutation);
const [deletePolicy, isDeleting] = useMutation<PolicyListViewDeleteMutation>(DeletePolicyMutation);
const handleDelete = (e: React.MouseEvent) => {
const latestVersion = policy.versions?.edges[0]?.node;
const status = latestVersion?.status || "DRAFT";
const handleDeletePolicy = (e: React.MouseEvent) => {
e.stopPropagation();
if (window.confirm("Are you sure you want to delete this policy? This action cannot be undone.")) {
setIsDeleting(true);
commitDeleteMutation({
deletePolicy({
variables: {
input: {
policyId: policy.id,
@@ -96,7 +150,6 @@ function PolicyTableRow({
],
},
onCompleted: (_, errors) => {
setIsDeleting(false);
if (errors) {
console.error("Error deleting policy:", errors);
toast({
@@ -112,7 +165,6 @@ function PolicyTableRow({
});
},
onError: (error) => {
setIsDeleting(false);
console.error("Error deleting policy:", error);
toast({
title: "Error",
@@ -131,27 +183,33 @@ function PolicyTableRow({
return (
<tr
className="border-t border-[#ECEFEC] hover:bg-[rgba(5,77,5,0.01)] cursor-pointer"
className="border-t border-solid-b hover:bg-subtle-bg cursor-pointer"
onClick={() => {
navigate(`/organizations/${organizationId}/policies/${policy.id}`);
}}
>
<td className="py-4 px-6">
<div className="flex flex-col">
<span className="font-medium text-[#141E12]">{policy.name}</span>
<span className="text-sm text-[#818780]">
Description
<span className="font-medium text-primary">{policy.title}</span>
<span className="text-sm text-tertiary">
{policy.description || "No description provided"}
</span>
</div>
</td>
<td className="py-4 px-6">
<span className="text-sm text-[#141E12]">Mon, 8 Mar. 2025</span>
<span className="text-sm text-primary">
{format(new Date(policy.updatedAt), "MMM d, yyyy")}
</span>
</td>
<td className="py-4 px-6">
<Badge
className="bg-[rgba(5,77,5,0.03)] text-[#6B716A] font-medium border-0 py-0 px-[6px] h-5 text-xs rounded-md"
className={`font-medium border-0 py-0 px-[6px] h-5 text-xs rounded-md ${
status === "PUBLISHED"
? "bg-success-bg text-success"
: "bg-secondary-bg text-tertiary"
}`}
>
Draft
{status === "PUBLISHED" ? "Published" : "Draft"}
</Badge>
</td>
<td className="py-4 px-6 text-right">
@@ -172,8 +230,8 @@ function PolicyTableRow({
View
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleDelete}
className="text-red-600 focus:text-red-600 focus:bg-red-50"
onClick={handleDeletePolicy}
className="text-danger focus:text-danger focus:bg-danger-bg"
disabled={isDeleting}
>
<Trash2 className="mr-2 h-4 w-4" />
@@ -186,60 +244,264 @@ function PolicyTableRow({
);
}
function CreatePolicyModal({
open,
onOpenChange,
organizationId,
organizationRef,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
organizationId: string;
organizationRef: PeopleSelector_organization$key;
}) {
const { toast } = useToast();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [ownerId, setOwnerId] = useState<string | null>(null);
const [createPolicy, isCreating] = useMutation<PolicyListViewCreateMutation>(createPolicyMutation);
// Reset form fields
const resetForm = () => {
setTitle("");
setContent("");
setOwnerId(null);
};
const handleCreatePolicy = () => {
if (!title.trim()) {
toast({
title: "Error",
description: "Please enter a policy title.",
variant: "destructive",
});
return;
}
if (!ownerId) {
toast({
title: "Error",
description: "Please select an owner for the policy.",
variant: "destructive",
});
return;
}
const input = {
organizationId,
title,
content,
ownerId,
};
createPolicy({
variables: {
input,
connections: [
ConnectionHandler.getConnectionID(
organizationId,
"PolicyListView_policies",
{orderBy: {field: "TITLE", direction: "ASC"}}
),
],
},
onCompleted: (response, errors) => {
if (errors) {
console.error("Error creating policy:", errors);
toast({
title: "Error",
description: "Failed to create policy. Please try again.",
variant: "destructive",
});
return;
}
toast({
title: "Success",
description: "Policy created successfully!",
});
resetForm();
onOpenChange(false);
},
onError: (error) => {
console.error("Error creating policy:", error);
toast({
title: "Error",
description: "Failed to create policy. Please try again.",
variant: "destructive",
});
},
});
};
// Reset form when modal closes
useEffect(() => {
if (!open) {
resetForm();
}
}, [open]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[1080px] h-[700px] max-h-[700px] p-0 gap-0 flex flex-col">
<DialogTitle className="sr-only">Create new policy</DialogTitle>
<DialogDescription className="sr-only">Form to create a new policy with title, content, and owner information.</DialogDescription>
<div className="flex justify-between items-center py-2 px-4 border-b border-solid-b h-[40px]">
<div className="flex items-center gap-1 text-sm">
<span className="text-tertiary">Policies</span>
<ChevronDown className="h-3 w-3 text-quaternary rotate-[270deg]" />
<span className="font-medium">New policy</span>
</div>
</div>
<div className="flex flex-1 overflow-hidden">
<div className="flex-1 p-6 overflow-y-auto">
<div className="mb-6">
<h1
className={`text-4xl leading-tight font-bold outline-none focus:outline-none ${!title ? 'text-gray-400' : 'text-black'}`}
contentEditable
suppressContentEditableWarning
onBlur={(e) => setTitle(e.currentTarget.textContent || "")}
style={{ WebkitTapHighlightColor: 'transparent' }}
onClick={(e) => {
if (!title) {
e.currentTarget.textContent = '';
}
}}
onFocus={(e) => {
if (!title) {
e.currentTarget.textContent = '';
}
}}
>
{title || "Enter policy title..."}
</h1>
</div>
<Textarea
placeholder="This Privacy Policy outlines how NovaSoft collects, uses, and protects personal information provided by users of its services. By accessing or using our platform, you agree to the collection and use of information in accordance with this policy..."
className="min-h-[300px] border-none resize-none p-0 focus-visible:ring-0 focus-visible:ring-offset-0"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</div>
<div className="w-[420px] bg-[rgba(5,77,5,0.03)] p-6 flex flex-col gap-4">
<h3 className="font-medium text-base">Properties</h3>
<div className="flex flex-col gap-4">
<div className="flex justify-between items-center py-2 border-t border-[rgba(2,42,2,0.08)]">
<span className="text-sm font-medium text-tertiary">Status</span>
<div className="bg-[rgba(0,39,0,0.05)] py-1.5 px-2 rounded-lg">
<span className="text-sm font-medium">Draft</span>
</div>
</div>
<div className="flex justify-between items-center py-2 border-t border-[rgba(2,42,2,0.08)]">
<span className="text-sm font-medium text-tertiary">Owner</span>
<div className="relative">
<PeopleSelector
organizationRef={organizationRef}
selectedPersonId={ownerId}
onSelect={setOwnerId}
placeholder="Select owner"
/>
</div>
</div>
<div className="flex justify-between items-center py-2 border-t border-[rgba(2,42,2,0.08)]">
<span className="text-sm font-medium text-tertiary">Review date</span>
<Button size="icon" variant="outline" className="h-8 w-8 rounded-full bg-[rgba(0,39,0,0.05)] border-none">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
<DialogFooter className="flex justify-end items-center gap-3 px-4 py-2 border-t border-solid-b h-[60px]">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="h-9"
>
Cancel
</Button>
<Button
onClick={handleCreatePolicy}
disabled={isCreating}
className="h-9"
>
{isCreating ? "Creating..." : "Create policy"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function PolicyListViewContent({
queryRef,
}: {
queryRef: PreloadedQuery<PolicyListViewQueryType>;
queryRef: PreloadedQuery<PolicyListViewQuery>;
}) {
const data = usePreloadedQuery<PolicyListViewQueryType>(
PolicyListViewQuery,
const data = usePreloadedQuery<PolicyListViewQuery>(
policyListViewQuery,
queryRef
);
const { organizationId } = useParams();
const policies =
data.organization.policies?.edges.map((edge) => edge?.node) ?? [];
data.organization?.policies?.edges.map((edge) => edge?.node) ?? [];
const [createModalOpen, setCreateModalOpen] = useState(false);
const handleOpenModal = () => {
setCreateModalOpen(true);
};
const handleCloseModal = (open: boolean) => {
setCreateModalOpen(open);
};
return (
<PageTemplate
title="Policies"
actions={
<Button asChild>
<Link to={`/organizations/${organizationId}/policies/new`}>
<Plus className="mr-2 h-4 w-4" />
Create policy
</Link>
<Button onClick={handleOpenModal}>
<Plus className="mr-2 h-4 w-4" />
New policy
</Button>
}
>
{/* Policy table */}
<div className="rounded-lg border border-[#ECEFEC] overflow-hidden bg-white">
<div className="rounded-lg border border-solid-b overflow-hidden bg-level-1">
<table className="w-full">
<thead>
<tr className="bg-white text-left">
<th className="py-3 px-6 text-xs font-medium text-[#818780] border-b border-[rgba(2,42,2,0.08)]">
<tr className="bg-level-1 text-left">
<th className="py-3 px-6 text-xs font-medium text-tertiary border-b border-low-b">
<div className="flex items-center gap-1">
Vendor
<ChevronDown className="h-3 w-3 text-[#C3C8C2]" />
Policy
<ChevronDown className="h-3 w-3 text-quaternary" />
</div>
</th>
<th className="py-3 px-6 text-xs font-medium text-[#818780] border-b border-[rgba(2,42,2,0.08)]">
<th className="py-3 px-6 text-xs font-medium text-tertiary border-b border-low-b">
<div className="flex items-center gap-1">
Last update
<ChevronDown className="h-3 w-3 text-[#C3C8C2]" />
<ChevronDown className="h-3 w-3 text-quaternary" />
</div>
</th>
<th className="py-3 px-6 text-xs font-medium text-[#818780] border-b border-[rgba(2,42,2,0.08)]">
<th className="py-3 px-6 text-xs font-medium text-tertiary border-b border-low-b">
<div className="flex items-center gap-1">
Status
<ChevronDown className="h-3 w-3 text-[#C3C8C2]" />
<ChevronDown className="h-3 w-3 text-quaternary" />
</div>
</th>
<th className="py-3 px-6 text-right text-xs font-medium text-[#818780] border-b border-[rgba(2,42,2,0.08)]"></th>
<th className="py-3 px-6 text-right text-xs font-medium text-tertiary border-b border-low-b"></th>
</tr>
</thead>
<tbody className="bg-white">
<tbody className="bg-level-1">
{policies.length > 0 ? (
policies.map((policy) => (
policies.map((policy: any) => (
<PolicyTableRow
key={policy.id}
policy={policy}
@@ -251,14 +513,12 @@ function PolicyListViewContent({
<td colSpan={4} className="py-12 text-center">
<div className="flex flex-col items-center gap-2">
<h3 className="text-lg font-medium">No policies found</h3>
<p className="text-[#818780]">
<p className="text-tertiary">
Create your first policy to get started
</p>
<Button asChild>
<Link to={`/organizations/${organizationId}/policies/new`}>
<Plus className="mr-2 h-4 w-4" />
Create policy
</Link>
<Button onClick={handleOpenModal}>
<Plus className="mr-2 h-4 w-4" />
Create policy
</Button>
</div>
</td>
@@ -267,13 +527,20 @@ function PolicyListViewContent({
</tbody>
</table>
</div>
<CreatePolicyModal
open={createModalOpen}
onOpenChange={handleCloseModal}
organizationId={organizationId!}
organizationRef={data.organization}
/>
</PageTemplate>
);
}
export default function PolicyListView() {
const [queryRef, loadQuery] =
useQueryLoader<PolicyListViewQueryType>(PolicyListViewQuery);
useQueryLoader<PolicyListViewQuery>(policyListViewQuery);
const { organizationId } = useParams();

View File

@@ -1,423 +0,0 @@
import { Suspense, useEffect, useState, useCallback } from "react";
import { useParams, Link, useNavigate } from "react-router";
import {
graphql,
PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
useMutation,
ConnectionHandler,
} from "react-relay";
import { Edit, Download, Shield, User, FileText, Calendar } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { PolicyViewQuery as PolicyViewQueryType } from "./__generated__/PolicyViewQuery.graphql";
import type { PolicyViewDeleteMutation } from "./__generated__/PolicyViewDeleteMutation.graphql";
import { useToast } from "@/hooks/use-toast";
import { PageTemplate } from "@/components/PageTemplate";
import { PolicyViewSkeleton } from "./PolicyPage";
const PolicyViewQuery = graphql`
query PolicyViewQuery($policyId: ID!) {
node(id: $policyId) {
id
... on Policy {
name
content
createdAt
updatedAt
reviewDate
status
owner {
id
fullName
primaryEmailAddress
}
}
}
}
`;
const DeletePolicyMutation = graphql`
mutation PolicyViewDeleteMutation(
$input: DeletePolicyInput!
$connections: [ID!]!
) {
deletePolicy(input: $input) {
deletedPolicyId @deleteEdge(connections: $connections)
}
}
`;
function PolicyViewContent({
queryRef,
}: {
queryRef: PreloadedQuery<PolicyViewQueryType>;
}) {
const data = usePreloadedQuery(PolicyViewQuery, queryRef);
const policy = data.node;
const { organizationId } = useParams();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState("content");
const [isDeleting, setIsDeleting] = useState(false);
const { toast } = useToast();
const [commitDeleteMutation] =
useMutation<PolicyViewDeleteMutation>(DeletePolicyMutation);
const handleDeletePolicy = useCallback(() => {
if (
window.confirm(
"Are you sure you want to delete this policy? This action cannot be undone."
)
) {
setIsDeleting(true);
commitDeleteMutation({
variables: {
input: {
policyId: policy.id,
},
connections: [
ConnectionHandler.getConnectionID(
organizationId!,
"PolicyListPage_policies"
),
],
},
onCompleted: (_, errors) => {
setIsDeleting(false);
if (errors) {
console.error("Error deleting policy:", errors);
toast({
title: "Error",
description: "Failed to delete policy. Please try again.",
variant: "destructive",
});
return;
}
toast({
title: "Success",
description: "Policy deleted successfully.",
});
navigate(`/organizations/${organizationId}/policies`);
},
onError: (error) => {
setIsDeleting(false);
console.error("Error deleting policy:", error);
toast({
title: "Error",
description: "Failed to delete policy. Please try again.",
variant: "destructive",
});
},
});
}
}, [policy.id, organizationId, commitDeleteMutation, navigate]);
// Extract a short description from the content
const getDescription = (content: string | undefined) => {
if (!content) return "No description available";
// Remove HTML tags
const withoutTags = content.replace(/<[^>]*>/g, "");
// Decode HTML entities
const decoded = withoutTags
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&nbsp;/g, " ");
// Get first paragraph or first 150 characters
const firstParagraph = decoded.split("\n\n")[0].trim();
return firstParagraph.length > 150
? firstParagraph.substring(0, 150) + "..."
: firstParagraph;
};
const formatDate = (dateString: string | undefined) => {
if (!dateString) return "N/A";
const date = new Date(dateString);
return date.toISOString().split("T")[0]; // YYYY-MM-DD format
};
return (
<PageTemplate
title={policy.name ?? ""}
description={
<div className="flex items-center gap-2 text-tertiary">
<Badge variant={policy.status === "ACTIVE" ? "default" : "outline"}>
{policy.status === "ACTIVE" ? "Active" : "Draft"}
</Badge>
{policy.owner && (
<div className="flex items-center gap-1">
<User className="h-3 w-3" />
<Link
to={`/organizations/${organizationId}/people/${policy.owner.id}`}
className="hover:underline"
>
{policy.owner.fullName}
</Link>
</div>
)}
{policy.reviewDate && (
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
<span>Review: {formatDate(policy.reviewDate)}</span>
</div>
)}
</div>
}
actions={
<div className="flex gap-2">
<Button variant="secondary" asChild>
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/edit`}
>
<Edit className="h-4 w-4 mr-2" />
Edit
</Link>
</Button>
<Button
onClick={() => {
// Logic to download policy content as PDF or text
const element = document.createElement("a");
const file = new Blob([policy.content || ""], {
type: "text/plain",
});
element.href = URL.createObjectURL(file);
element.download = `${policy.name}.txt`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button>
</div>
}
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card className="border shadow-sm">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-md bg-slate-100">
<Shield className="h-6 w-6" />
</div>
<div className="flex gap-2">
{policy.status && (
<Badge
className={`px-3 py-1 rounded-md font-medium ${
policy.status === "ACTIVE"
? "bg-success-bg text-success hover:bg-h-success-bg"
: policy.status === "DRAFT"
? "bg-warning-bg text-warning hover:bg-h-warning-bg"
: "bg-secondary-bg text-secondary hover:bg-h-secondary-bg"
}`}
>
{policy.status === "ACTIVE"
? "Active"
: policy.status === "DRAFT"
? "Draft"
: policy.status}
</Badge>
)}
</div>
</div>
<h1 className="text-3xl font-bold mb-3">{policy.name}</h1>
<p className="text-tertiary mb-6">
{getDescription(policy.content)}
</p>
<Tabs
defaultValue="content"
value={activeTab}
onValueChange={setActiveTab}
className="mb-6"
>
<TabsList className="border-b w-full rounded-none p-0 h-auto">
<TabsTrigger
value="content"
className={`px-4 py-2 font-medium ${
activeTab === "content"
? "border-primary text-primary"
: "text-tertiary"
}`}
>
Policy Content
</TabsTrigger>
<TabsTrigger
value="history"
className={`px-4 py-2 font-medium ${
activeTab === "history"
? "border-primary text-primary"
: "text-tertiary"
}`}
>
Version History
</TabsTrigger>
<TabsTrigger
value="approvals"
className={`px-4 py-2 font-medium ${
activeTab === "approvals"
? "border-primary text-primary"
: "text-tertiary"
}`}
>
Approvals
</TabsTrigger>
</TabsList>
<TabsContent value="content" className="pt-6">
<div className="prose prose-sm md:prose-base lg:prose-lg max-w-none">
<div
className="policy-content"
dangerouslySetInnerHTML={{
__html: policy.content || "",
}}
/>
</div>
</TabsContent>
<TabsContent value="history" className="pt-6">
<div className="text-center py-12">
<p className="text-tertiary">
Version history will be available soon.
</p>
</div>
</TabsContent>
<TabsContent value="approvals" className="pt-6">
<div className="text-center py-12">
<p className="text-tertiary">
Approval workflow will be available soon.
</p>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
<div className="lg:col-span-1">
<Card className="border shadow-sm mb-6">
<CardContent className="p-6">
<h2 className="text-xl font-semibold mb-6">Policy Details</h2>
<div className="space-y-6">
<div>
<div className="flex items-center gap-2 text-tertiary mb-1">
<FileText className="h-4 w-4" />
<span className="text-sm">Last Updated</span>
</div>
<p className="font-medium">{formatDate(policy.updatedAt)}</p>
</div>
<div>
<div className="flex items-center gap-2 text-tertiary mb-1">
<FileText className="h-4 w-4" />
<span className="text-sm">Review Due</span>
</div>
<p className="font-medium">
{policy.reviewDate
? formatDate(policy.reviewDate)
: "Not set"}
</p>
</div>
<div>
<div className="flex items-center gap-2 text-tertiary mb-1">
<User className="h-4 w-4" />
<span className="text-sm">Owner</span>
</div>
<Link
to={`/organizations/${organizationId}/people/${policy.owner?.id}`}
className="hover:underline"
>
<p className="font-medium">
{policy.owner ? policy.owner.fullName : "Not assigned"}
</p>
</Link>
</div>
</div>
</CardContent>
</Card>
<Button asChild className="w-full">
<Link
to={`/organizations/${organizationId}/policies/${policy.id}/edit`}
>
<Edit className="mr-2 h-4 w-4" />
Edit Policy
</Link>
</Button>
<Card className="border shadow-sm mt-6 bg-danger-bg">
<CardContent className="p-6">
<h3 className="text-danger font-semibold mb-3">Danger Zone</h3>
<p className="text-sm text-tertiary mb-4">
Permanently delete this policy and all of its data. This action
cannot be undone.
</p>
<Button
variant="destructive"
className="w-full"
onClick={handleDeletePolicy}
disabled={isDeleting}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2"
>
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
</svg>
{isDeleting ? "Deleting..." : "Delete Policy"}
</Button>
</CardContent>
</Card>
</div>
</div>
</PageTemplate>
);
}
export default function PolicyView() {
const [queryRef, loadQuery] =
useQueryLoader<PolicyViewQueryType>(PolicyViewQuery);
const { policyId } = useParams();
useEffect(() => {
loadQuery({ policyId: policyId! });
}, [loadQuery, policyId]);
if (!queryRef) {
return <PolicyViewSkeleton />;
}
return (
<Suspense fallback={<PolicyViewSkeleton />}>
{queryRef && <PolicyViewContent queryRef={queryRef} />}
</Suspense>
);
}

View File

@@ -5,9 +5,9 @@ import { useLocation } from "react-router";
import { ErrorBoundaryWithLocation } from "../ErrorBoundary";
import { lazy } from "@probo/react-lazy";
const PolicyView = lazy(() => import("./PolicyView"));
const ShowPolicyView = lazy(() => import("./ShowPolicyView"));
export function PolicyViewSkeleton() {
export function ShowPolicyViewSkeleton() {
return (
<PageTemplateSkeleton
withDescription
@@ -108,13 +108,13 @@ export function PolicyViewSkeleton() {
);
}
export function PolicyPage() {
export function ShowPolicyPage() {
const location = useLocation();
return (
<Suspense key={location.pathname} fallback={<PolicyViewSkeleton />}>
<Suspense key={location.pathname} fallback={<ShowPolicyViewSkeleton />}>
<ErrorBoundaryWithLocation>
<PolicyView />
<ShowPolicyView />
</ErrorBoundaryWithLocation>
</Suspense>
);

View File

@@ -0,0 +1,426 @@
import { Suspense, useEffect, useState, useCallback, ReactNode } from "react";
import { useParams, Link, useNavigate } from "react-router";
import {
graphql,
PreloadedQuery,
usePreloadedQuery,
useQueryLoader,
useMutation,
} from "react-relay";
import { Clock, Download, Edit, Trash2, MoreHorizontal, X, FileSignature } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { PageTemplate } from "@/components/PageTemplate";
import type { ShowPolicyViewQuery } from "./__generated__/ShowPolicyViewQuery.graphql";
import { ShowPolicyViewPublishMutation } from "./__generated__/ShowPolicyViewPublishMutation.graphql";
import { ShowPolicyViewCreateDraftMutation } from "./__generated__/ShowPolicyViewCreateDraftMutation.graphql";
import ReactMarkdown from "react-markdown";
import { ShowPolicyViewSkeleton } from "./ShowPolicyPage";
import { format } from "date-fns";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { SignaturesModal } from "./SignaturesModal";
import { VersionHistoryModal } from "./VersionHistoryModal";
const policyViewQuery = graphql`
query ShowPolicyViewQuery($policyId: ID!) {
node(id: $policyId) {
id
... on Policy {
title
description
createdAt
updatedAt
currentPublishedVersion
owner {
id
fullName
primaryEmailAddress
}
...SignaturesModal_policyVersions
...VersionHistoryModal_policyVersions
latestVersion: versions(first: 1) {
edges {
node {
id
version
status
content
changelog
publishedAt
publishedBy {
fullName
}
createdAt
updatedAt
}
}
}
}
}
}
`;
const publishPolicyVersionMutation = graphql`
mutation ShowPolicyViewPublishMutation($input: PublishPolicyVersionInput!) {
publishPolicyVersion(input: $input) {
policy {
id
currentPublishedVersion
}
policyVersion {
id
status
publishedAt
publishedBy {
fullName
}
}
}
}
`;
const createDraftPolicyVersionMutation = graphql`
mutation ShowPolicyViewCreateDraftMutation($input: CreateDraftPolicyVersionInput!) {
createDraftPolicyVersion(input: $input) {
policyVersionEdge {
node {
id
version
status
}
}
}
}
`;
function ShowPolicyContent({
queryRef,
}: {
queryRef: PreloadedQuery<ShowPolicyViewQuery>;
}) {
const data = usePreloadedQuery<ShowPolicyViewQuery>(policyViewQuery, queryRef);
const policy = data.node;
const { organizationId } = useParams();
const navigate = useNavigate();
const { toast } = useToast();
const [queryRef2, loadQuery] = useQueryLoader<ShowPolicyViewQuery>(policyViewQuery);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false);
const [isSignaturesModalOpen, setIsSignaturesModalOpen] = useState(false);
const [publishDraft, isPublishInFlight] = useMutation<ShowPolicyViewPublishMutation>(publishPolicyVersionMutation);
const [createDraft, isCreateDraftInFlight] = useMutation<ShowPolicyViewCreateDraftMutation>(createDraftPolicyVersionMutation);
const latestVersionEdge = policy.latestVersion?.edges[0];
const latestVersionNode = latestVersionEdge?.node;
const isDraft = latestVersionNode?.status === "DRAFT";
useEffect(() => {
// No need to update selectedVersion state since we're using the VersionHistoryModal component
}, []);
// Handle delete policy
const handleDeletePolicy = useCallback(() => {
setIsDeleteDialogOpen(true);
}, []);
// Confirm delete policy
const confirmDeletePolicy = useCallback(() => {
setIsDeleting(true);
setTimeout(() => {
toast({
title: "Policy deleted",
description: "The policy has been deleted successfully",
});
setIsDeleting(false);
setIsDeleteDialogOpen(false);
navigate(`/organizations/${organizationId}/policies`);
}, 1000);
}, [toast, navigate, organizationId]);
// Navigate to publish flow
const handlePublish = useCallback(() => {
if (!policy.id) return;
publishDraft({
variables: {
input: {
policyId: policy.id
}
},
onCompleted: (_, errors) => {
if (errors) {
toast({
title: "Error publishing policy",
description: errors[0]?.message || "An unknown error occurred",
variant: "destructive"
});
return;
}
toast({
title: "Policy published",
description: `The policy has been published successfully`,
});
// Reload the query to refresh the data
loadQuery({ policyId: policy.id });
},
onError: (error) => {
toast({
title: "Error publishing policy",
description: error.message || "An unknown error occurred",
variant: "destructive"
});
}
});
}, [policy.id, publishDraft, toast, loadQuery]);
// Open version history modal
const handleVersionHistoryClick = useCallback(() => {
setIsVersionHistoryOpen(true);
}, []);
// Restore version
const handleRestoreVersion = useCallback((versionNumber: number) => {
// Here you would implement the logic to restore a version
toast({
title: "Version restored",
description: `Version ${versionNumber} has been restored`,
});
setIsVersionHistoryOpen(false);
// Reload the query to refresh the data
if (policy.id) {
loadQuery({ policyId: policy.id });
}
}, [policy.id, loadQuery, toast]);
// Handle edit policy
const handleEditPolicy = useCallback(() => {
if (!policy.id) return;
if (latestVersionNode?.status === "PUBLISHED") {
// Create a new draft version first
createDraft({
variables: {
input: {
policyID: policy.id
}
},
onCompleted: (response, errors) => {
if (errors) {
toast({
title: "Error creating draft",
description: errors[0]?.message || "An unknown error occurred",
variant: "destructive"
});
return;
}
const newDraftId = response.createDraftPolicyVersion.policyVersionEdge.node.id;
navigate(`/organizations/${organizationId}/policies/${policy.id}/versions/${newDraftId}/edit`);
},
onError: (error) => {
toast({
title: "Error creating draft",
description: error.message || "An unknown error occurred",
variant: "destructive"
});
}
});
} else {
// Navigate directly to edit if it's already a draft
navigate(`/organizations/${organizationId}/policies/${policy.id}/versions/${latestVersionNode?.id}/edit`);
}
}, [policy.id, latestVersionNode, createDraft, navigate, organizationId, toast]);
// Format date helper
const formatDate = (dateString?: string) => {
if (!dateString) return "N/A";
const date = new Date(dateString);
return format(date, "MMM d, yyyy");
};
return (
<PageTemplate
title={policy.title!}
actions={
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="rounded-full h-9 px-3 gap-1.5 shadow-sm border-[#022A0214] bg-white text-[#141E12]"
onClick={handleVersionHistoryClick}
>
<Clock className="h-4 w-4" />
<span className="font-medium">Version history</span>
</Button>
<Button
variant="outline"
size="sm"
className="rounded-full h-9 px-3 gap-1.5 shadow-sm border-[#022A0214] bg-white text-[#141E12]"
onClick={() => setIsSignaturesModalOpen(true)}
>
<FileSignature className="h-4 w-4" />
<span className="font-medium">Signature history</span>
</Button>
{isDraft && (
<Button
variant="default"
size="sm"
className="rounded-full h-9 px-3 gap-1.5 shadow-sm"
onClick={handlePublish}
disabled={isPublishInFlight}
>
<span className="font-medium">Publish version</span>
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="rounded-full h-9 w-9 shadow-sm border-[#022A0214] bg-white text-[#141E12]"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<div onClick={handleEditPolicy}>
<Edit className="mr-2 h-4 w-4" />
{latestVersionNode?.status === "PUBLISHED" ? "Create new draft" : "Edit draft policy"}
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleDeletePolicy}
className="text-danger focus:text-danger focus:bg-danger-bg"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete policy
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
}
>
<div className="space-y-4">
{latestVersionNode ? (
<div className="bg-white rounded-lg border border-solid-b shadow-sm p-6">
{latestVersionNode.changelog && (
<div className="mb-6 p-4 bg-level-1 rounded-md border border-solid-b">
<div className="text-xs text-tertiary uppercase font-medium mb-1">Change summary</div>
<div className="text-sm text-secondary">
{latestVersionNode.changelog}
</div>
</div>
)}
<div className="prose prose-olive max-w-none">
<ReactMarkdown>
{latestVersionNode.content || "No content available"}
</ReactMarkdown>
</div>
<div className="mt-8 pt-4 border-t border-solid-b text-xs text-tertiary flex justify-between items-center">
<div>
{latestVersionNode.status === "PUBLISHED"
? `Published on ${formatDate(latestVersionNode.publishedAt || "")}${latestVersionNode.publishedBy ? ` by ${latestVersionNode.publishedBy.fullName}` : ''}`
: `Last modified on ${formatDate(latestVersionNode.updatedAt)} by ${policy.owner?.fullName || 'Unknown'}`}
</div>
</div>
</div>
) : (
<div className="bg-white rounded-lg border border-solid-b shadow-sm p-6 text-center text-tertiary">
No content available
</div>
)}
</div>
{/* Signatures Modal */}
<SignaturesModal
isOpen={isSignaturesModalOpen}
onClose={() => setIsSignaturesModalOpen(false)}
policyRef={policy}
owner={policy.owner}
/>
{/* Version History Modal */}
<VersionHistoryModal
isOpen={isVersionHistoryOpen}
onClose={() => setIsVersionHistoryOpen(false)}
policyRef={policy}
onRestoreVersion={handleRestoreVersion}
/>
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Policy</DialogTitle>
<DialogDescription>
Are you sure you want to delete the policy "{policy.title}"? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setIsDeleteDialogOpen(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={confirmDeletePolicy}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</PageTemplate>
);
}
export default function ShowPolicyView() {
const [queryRef, loadQuery] = useQueryLoader<ShowPolicyViewQuery>(policyViewQuery);
const { policyId } = useParams();
useEffect(() => {
loadQuery({ policyId: policyId! });
}, [loadQuery, policyId]);
if (!queryRef) {
return <ShowPolicyViewSkeleton />;
}
return (
<Suspense fallback={<ShowPolicyViewSkeleton />}>
<ShowPolicyContent queryRef={queryRef} />
</Suspense>
);
}

View File

@@ -0,0 +1,74 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { format } from "date-fns";
interface PolicyVersionSignature {
id: string;
state: "REQUESTED" | "SIGNED";
signedBy: {
fullName: string;
};
signedAt?: string;
requestedAt: string;
requestedBy: {
fullName: string;
};
}
interface SignaturesListProps {
signatures: PolicyVersionSignature[];
}
export function SignaturesList({ signatures }: SignaturesListProps) {
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return format(date, "h:mm a • MMM d, yyyy");
};
return (
<div className="space-y-4">
{signatures.map((signature) => (
<div key={signature.id} className="bg-white rounded-lg border border-solid-b shadow-sm p-6">
<div className="flex items-center gap-3 mb-4">
<Avatar className="h-10 w-10">
<AvatarImage src="" alt={signature.signedBy.fullName} />
<AvatarFallback>
{signature.signedBy.fullName.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{signature.signedBy.fullName}</div>
<div className="text-sm text-tertiary">
{signature.state === "SIGNED" && signature.signedAt
? formatDateTime(signature.signedAt)
: formatDateTime(signature.requestedAt)}
</div>
</div>
</div>
<div className="space-y-2">
<div className="text-sm">
<span className="text-muted-foreground">Status:</span>{" "}
<span className={`px-2 py-0.5 rounded-full ${
signature.state === "SIGNED"
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`}>
{signature.state}
</span>
</div>
<div className="text-sm">
<span className="text-muted-foreground">Requested by:</span>{" "}
{signature.requestedBy.fullName}
</div>
{signature.state === "SIGNED" && signature.signedAt && (
<div className="text-sm">
<span className="text-muted-foreground">Signed at:</span>{" "}
{formatDateTime(signature.signedAt)}
</div>
)}
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,469 @@
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import { useState, useEffect } from "react";
import { graphql, useFragment, useMutation, useQueryLoader, usePreloadedQuery, ConnectionHandler } from "react-relay";
import type { SignaturesModal_policyVersions$data, SignaturesModal_policyVersions$key } from "./__generated__/SignaturesModal_policyVersions.graphql";
import type { SignaturesModalRequestSignatureMutation } from "./__generated__/SignaturesModalRequestSignatureMutation.graphql";
import type { SignaturesModalOrganizationQuery } from "./__generated__/SignaturesModalOrganizationQuery.graphql";
import { useToast } from "@/hooks/use-toast";
import { useParams } from "react-router";
import { Loader2, CheckCircle2, Clock } from "lucide-react";
import { PreloadedQuery } from "react-relay";
export const policyVersionsFragment = graphql`
fragment SignaturesModal_policyVersions on Policy {
title
policyVersions: versions(first: 10) {
edges {
node {
id
version
status
publishedAt
updatedAt
publishedBy {
fullName
}
signatures(first: 100) @connection(key: "SignaturesModal_policyVersions_signatures") {
edges {
node {
id
state
signedAt
requestedAt
signedBy {
fullName
id
}
requestedBy {
fullName
}
}
}
}
}
}
}
}
`;
const requestSignatureMutation = graphql`
mutation SignaturesModalRequestSignatureMutation($input: RequestSignatureInput!, $connections: [ID!]!) {
requestSignature(input: $input) {
policyVersionSignatureEdge @prependEdge(connections: $connections) {
node {
id
state
signedAt
requestedAt
signedBy {
fullName
id
}
requestedBy {
fullName
}
}
}
}
}
`;
const organizationQuery = graphql`
query SignaturesModalOrganizationQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
id
peoples(first: 100, orderBy: { direction: ASC, field: FULL_NAME }) {
edges {
node {
id
fullName
primaryEmailAddress
kind
}
}
}
}
}
}
`;
interface SignaturesModalProps {
isOpen: boolean;
onClose: () => void;
policyRef: SignaturesModal_policyVersions$key;
owner?: {
fullName: string;
} | null;
}
// Define the signature type based on the GraphQL response
// interface Signature {
// id: string;
// state: string;
// signedAt?: string | null;
// requestedAt: string;
// signedBy: {
// fullName: string;
// id: string;
// } | null;
// requestedBy: {
// fullName: string;
// } | null;
// }
export function SignaturesModal({
isOpen,
onClose,
policyRef,
owner,
}: SignaturesModalProps) {
const data = useFragment<SignaturesModal_policyVersions$key>(
policyVersionsFragment,
policyRef
);
// Safely access and extract version nodes
const versionNodes = (data?.policyVersions?.edges?.map(edge => edge.node) || []);
// Filter to just published versions and sort by version number
const publishedVersions = versionNodes
.filter(v => v.status === "PUBLISHED")
.sort((a, b) => b.version - a.version);
const [selectedVersion, setSelectedVersion] = useState<number>(
publishedVersions[0]?.version || 0
);
const selectedVersionData = versionNodes.find(v => v.version === selectedVersion);
const { organizationId } = useParams();
const [queryRef, loadQuery] = useQueryLoader<SignaturesModalOrganizationQuery>(organizationQuery);
const { toast } = useToast();
const [isRequesting, setIsRequesting] = useState<string | null>(null);
const [commitRequestSignature] = useMutation<SignaturesModalRequestSignatureMutation>(requestSignatureMutation);
// Load organization data when the modal opens and we have a valid version selected
useEffect(() => {
if (isOpen && organizationId && selectedVersionData?.status === "PUBLISHED") {
loadQuery({ organizationId });
}
}, [isOpen, organizationId, loadQuery, selectedVersionData]);
const formatDateTime = (dateString?: string | null) => {
if (!dateString) return "N/A";
const date = new Date(dateString);
return format(date, "h:mm a • MMM d, yyyy");
};
// Safely type signatures as our defined Signature interface
const signatures = selectedVersionData?.signatures?.edges?.map(edge => edge.node) || [];
const handleRequestSignature = (personId: string) => {
if (!selectedVersionData) return;
setIsRequesting(personId);
commitRequestSignature({
variables: {
input: {
policyVersionId: selectedVersionData.id,
signatoryId: personId,
},
connections: [
ConnectionHandler.getConnectionID(
selectedVersionData.id,
"SignaturesModal_policyVersions_signatures"
),
],
},
onCompleted: () => {
toast({
title: "Success",
description: "Signature request sent successfully",
});
setIsRequesting(null);
},
onError: (error) => {
toast({
title: "Error",
description: error.message,
variant: "destructive",
});
setIsRequesting(null);
},
});
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[1080px] h-[744px] p-0 overflow-hidden flex flex-col">
<div className="flex flex-1 overflow-hidden">
{/* Version History Sidebar */}
<div className="w-[316px] border-r border-solid-b h-full overflow-y-auto">
<h2 className="text-lg font-semibold p-6 pb-4">Version history</h2>
<div className="overflow-y-auto">
{versionNodes.length > 0 ? (
versionNodes.map(version => {
const isPublished = version.status === "PUBLISHED";
const isSelected = selectedVersion === version.version;
return (
<div
key={version.id}
className={`flex items-center gap-3 p-6 py-4 ${
isPublished ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'
} ${isSelected ? 'bg-slate-50' : ''}`}
onClick={() => isPublished && setSelectedVersion(version.version)}
>
<Avatar className="h-10 w-10">
<AvatarImage src="" alt={version.publishedBy?.fullName || owner?.fullName || ""} />
<AvatarFallback>
{(version.publishedBy?.fullName || owner?.fullName || "").charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">Version {version.version}</span>
{isPublished ? (
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800">
Published
</span>
) : (
<span className="text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800">
Draft
</span>
)}
</div>
<div className="text-sm text-tertiary">
{version.publishedBy?.fullName || owner?.fullName || "Unknown"} • {formatDateTime(version.publishedAt || version.updatedAt)}
</div>
</div>
</div>
);
})
) : (
<div className="p-6 text-center text-tertiary">
No versions available
</div>
)}
</div>
</div>
{/* Main Content Area */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Section header */}
<div className="p-6 border-b border-solid-b">
<h2 className="text-2xl font-semibold mb-2">
{selectedVersionData?.status === "PUBLISHED" ? "Signatures & Requests" : "Signatures"}
</h2>
{selectedVersionData?.status === "PUBLISHED" && (
<p className="text-sm text-muted-foreground">
Click request to ask for a signature from people in your organization
</p>
)}
</div>
{/* Content area with unified people and signatures list */}
<div className="flex-1 overflow-y-auto p-6">
{selectedVersionData ? (
selectedVersionData.status === "PUBLISHED" && queryRef ? (
<PeopleAndSignaturesList
queryRef={queryRef}
onRequestSignature={handleRequestSignature}
requestingId={isRequesting}
existingSignatures={signatures}
formatDateTime={formatDateTime}
/>
) : (
<div className="space-y-4">
{signatures.length > 0 ? (
<div className="divide-y divide-border">
{signatures.map(signature => (
<div key={signature.id} className="py-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarFallback>
{signature.signedBy?.fullName?.charAt(0) || signature.requestedBy?.fullName?.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{signature.signedBy?.fullName || signature.requestedBy?.fullName}</div>
<div className="text-sm text-tertiary">
{signature.state === "SIGNED"
? `Signed on ${formatDateTime(signature.signedAt)}`
: `Requested on ${formatDateTime(signature.requestedAt)}`}
</div>
</div>
</div>
<span className={`text-xs px-2 py-0.5 rounded-full ${
signature.state === "SIGNED"
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{signature.state === "SIGNED" ? "Signed" : "Pending"}
</span>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8">
<p className="text-tertiary">No signatures available for this version</p>
{selectedVersionData.status !== "PUBLISHED" && (
<p className="text-sm text-muted-foreground mt-2">Only published versions can have signatures</p>
)}
</div>
)}
</div>
)
) : (
<div className="text-center text-tertiary py-8">
No version selected
</div>
)}
{selectedVersionData?.status === "PUBLISHED" && !queryRef && (
<div className="py-8 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto mb-2" />
<p className="text-tertiary">Loading...</p>
</div>
)}
</div>
</div>
</div>
<div className="flex justify-end p-4 border-t border-solid-b mt-auto">
<Button
variant="outline"
onClick={onClose}
className="mr-2"
>
Close
</Button>
</div>
</DialogContent>
</Dialog>
);
}
// Combined people and signatures list component
function PeopleAndSignaturesList({
queryRef,
onRequestSignature,
requestingId,
existingSignatures,
formatDateTime,
}: {
queryRef: PreloadedQuery<SignaturesModalOrganizationQuery>,
onRequestSignature: (personId: string) => void,
requestingId: string | null,
existingSignatures: Array<SignaturesModal_policyVersions$data["policyVersions"]["edges"][0]["node"]["signatures"]["edges"][0]["node"]>,
formatDateTime: (date?: string | null) => string,
}) {
const data = usePreloadedQuery<SignaturesModalOrganizationQuery>(
organizationQuery,
queryRef
);
const people = data?.organization?.peoples?.edges || [];
if (!data?.organization) {
return (
<div className="text-center py-8">
Organization not found
</div>
);
}
if (people.length === 0) {
return (
<div className="text-center py-8">
No people available to request signatures from
</div>
);
}
// Create lookup for signatures by person ID
const signaturesByPersonId = new Map();
existingSignatures.forEach(sig => {
if (sig.signedBy?.id) {
signaturesByPersonId.set(sig.signedBy.id, sig);
}
});
return (
<div className="divide-y divide-border">
{people.map((edge: any) => {
const person = edge.node;
const signature = signaturesByPersonId.get(person.id);
const isRequesting = requestingId === person.id;
return (
<div key={person.id} className="py-4">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<Avatar className="h-10 w-10">
<AvatarFallback>
{person.fullName.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{person.fullName}</div>
<div className="text-sm text-tertiary">
{signature ? (
signature.state === "SIGNED" ? (
<div className="flex items-center gap-1 text-green-700">
<CheckCircle2 className="h-3.5 w-3.5" />
<span>Signed on {formatDateTime(signature.signedAt)}</span>
</div>
) : (
<div className="flex items-center gap-1 text-amber-700">
<Clock className="h-3.5 w-3.5" />
<span>Requested on {formatDateTime(signature.requestedAt)}</span>
</div>
)
) : (
person.primaryEmailAddress
)}
</div>
</div>
</div>
{signature ? (
<span className={`text-xs px-2 py-0.5 rounded-full ${
signature.state === "SIGNED"
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{signature.state === "SIGNED" ? "Signed" : "Pending"}
</span>
) : (
<Button
variant="outline"
size="sm"
onClick={() => onRequestSignature(person.id)}
disabled={isRequesting}
>
{isRequesting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Requesting...
</>
) : (
"Request Signature"
)}
</Button>
)}
</div>
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,149 @@
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import ReactMarkdown from "react-markdown";
import { graphql, useFragment } from "react-relay";
import type { VersionHistoryModal_policyVersions$key } from "./__generated__/VersionHistoryModal_policyVersions.graphql";
export const policyVersionsFragment = graphql`
fragment VersionHistoryModal_policyVersions on Policy {
title
owner {
fullName
}
versionHistory: versions(first: 20) {
edges {
node {
id
version
status
content
changelog
publishedAt
updatedAt
publishedBy {
fullName
}
}
}
}
}
`;
interface VersionHistoryModalProps {
isOpen: boolean;
onClose: () => void;
policyRef: VersionHistoryModal_policyVersions$key;
onRestoreVersion?: (versionNumber: number) => void;
}
export function VersionHistoryModal({
isOpen,
onClose,
policyRef,
onRestoreVersion
}: VersionHistoryModalProps) {
const data = useFragment<VersionHistoryModal_policyVersions$key>(
policyVersionsFragment,
policyRef
);
// Safely access and extract version nodes
const versionNodes = data?.versionHistory?.edges?.map(edge => edge.node) || [];
// Sort versions by version number (descending)
const versions = [...versionNodes].sort((a, b) => b.version - a.version);
const latestVersion = versions.length > 0 ? versions[0].version : 0;
const [selectedVersion, setSelectedVersion] = useState<number>(latestVersion);
const currentVersionData = versions.find(v => v.version === selectedVersion);
// Format time helper for version history
const formatDateTime = (dateString?: string | null) => {
if (!dateString) return "N/A";
const date = new Date(dateString);
return format(date, "h:mm a • MMM d, yyyy");
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[1080px] h-[744px] p-0 overflow-hidden flex flex-col">
<div className="flex flex-1 overflow-hidden">
{/* Version History Sidebar */}
<div className="w-[316px] border-r border-solid-b h-full overflow-y-auto">
<h2 className="text-lg font-semibold p-6 pb-4">Version history</h2>
<div className="overflow-y-auto">
{versions.map((version) => (
<div
key={version.id}
className={`flex items-center gap-3 p-6 py-4 cursor-pointer ${selectedVersion === version.version ? 'bg-slate-50' : ''}`}
onClick={() => setSelectedVersion(version.version)}
>
<Avatar className="h-10 w-10">
<AvatarImage src="" alt={version.publishedBy?.fullName || data.owner?.fullName || ""} />
<AvatarFallback>
{(version.publishedBy?.fullName || data.owner?.fullName || "").charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">Version {version.version}</span>
{version.status === "PUBLISHED" ? (
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-800">
Published
</span>
) : (
<span className="text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-800">
Draft
</span>
)}
</div>
<div className="text-sm text-tertiary">
{version.publishedBy?.fullName || data.owner?.fullName || "Unknown"} • {formatDateTime(version.publishedAt || version.updatedAt)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Content Area */}
<div className="flex-1 p-6 relative overflow-y-auto">
<h2 className="text-2xl font-semibold mb-6">{data.title}</h2>
<div className="prose prose-olive max-w-none">
{selectedVersion && (
<ReactMarkdown>
{versions.find(v => v.version === selectedVersion)?.content || "No content available"}
</ReactMarkdown>
)}
</div>
</div>
</div>
<div className="flex justify-end p-4 border-t border-solid-b mt-auto">
<Button
variant="outline"
onClick={onClose}
className="mr-2"
>
Cancel
</Button>
{onRestoreVersion && (
<Button
variant="default"
onClick={() => onRestoreVersion(selectedVersion)}
disabled={selectedVersion === latestVersion}
>
Restore version
</Button>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<171200736f3a443faae3f8b7c2085f4f>>
* @generated SignedSource<<d8d76cb73e37916ddc34017550ff3702>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,30 +9,18 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type UpdatePolicyInput = {
content?: string | null | undefined;
id: string;
name?: string | null | undefined;
ownerId?: string | null | undefined;
reviewDate?: string | null | undefined;
status?: PolicyStatus | null | undefined;
export type UpdatePolicyVersionInput = {
content: string;
policyVersionId: string;
};
export type EditPolicyViewMutation$variables = {
input: UpdatePolicyInput;
input: UpdatePolicyVersionInput;
};
export type EditPolicyViewMutation$data = {
readonly updatePolicy: {
readonly policy: {
readonly updatePolicyVersion: {
readonly policyVersion: {
readonly content: string;
readonly id: string;
readonly name: string;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
};
};
};
@@ -49,14 +37,7 @@ var v0 = [
"name": "input"
}
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v2 = [
v1 = [
{
"alias": null,
"args": [
@@ -66,25 +47,24 @@ v2 = [
"variableName": "input"
}
],
"concreteType": "UpdatePolicyPayload",
"concreteType": "UpdatePolicyVersionPayload",
"kind": "LinkedField",
"name": "updatePolicy",
"name": "updatePolicyVersion",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "policy",
"name": "policyVersion",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "id",
"storageKey": null
},
{
@@ -93,39 +73,6 @@ v2 = [
"kind": "ScalarField",
"name": "content",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
@@ -140,7 +87,7 @@ return {
"kind": "Fragment",
"metadata": null,
"name": "EditPolicyViewMutation",
"selections": (v2/*: any*/),
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
@@ -149,19 +96,19 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditPolicyViewMutation",
"selections": (v2/*: any*/)
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "643990a3ab54cb51144f434b023531e7",
"cacheID": "02b56043d606c6237dd502a7695fea50",
"id": null,
"metadata": {},
"name": "EditPolicyViewMutation",
"operationKind": "mutation",
"text": "mutation EditPolicyViewMutation(\n $input: UpdatePolicyInput!\n) {\n updatePolicy(input: $input) {\n policy {\n id\n name\n content\n status\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n}\n"
"text": "mutation EditPolicyViewMutation(\n $input: UpdatePolicyVersionInput!\n) {\n updatePolicyVersion(input: $input) {\n policyVersion {\n id\n content\n }\n }\n}\n"
}
};
})();
(node as any).hash = "41d4945568bc1f43eeedc727a968654d";
(node as any).hash = "556fbcc4fadc0011c1a2154dcffeb389";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<312876323f4072e43048dfe1b2e21d08>>
* @generated SignedSource<<7738a95cc83ee25c08f6ddc556f144c1>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,25 +10,26 @@
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type EditPolicyViewQuery$variables = {
organizationId: string;
policyId: string;
policyVersionId: string;
};
export type EditPolicyViewQuery$data = {
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
};
readonly policy: {
readonly content?: string;
readonly id: string;
readonly name?: string;
readonly owner?: {
readonly fullName: string;
readonly id: string;
};
readonly reviewDate?: string | null | undefined;
readonly status?: PolicyStatus;
readonly title?: string;
};
readonly policyVersion: {
readonly content?: string;
readonly id: string;
};
};
export type EditPolicyViewQuery = {
@@ -47,56 +48,61 @@ v1 = {
"kind": "LocalArgument",
"name": "policyId"
},
v2 = [
v2 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "policyVersionId"
},
v3 = [
{
"kind": "Variable",
"name": "id",
"variableName": "policyId"
"variableName": "policyVersionId"
}
],
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"name": "id",
"storageKey": null
},
v5 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"storageKey": null
},
}
],
"type": "PolicyVersion",
"abstractKey": null
},
v6 = [
{
"kind": "Variable",
"name": "id",
"variableName": "policyId"
}
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v8 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"name": "title",
"storageKey": null
},
{
@@ -107,8 +113,8 @@ v5 = {
"name": "owner",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/)
(v4/*: any*/),
(v7/*: any*/)
],
"storageKey": null
}
@@ -116,21 +122,21 @@ v5 = {
"type": "Policy",
"abstractKey": null
},
v6 = [
v9 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v7 = {
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v8 = [
v11 = [
{
"kind": "Literal",
"name": "first",
@@ -149,32 +155,46 @@ return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
(v1/*: any*/),
(v2/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "EditPolicyViewQuery",
"selections": [
{
"alias": "policy",
"args": (v2/*: any*/),
"alias": "policyVersion",
"args": (v3/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"alias": "policy",
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
(v8/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v9/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
@@ -192,41 +212,56 @@ return {
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
(v0/*: any*/),
(v2/*: any*/)
],
"kind": "Operation",
"name": "EditPolicyViewQuery",
"selections": [
{
"alias": "policy",
"args": (v2/*: any*/),
"alias": "policyVersion",
"args": (v3/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v3/*: any*/),
(v10/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"alias": "policy",
"args": (v6/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v3/*: any*/),
(v10/*: any*/),
(v4/*: any*/),
(v8/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v9/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v10/*: any*/),
(v4/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v8/*: any*/),
"args": (v11/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
@@ -248,8 +283,8 @@ return {
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
@@ -257,7 +292,7 @@ return {
"name": "primaryEmailAddress",
"storageKey": null
},
(v7/*: any*/)
(v10/*: any*/)
],
"storageKey": null
},
@@ -301,7 +336,7 @@ return {
},
{
"alias": null,
"args": (v8/*: any*/),
"args": (v11/*: any*/),
"filters": [
"orderBy"
],
@@ -320,16 +355,16 @@ return {
]
},
"params": {
"cacheID": "4d52d22f9d8acc7912a57d974f558248",
"cacheID": "4d586701fc5f2e4976bb87d9be61c594",
"id": null,
"metadata": {},
"name": "EditPolicyViewQuery",
"operationKind": "query",
"text": "query EditPolicyViewQuery(\n $policyId: ID!\n $organizationId: ID!\n) {\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n status\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
"text": "query EditPolicyViewQuery(\n $policyId: ID!\n $organizationId: ID!\n $policyVersionId: ID!\n) {\n policyVersion: node(id: $policyVersionId) {\n __typename\n id\n ... on PolicyVersion {\n content\n }\n }\n policy: node(id: $policyId) {\n __typename\n id\n ... on Policy {\n title\n owner {\n id\n fullName\n }\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d16bdd881b27b1aa5368130ce036fd20";
(node as any).hash = "2a934195ed33bd073e3976e8cc19a925";
export default node;

View File

@@ -1,230 +0,0 @@
/**
* @generated SignedSource<<84789bb949f94e924de1c9b9d566b4a5>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type NewPolicyViewQuery$variables = {
organizationId: string;
};
export type NewPolicyViewQuery$data = {
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
};
};
export type NewPolicyViewQuery = {
response: NewPolicyViewQuery$data;
variables: NewPolicyViewQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = [
{
"kind": "Literal",
"name": "first",
"value": 100
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "FULL_NAME"
}
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "NewPolicyViewQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PeopleSelector_organization"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "NewPolicyViewQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PeopleEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"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
}
],
"storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
},
{
"alias": null,
"args": (v4/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "PeopleSelector_organization_peoples",
"kind": "LinkedHandle",
"name": "peoples"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "552067b4b0632972c14f7a8dcaf171aa",
"id": null,
"metadata": {},
"name": "NewPolicyViewQuery",
"operationKind": "query",
"text": "query NewPolicyViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleSelector_organization\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "98fa41bfaf0d76adce1c91cf8d6ade28";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<2026b385ee5eb895da6ca9c919762913>>
* @generated SignedSource<<885dc2a54ba3b8d4663e197cfe61d704>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,39 +9,36 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type CreatePolicyInput = {
content: string;
name: string;
organizationId: string;
ownerId: string;
reviewDate?: string | null | undefined;
status: PolicyStatus;
title: string;
};
export type NewPolicyViewMutation$variables = {
export type PolicyListViewCreateMutation$variables = {
connections: ReadonlyArray<string>;
input: CreatePolicyInput;
};
export type NewPolicyViewMutation$data = {
export type PolicyListViewCreateMutation$data = {
readonly createPolicy: {
readonly policyEdge: {
readonly node: {
readonly content: string;
readonly createdAt: string;
readonly description: string;
readonly id: string;
readonly name: string;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly reviewDate: string | null | undefined;
readonly status: PolicyStatus;
readonly title: string;
readonly updatedAt: string;
};
};
};
};
export type NewPolicyViewMutation = {
response: NewPolicyViewMutation$data;
variables: NewPolicyViewMutation$variables;
export type PolicyListViewCreateMutation = {
response: PolicyListViewCreateMutation$data;
variables: PolicyListViewCreateMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -90,28 +87,28 @@ v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"name": "updatedAt",
"storageKey": null
},
{
@@ -147,7 +144,7 @@ return {
],
"kind": "Fragment",
"metadata": null,
"name": "NewPolicyViewMutation",
"name": "PolicyListViewCreateMutation",
"selections": [
{
"alias": null,
@@ -172,7 +169,7 @@ return {
(v0/*: any*/)
],
"kind": "Operation",
"name": "NewPolicyViewMutation",
"name": "PolicyListViewCreateMutation",
"selections": [
{
"alias": null,
@@ -205,16 +202,16 @@ return {
]
},
"params": {
"cacheID": "aa29cde3106d1c8d35c6ae8e74f310b7",
"cacheID": "f7aae6d7a0d276e71caec643e43b5ee5",
"id": null,
"metadata": {},
"name": "NewPolicyViewMutation",
"name": "PolicyListViewCreateMutation",
"operationKind": "mutation",
"text": "mutation NewPolicyViewMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n id\n name\n content\n status\n reviewDate\n owner {\n id\n fullName\n }\n }\n }\n }\n}\n"
"text": "mutation PolicyListViewCreateMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n id\n title\n description\n createdAt\n updatedAt\n owner {\n id\n fullName\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "f6dcabf6c51e3858c9750e00fcc0cc15";
(node as any).hash = "d55ac010a7ad352d830d3a38f2f8d5a8";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<391e3817107bb96f50aad61d20cad0a8>>
* @generated SignedSource<<5b24e53258fceb03535363014f57a15e>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,8 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
import { FragmentRefs } from "relay-runtime";
export type PolicyStatus = "DRAFT" | "PUBLISHED";
export type PolicyListViewQuery$variables = {
organizationId: string;
};
@@ -18,15 +19,34 @@ export type PolicyListViewQuery$data = {
readonly policies?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly content: string;
readonly createdAt: string;
readonly currentPublishedVersion: number | null | undefined;
readonly description: string;
readonly id: string;
readonly name: string;
readonly status: PolicyStatus;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly title: string;
readonly updatedAt: string;
readonly versions: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly status: PolicyStatus;
readonly updatedAt: string;
};
}>;
};
};
}>;
};
readonly " $fragmentSpreads": FragmentRefs<"PeopleSelector_organization">;
};
readonly viewer: {
readonly user: {
readonly id: string;
};
};
};
export type PolicyListViewQuery = {
@@ -42,28 +62,94 @@ var v0 = [
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
v2 = {
"alias": null,
"args": null,
"concreteType": "User",
"kind": "LinkedField",
"name": "user",
"plural": false,
"selections": [
(v1/*: any*/)
],
"storageKey": null
},
v3 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v4 = {
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "TITLE"
}
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v4 = [
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v9 = {
"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
},
v10 = [
{
"alias": null,
"args": null,
@@ -80,19 +166,26 @@ v4 = [
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "currentPublishedVersion",
"storageKey": null
},
{
@@ -102,66 +195,103 @@ v4 = [
"name": "createdAt",
"storageKey": null
},
(v5/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v1/*: any*/),
(v6/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": 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": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
(v5/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
},
(v3/*: any*/)
(v7/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
(v8/*: any*/)
],
"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
}
(v9/*: any*/)
],
v5 = [
v11 = [
{
"kind": "Literal",
"name": "first",
"value": 100
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "FULL_NAME"
}
}
],
v12 = [
"orderBy"
],
v13 = [
{
"kind": "Literal",
"name": "first",
"value": 50
},
(v4/*: any*/)
];
return {
"fragment": {
@@ -170,9 +300,21 @@ return {
"metadata": null,
"name": "PolicyListViewQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v1/*: any*/),
"args": (v3/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
@@ -182,14 +324,21 @@ return {
"kind": "InlineFragment",
"selections": [
{
"alias": "policies",
"args": null,
"kind": "FragmentSpread",
"name": "PeopleSelector_organization"
},
{
"alias": "policies",
"args": [
(v4/*: any*/)
],
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "__PolicyListView_policies_connection",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": null
"selections": (v10/*: any*/),
"storageKey": "__PolicyListView_policies_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"TITLE\"})"
}
],
"type": "Organization",
@@ -208,32 +357,100 @@ return {
"kind": "Operation",
"name": "PolicyListViewQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
(v2/*: any*/),
(v1/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v1/*: any*/),
"args": (v3/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v7/*: any*/),
(v1/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v5/*: any*/),
"args": (v11/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PeopleEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
(v6/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
},
(v7/*: any*/)
],
"storageKey": null
},
(v8/*: any*/)
],
"storageKey": null
},
(v9/*: any*/)
],
"storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
},
{
"alias": null,
"args": (v11/*: any*/),
"filters": (v12/*: any*/),
"handle": "connection",
"key": "PeopleSelector_organization_peoples",
"kind": "LinkedHandle",
"name": "peoples"
},
{
"alias": null,
"args": (v13/*: any*/),
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "policies",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": "policies(first:100)"
"selections": (v10/*: any*/),
"storageKey": "policies(first:50,orderBy:{\"direction\":\"ASC\",\"field\":\"TITLE\"})"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": null,
"args": (v13/*: any*/),
"filters": (v12/*: any*/),
"handle": "connection",
"key": "PolicyListView_policies",
"kind": "LinkedHandle",
@@ -242,15 +459,14 @@ return {
],
"type": "Organization",
"abstractKey": null
},
(v2/*: any*/)
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "6434c8135f68ac64eb2f06983ddc3595",
"cacheID": "ed650f54cc226d3adea27a78a777890c",
"id": null,
"metadata": {
"connection": [
@@ -267,11 +483,11 @@ return {
},
"name": "PolicyListViewQuery",
"operationKind": "query",
"text": "query PolicyListViewQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n name\n content\n createdAt\n updatedAt\n status\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n"
"text": "query PolicyListViewQuery(\n $organizationId: ID!\n) {\n viewer {\n user {\n id\n }\n id\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...PeopleSelector_organization\n policies(first: 50, orderBy: {field: TITLE, direction: ASC}) {\n edges {\n node {\n id\n title\n description\n currentPublishedVersion\n createdAt\n updatedAt\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n updatedAt\n }\n }\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n id\n }\n}\n\nfragment PeopleSelector_organization on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "5567339ad9b2be90b94edc8b1a16fe1e";
(node as any).hash = "b3b0798be0779c09013a4743d015e87f";
export default node;

View File

@@ -1,132 +0,0 @@
/**
* @generated SignedSource<<4e559f39bd9ec2b3782113ef034cad03>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeletePolicyInput = {
policyId: string;
};
export type PolicyViewDeleteMutation$variables = {
connections: ReadonlyArray<string>;
input: DeletePolicyInput;
};
export type PolicyViewDeleteMutation$data = {
readonly deletePolicy: {
readonly deletedPolicyId: string;
};
};
export type PolicyViewDeleteMutation = {
response: PolicyViewDeleteMutation$data;
variables: PolicyViewDeleteMutation$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": "deletedPolicyId",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "PolicyViewDeleteMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeletePolicyPayload",
"kind": "LinkedField",
"name": "deletePolicy",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "PolicyViewDeleteMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "DeletePolicyPayload",
"kind": "LinkedField",
"name": "deletePolicy",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "deleteEdge",
"key": "",
"kind": "ScalarHandle",
"name": "deletedPolicyId",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "67e67147e0c7ec0489346b1ad86899c9",
"id": null,
"metadata": {},
"name": "PolicyViewDeleteMutation",
"operationKind": "mutation",
"text": "mutation PolicyViewDeleteMutation(\n $input: DeletePolicyInput!\n) {\n deletePolicy(input: $input) {\n deletedPolicyId\n }\n}\n"
}
};
})();
(node as any).hash = "7d421e9b068f3f676a4c3069769b1c18";
export default node;

View File

@@ -1,199 +0,0 @@
/**
* @generated SignedSource<<eaeef5694bdd615c03bea3b3f8957df3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type PolicyViewQuery$variables = {
policyId: string;
};
export type PolicyViewQuery$data = {
readonly node: {
readonly content?: string;
readonly createdAt?: string;
readonly id: string;
readonly name?: string;
readonly owner?: {
readonly fullName: string;
readonly id: string;
readonly primaryEmailAddress: string;
};
readonly reviewDate?: string | null | undefined;
readonly status?: PolicyStatus;
readonly updatedAt?: string;
};
};
export type PolicyViewQuery = {
response: PolicyViewQuery$data;
variables: PolicyViewQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "policyId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "policyId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "reviewDate",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"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
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Policy",
"abstractKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "PolicyViewQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "PolicyViewQuery",
"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*/),
(v3/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "26bf57eeab9c600a3146e01085ecae8e",
"id": null,
"metadata": {},
"name": "PolicyViewQuery",
"operationKind": "query",
"text": "query PolicyViewQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n id\n ... on Policy {\n name\n content\n createdAt\n updatedAt\n reviewDate\n status\n owner {\n id\n fullName\n primaryEmailAddress\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "0fcc28fc290df69d14d7d549bd09216f";
export default node;

View File

@@ -0,0 +1,135 @@
/**
* @generated SignedSource<<290d3c4fb382c7b99c60eeee5cfa8c5a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "DRAFT" | "PUBLISHED";
export type CreateDraftPolicyVersionInput = {
policyID: string;
};
export type ShowPolicyViewCreateDraftMutation$variables = {
input: CreateDraftPolicyVersionInput;
};
export type ShowPolicyViewCreateDraftMutation$data = {
readonly createDraftPolicyVersion: {
readonly policyVersionEdge: {
readonly node: {
readonly id: string;
readonly status: PolicyStatus;
readonly version: number;
};
};
};
};
export type ShowPolicyViewCreateDraftMutation = {
response: ShowPolicyViewCreateDraftMutation$data;
variables: ShowPolicyViewCreateDraftMutation$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": "ShowPolicyViewCreateDraftMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowPolicyViewCreateDraftMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "e96d226e93260cd989dc122193fbcbce",
"id": null,
"metadata": {},
"name": "ShowPolicyViewCreateDraftMutation",
"operationKind": "mutation",
"text": "mutation ShowPolicyViewCreateDraftMutation(\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 = "49282edca41fc2bdca1a1c6db4b2dc11";
export default node;

View File

@@ -0,0 +1,211 @@
/**
* @generated SignedSource<<024ec8d58aa5f37adad3ac5e98c15a90>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "DRAFT" | "PUBLISHED";
export type PublishPolicyVersionInput = {
policyId: string;
};
export type ShowPolicyViewPublishMutation$variables = {
input: PublishPolicyVersionInput;
};
export type ShowPolicyViewPublishMutation$data = {
readonly publishPolicyVersion: {
readonly policy: {
readonly currentPublishedVersion: number | null | undefined;
readonly id: string;
};
readonly policyVersion: {
readonly id: string;
readonly publishedAt: string | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly status: PolicyStatus;
};
};
};
export type ShowPolicyViewPublishMutation = {
response: ShowPolicyViewPublishMutation$data;
variables: ShowPolicyViewPublishMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"concreteType": "Policy",
"kind": "LinkedField",
"name": "policy",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "currentPublishedVersion",
"storageKey": null
}
],
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "publishedAt",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ShowPolicyViewPublishMutation",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "PublishPolicyVersionPayload",
"kind": "LinkedField",
"name": "publishPolicyVersion",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "policyVersion",
"plural": false,
"selections": [
(v2/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
(v6/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowPolicyViewPublishMutation",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "PublishPolicyVersionPayload",
"kind": "LinkedField",
"name": "publishPolicyVersion",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "policyVersion",
"plural": false,
"selections": [
(v2/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
(v6/*: any*/),
(v2/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "2047a011abeae6642230cb7b31080b08",
"id": null,
"metadata": {},
"name": "ShowPolicyViewPublishMutation",
"operationKind": "mutation",
"text": "mutation ShowPolicyViewPublishMutation(\n $input: PublishPolicyVersionInput!\n) {\n publishPolicyVersion(input: $input) {\n policy {\n id\n currentPublishedVersion\n }\n policyVersion {\n id\n status\n publishedAt\n publishedBy {\n fullName\n id\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d81485a797858cf5f2834cf131d248cc";
export default node;

View File

@@ -0,0 +1,606 @@
/**
* @generated SignedSource<<351444982c4868267f30cc9fc8a2399a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PolicyStatus = "DRAFT" | "PUBLISHED";
export type ShowPolicyViewQuery$variables = {
policyId: string;
};
export type ShowPolicyViewQuery$data = {
readonly node: {
readonly createdAt?: string;
readonly currentPublishedVersion?: number | null | undefined;
readonly description?: string;
readonly id: string;
readonly latestVersion?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly changelog: string;
readonly content: string;
readonly createdAt: string;
readonly id: string;
readonly publishedAt: string | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly status: PolicyStatus;
readonly updatedAt: string;
readonly version: number;
};
}>;
};
readonly owner?: {
readonly fullName: string;
readonly id: string;
readonly primaryEmailAddress: string;
};
readonly title?: string;
readonly updatedAt?: string;
readonly " $fragmentSpreads": FragmentRefs<"SignaturesModal_policyVersions" | "VersionHistoryModal_policyVersions">;
};
};
export type ShowPolicyViewQuery = {
response: ShowPolicyViewQuery$data;
variables: ShowPolicyViewQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "policyId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "policyId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "currentPublishedVersion",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v2/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
}
],
"storageKey": null
},
v10 = [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"storageKey": null
},
v14 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "changelog",
"storageKey": null
},
v15 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "publishedAt",
"storageKey": null
},
v16 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v17 = [
(v8/*: any*/),
(v2/*: any*/)
],
v18 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": (v17/*: any*/),
"storageKey": null
},
v19 = [
{
"kind": "Literal",
"name": "first",
"value": 100
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ShowPolicyViewQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v9/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "SignaturesModal_policyVersions"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "VersionHistoryModal_policyVersions"
},
{
"alias": "latestVersion",
"args": (v10/*: any*/),
"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*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/),
(v14/*: any*/),
(v15/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "publishedBy",
"plural": false,
"selections": [
(v8/*: any*/)
],
"storageKey": null
},
(v5/*: any*/),
(v6/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"type": "Policy",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowPolicyViewQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v16/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v9/*: any*/),
{
"alias": "policyVersions",
"args": [
{
"kind": "Literal",
"name": "first",
"value": 10
}
],
"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*/),
(v11/*: any*/),
(v12/*: any*/),
(v15/*: any*/),
(v6/*: any*/),
(v18/*: any*/),
{
"alias": null,
"args": (v19/*: any*/),
"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
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "signedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "requestedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "signedBy",
"plural": false,
"selections": (v17/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "requestedBy",
"plural": false,
"selections": (v17/*: any*/),
"storageKey": null
},
(v16/*: 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
}
],
"storageKey": "signatures(first:100)"
},
{
"alias": null,
"args": (v19/*: any*/),
"filters": null,
"handle": "connection",
"key": "SignaturesModal_policyVersions_signatures",
"kind": "LinkedHandle",
"name": "signatures"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:10)"
},
{
"alias": "versionHistory",
"args": [
{
"kind": "Literal",
"name": "first",
"value": 20
}
],
"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*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/),
(v14/*: any*/),
(v15/*: any*/),
(v6/*: any*/),
(v18/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:20)"
},
{
"alias": "latestVersion",
"args": (v10/*: any*/),
"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*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/),
(v14/*: any*/),
(v15/*: any*/),
(v18/*: any*/),
(v5/*: any*/),
(v6/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"type": "Policy",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "751e6cc23218ca2da7b5bf44d3eef59c",
"id": null,
"metadata": {},
"name": "ShowPolicyViewQuery",
"operationKind": "query",
"text": "query ShowPolicyViewQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n id\n ... on Policy {\n title\n description\n createdAt\n updatedAt\n currentPublishedVersion\n owner {\n id\n fullName\n primaryEmailAddress\n }\n ...SignaturesModal_policyVersions\n ...VersionHistoryModal_policyVersions\n latestVersion: versions(first: 1) {\n edges {\n node {\n id\n version\n status\n content\n changelog\n publishedAt\n publishedBy {\n fullName\n id\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n}\n\nfragment SignaturesModal_policyVersions on Policy {\n title\n policyVersions: versions(first: 10) {\n edges {\n node {\n id\n version\n status\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n signatures(first: 100) {\n edges {\n node {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n id\n }\n requestedBy {\n fullName\n id\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n }\n}\n\nfragment VersionHistoryModal_policyVersions on Policy {\n title\n owner {\n fullName\n id\n }\n versionHistory: versions(first: 20) {\n edges {\n node {\n id\n version\n status\n content\n changelog\n publishedAt\n updatedAt\n publishedBy {\n fullName\n id\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "58adb8690071648aecb252b2f807ea7d";
export default node;

View File

@@ -0,0 +1,206 @@
/**
* @generated SignedSource<<070580dfc3d76a6f1ca8b0227dc8c1c6>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PeopleKind = "CONTRACTOR" | "EMPLOYEE" | "SERVICE_ACCOUNT";
export type SignaturesModalOrganizationQuery$variables = {
organizationId: string;
};
export type SignaturesModalOrganizationQuery$data = {
readonly organization: {
readonly id?: string;
readonly peoples?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly fullName: string;
readonly id: string;
readonly kind: PeopleKind;
readonly primaryEmailAddress: string;
};
}>;
};
};
};
export type SignaturesModalOrganizationQuery = {
response: SignaturesModalOrganizationQuery$data;
variables: SignaturesModalOrganizationQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "FULL_NAME"
}
}
],
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PeopleEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "primaryEmailAddress",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "kind",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "peoples(first:100,orderBy:{\"direction\":\"ASC\",\"field\":\"FULL_NAME\"})"
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "SignaturesModalOrganizationQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
(v3/*: any*/)
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "SignaturesModalOrganizationQuery",
"selections": [
{
"alias": "organization",
"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": [
(v3/*: any*/)
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "a2551c513c1c12a46c0da5328086475a",
"id": null,
"metadata": {},
"name": "SignaturesModalOrganizationQuery",
"operationKind": "query",
"text": "query SignaturesModalOrganizationQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n peoples(first: 100, orderBy: {direction: ASC, field: FULL_NAME}) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n kind\n }\n }\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "39bec552bdf8762487cec72839225b84";
export default node;

View File

@@ -0,0 +1,264 @@
/**
* @generated SignedSource<<fda822cf46d8b9a49852a53edd8e363d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyVersionSignatureState = "REQUESTED" | "SIGNED";
export type RequestSignatureInput = {
policyVersionId: string;
signatoryId: string;
};
export type SignaturesModalRequestSignatureMutation$variables = {
connections: ReadonlyArray<string>;
input: RequestSignatureInput;
};
export type SignaturesModalRequestSignatureMutation$data = {
readonly requestSignature: {
readonly policyVersionSignatureEdge: {
readonly node: {
readonly id: string;
readonly requestedAt: string;
readonly requestedBy: {
readonly fullName: string;
};
readonly signedAt: string | null | undefined;
readonly signedBy: {
readonly fullName: string;
readonly id: string;
};
readonly state: PolicyVersionSignatureState;
};
};
};
};
export type SignaturesModalRequestSignatureMutation = {
response: SignaturesModalRequestSignatureMutation$data;
variables: SignaturesModalRequestSignatureMutation$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,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "signedAt",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "requestedAt",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v8 = [
(v7/*: any*/),
(v3/*: any*/)
],
v9 = {
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "signedBy",
"plural": false,
"selections": (v8/*: any*/),
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "SignaturesModalRequestSignatureMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "RequestSignaturePayload",
"kind": "LinkedField",
"name": "requestSignature",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "policyVersionSignatureEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v9/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "requestedBy",
"plural": false,
"selections": [
(v7/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "SignaturesModalRequestSignatureMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "RequestSignaturePayload",
"kind": "LinkedField",
"name": "requestSignature",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "policyVersionSignatureEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v9/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "requestedBy",
"plural": false,
"selections": (v8/*: any*/),
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"filters": null,
"handle": "prependEdge",
"key": "",
"kind": "LinkedHandle",
"name": "policyVersionSignatureEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "4c6a5b6ed95c76c157f8627409a4481b",
"id": null,
"metadata": {},
"name": "SignaturesModalRequestSignatureMutation",
"operationKind": "mutation",
"text": "mutation SignaturesModalRequestSignatureMutation(\n $input: RequestSignatureInput!\n) {\n requestSignature(input: $input) {\n policyVersionSignatureEdge {\n node {\n id\n state\n signedAt\n requestedAt\n signedBy {\n fullName\n id\n }\n requestedBy {\n fullName\n id\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "2b17fe4feae2614a6697c63a7334a1cf";
export default node;

View File

@@ -0,0 +1,298 @@
/**
* @generated SignedSource<<ee81b03b50b0bc65932295b898b856d4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type PolicyStatus = "DRAFT" | "PUBLISHED";
export type PolicyVersionSignatureState = "REQUESTED" | "SIGNED";
import { FragmentRefs } from "relay-runtime";
export type SignaturesModal_policyVersions$data = {
readonly policyVersions: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly publishedAt: string | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly signatures: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly requestedAt: string;
readonly requestedBy: {
readonly fullName: string;
};
readonly signedAt: string | null | undefined;
readonly signedBy: {
readonly fullName: string;
readonly id: string;
};
readonly state: PolicyVersionSignatureState;
};
}>;
};
readonly status: PolicyStatus;
readonly updatedAt: string;
readonly version: number;
};
}>;
};
readonly title: string;
readonly " $fragmentType": "SignaturesModal_policyVersions";
};
export type SignaturesModal_policyVersions$key = {
readonly " $data"?: SignaturesModal_policyVersions$data;
readonly " $fragmentSpreads": FragmentRefs<"SignaturesModal_policyVersions">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
v2 = [
(v1/*: any*/)
];
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": null
}
]
},
"name": "SignaturesModal_policyVersions",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": "policyVersions",
"args": [
{
"kind": "Literal",
"name": "first",
"value": 10
}
],
"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": [
(v0/*: any*/),
{
"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": "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": (v2/*: any*/),
"storageKey": null
},
{
"alias": "signatures",
"args": null,
"concreteType": "PolicyVersionSignatureConnection",
"kind": "LinkedField",
"name": "__SignaturesModal_policyVersions_signatures_connection",
"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": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "signedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "requestedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "signedBy",
"plural": false,
"selections": [
(v1/*: any*/),
(v0/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "requestedBy",
"plural": false,
"selections": (v2/*: any*/),
"storageKey": null
},
{
"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
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:10)"
}
],
"type": "Policy",
"abstractKey": null
};
})();
(node as any).hash = "da9bf0fc12618afb0f9ec8f2337b9b9b";
export default node;

View File

@@ -0,0 +1,181 @@
/**
* @generated SignedSource<<21626cad90fcb579071d73bc47e3fb7a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type PolicyStatus = "DRAFT" | "PUBLISHED";
import { FragmentRefs } from "relay-runtime";
export type VersionHistoryModal_policyVersions$data = {
readonly owner: {
readonly fullName: string;
};
readonly title: string;
readonly versionHistory: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly changelog: string;
readonly content: string;
readonly id: string;
readonly publishedAt: string | null | undefined;
readonly publishedBy: {
readonly fullName: string;
} | null | undefined;
readonly status: PolicyStatus;
readonly updatedAt: string;
readonly version: number;
};
}>;
};
readonly " $fragmentType": "VersionHistoryModal_policyVersions";
};
export type VersionHistoryModal_policyVersions$key = {
readonly " $data"?: VersionHistoryModal_policyVersions$data;
readonly " $fragmentSpreads": FragmentRefs<"VersionHistoryModal_policyVersions">;
};
const node: ReaderFragment = (function(){
var v0 = [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
];
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "VersionHistoryModal_policyVersions",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": (v0/*: any*/),
"storageKey": null
},
{
"alias": "versionHistory",
"args": [
{
"kind": "Literal",
"name": "first",
"value": 20
}
],
"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": [
{
"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": (v0/*: any*/),
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:20)"
}
],
"type": "Policy",
"abstractKey": null
};
})();
(node as any).hash = "09cd9142880cfbb239ce866ea81afe48";
export default node;

View File

@@ -101,8 +101,7 @@ const showRiskViewQuery = graphql`
edges {
node {
id
name
status
title
createdAt
}
}
@@ -155,8 +154,7 @@ const organizationPoliciesQuery = graphql`
edges {
node {
id
name
status
title
}
}
}
@@ -530,7 +528,7 @@ function ShowRiskViewContent({
return policies.filter((policy) => {
return (
!policySearchQuery ||
policy.name.toLowerCase().includes(policySearchQuery.toLowerCase())
policy.title.toLowerCase().includes(policySearchQuery.toLowerCase())
);
});
}, [getPolicies, policySearchQuery]);
@@ -731,7 +729,7 @@ function ShowRiskViewContent({
toast({
title: "Success",
description: `Linked policy "${policy.name}" to this risk.`,
description: `Linked policy "${policy.title}" to this risk.`,
});
},
onError: (error) => {
@@ -803,7 +801,7 @@ function ShowRiskViewContent({
toast({
title: "Success",
description: `Unlinked policy "${policy.name}" from this risk.`,
description: `Unlinked policy "${policy.title}" from this risk.`,
});
},
onError: (error) => {
@@ -1090,15 +1088,26 @@ function ShowRiskViewContent({
<TabsContent value="policies" className="space-y-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Risk Policies</h2>
<Button
onClick={() => {
setIsPolicyDialogOpen(true);
loadPoliciesData();
}}
>
<Plus className="mr-2 h-4 w-4" />
Link Policy
</Button>
<div className="flex space-x-2">
<Button
variant="outline"
asChild
>
<Link to={`/organizations/${organizationId}/policies/new?riskId=${risk.id}`}>
<Plus className="mr-2 h-4 w-4" />
New Version
</Link>
</Button>
<Button
onClick={() => {
setIsPolicyDialogOpen(true);
loadPoliciesData();
}}
>
<Plus className="mr-2 h-4 w-4" />
Link Policy
</Button>
</div>
</div>
{policies.length > 0 ? (
<div className="rounded-md border">
@@ -1117,7 +1126,7 @@ function ShowRiskViewContent({
to={`/organizations/${organizationId}/policies/${policy.id}`}
className="font-medium text-blue-600 hover:underline"
>
{policy.name}
{policy.title}
</Link>
</TableCell>
<TableCell>
@@ -1350,7 +1359,7 @@ function ShowRiskViewContent({
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<h3 className="font-medium">{policy.name}</h3>
<h3 className="font-medium">{policy.title}</h3>
</div>
{isLinked ? (
<Button

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<86be887d4ec59050fd69900fd80863a2>>
* @generated SignedSource<<0335bb26d78f02170c02c7c2279a6bfd>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type ShowRiskViewOrganizationPoliciesQuery$variables = {
organizationId: string;
};
@@ -20,8 +19,7 @@ export type ShowRiskViewOrganizationPoliciesQuery$data = {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly name: string;
readonly status: PolicyStatus;
readonly title: string;
};
}>;
};
@@ -83,14 +81,7 @@ v4 = [
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"name": "title",
"storageKey": null
},
(v3/*: any*/)
@@ -228,7 +219,7 @@ return {
]
},
"params": {
"cacheID": "a229f1f805e79f8de378305e021d7fea",
"cacheID": "a92a6b2390e95e67b42fd18cc1030a9a",
"id": null,
"metadata": {
"connection": [
@@ -245,11 +236,11 @@ return {
},
"name": "ShowRiskViewOrganizationPoliciesQuery",
"operationKind": "query",
"text": "query ShowRiskViewOrganizationPoliciesQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n name\n status\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query ShowRiskViewOrganizationPoliciesQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n title\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "26cbf19cd716aee7413931357b9c5354";
(node as any).hash = "b47adf6c3a247367a6b23dd161df2702";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e318c9e0fe4355eb4923acb54da59ebb>>
* @generated SignedSource<<5d1745636bcff5f05d946bcd613463e5>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,7 +10,6 @@
import { ConcreteRequest } from 'relay-runtime';
export type MesureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type RiskTreatment = "ACCEPTED" | "AVOIDED" | "MITIGATED" | "TRANSFERRED";
export type ShowRiskViewQuery$variables = {
riskId: string;
@@ -56,8 +55,7 @@ export type ShowRiskViewQuery$data = {
readonly node: {
readonly createdAt: string;
readonly id: string;
readonly name: string;
readonly status: PolicyStatus;
readonly title: string;
};
}>;
};
@@ -285,12 +283,11 @@ v18 = [
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"name": "title",
"storageKey": null
},
(v12/*: any*/),
@@ -517,7 +514,7 @@ return {
]
},
"params": {
"cacheID": "05f0fe48c916ecda883f606eed0c9244",
"cacheID": "a88eba4c398c292d41deaddd1d77bb3b",
"id": null,
"metadata": {
"connection": [
@@ -552,11 +549,11 @@ return {
},
"name": "ShowRiskViewQuery",
"operationKind": "query",
"text": "query ShowRiskViewQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n description\n treatment\n owner {\n id\n fullName\n }\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n createdAt\n updatedAt\n mesures(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n createdAt\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n policies(first: 100) {\n edges {\n node {\n id\n name\n status\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query ShowRiskViewQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n description\n treatment\n owner {\n id\n fullName\n }\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n note\n createdAt\n updatedAt\n mesures(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n createdAt\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n policies(first: 100) {\n edges {\n node {\n id\n title\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "25e67618b61999a955716e937a53a470";
(node as any).hash = "96158658ecb7c406fc969e086917c03e";
export default node;

View File

@@ -31,4 +31,6 @@ const (
EmailEntityType
ControlEntityType
RiskEntityType
PolicyVersionEntityType
PolicyVersionSignatureEntityType
)

View File

@@ -0,0 +1,60 @@
CREATE TABLE policy_versions (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
policy_id TEXT NOT NULL REFERENCES policies(id) ON DELETE CASCADE,
version_number INTEGER NOT NULL,
content TEXT NOT NULL,
changelog TEXT NOT NULL,
created_by TEXT NOT NULL,
status policy_status NOT NULL,
published_by TEXT REFERENCES peoples(id),
published_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE(policy_id, version_number)
);
INSERT INTO policy_versions (
id,
tenant_id,
policy_id,
version_number,
content,
changelog,
created_by,
status,
published_by,
published_at,
created_at,
updated_at
)
SELECT
generate_gid(decode_base64_unpadded(p.tenant_id), 16),
p.tenant_id,
p.id,
1,
'',
'Initial version',
p.owner_id,
'DRAFT',
NULL,
NULL,
p.created_at,
p.updated_at
FROM policies p;
ALTER TABLE policies RENAME COLUMN name TO title;
ALTER TABLE policies DROP COLUMN content;
ALTER TABLE policies ADD COLUMN description TEXT NOT NULL DEFAULT '';
ALTER TABLE policies ALTER COLUMN owner_id DROP NOT NULL;
ALTER TABLE policies DROP COLUMN review_date;
ALTER TABLE policies DROP COLUMN status;
ALTER TABLE policies ADD COLUMN current_published_version INTEGER;
UPDATE policies p
SET current_published_version = 1
WHERE EXISTS (
SELECT 1 FROM policy_versions pv
WHERE pv.policy_id = p.id
AND pv.status = 'published'
);

View File

@@ -0,0 +1,22 @@
CREATE TYPE policy_status_new AS ENUM ('DRAFT', 'PUBLISHED');
ALTER TABLE policy_versions ADD COLUMN temp_status TEXT;
UPDATE policy_versions SET temp_status =
CASE WHEN status::TEXT = 'ACTIVE' THEN 'PUBLISHED'
ELSE status::TEXT
END;
ALTER TABLE policy_versions DROP COLUMN status;
ALTER TABLE policy_versions ADD COLUMN status policy_status_new;
UPDATE policy_versions SET status = temp_status::policy_status_new;
ALTER TABLE policy_versions DROP COLUMN temp_status;
DROP TYPE policy_status;
ALTER TYPE policy_status_new RENAME TO policy_status;
CREATE UNIQUE INDEX policy_one_draft_version_idx ON policy_versions (policy_id, status)
WHERE status = 'DRAFT';

View File

@@ -0,0 +1,15 @@
CREATE TYPE policy_version_signature_state AS ENUM ('REQUESTED', 'SIGNED');
CREATE TABLE policy_version_signatures (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
policy_version_id TEXT NOT NULL REFERENCES policy_versions(id) ON DELETE CASCADE,
state policy_version_signature_state NOT NULL,
signed_by TEXT NOT NULL REFERENCES peoples(id),
signed_at TIMESTAMP WITH TIME ZONE,
requested_at TIMESTAMP WITH TIME ZONE NOT NULL,
requested_by TEXT NOT NULL REFERENCES peoples(id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE (policy_version_id, signed_by)
);

View File

@@ -102,7 +102,6 @@ func (p *People) LoadByUserID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
userID gid.GID,
) error {
q := `
@@ -120,14 +119,13 @@ FROM
peoples
WHERE
%s
AND organization_id = @organization_id
AND user_id = @user_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"organization_id": organizationID, "user_id": userID}
args := pgx.StrictNamedArgs{"user_id": userID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)

View File

@@ -1,3 +1,17 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
@@ -14,15 +28,13 @@ import (
type (
Policy struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
OwnerID gid.GID `db:"owner_id"`
Status PolicyStatus `db:"status"`
Name string `db:"name"`
Content string `db:"content"`
ReviewDate *time.Time `db:"review_date"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
OwnerID gid.GID `db:"owner_id"`
Title string `db:"title"`
CurrentPublishedVersion *int `db:"current_published_version"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Policies []*Policy
@@ -32,8 +44,8 @@ func (p Policy) CursorKey(orderBy PolicyOrderField) page.CursorKey {
switch orderBy {
case PolicyOrderFieldCreatedAt:
return page.NewCursorKey(p.ID, p.CreatedAt)
case PolicyOrderFieldName:
return page.NewCursorKey(p.ID, p.Name)
case PolicyOrderFieldTitle:
return page.NewCursorKey(p.ID, p.Title)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
@@ -50,10 +62,8 @@ SELECT
id,
organization_id,
owner_id,
name,
status,
content,
review_date,
title,
current_published_version,
created_at,
updated_at
FROM
@@ -93,13 +103,11 @@ func (p *Policies) LoadByOrganizationID(
) error {
q := `
SELECT
id,
id,
organization_id,
owner_id,
name,
status,
content,
review_date,
title,
current_published_version,
created_at,
updated_at
FROM
@@ -140,41 +148,35 @@ func (p Policy) Insert(
INSERT INTO
policies (
tenant_id,
id,
organization_id,
owner_id,
name,
status,
content,
review_date,
created_at,
updated_at
id,
organization_id,
owner_id,
title,
current_published_version,
created_at,
updated_at
)
VALUES (
@tenant_id,
@policy_id,
@organization_id,
@owner_id,
@name,
@status,
@content,
@review_date,
@title,
@current_published_version,
@created_at,
@updated_at
);
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"policy_id": p.ID,
"organization_id": p.OrganizationID,
"owner_id": p.OwnerID,
"name": p.Name,
"status": p.Status,
"content": p.Content,
"review_date": p.ReviewDate,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
"tenant_id": scope.GetTenantID(),
"policy_id": p.ID,
"organization_id": p.OrganizationID,
"owner_id": p.OwnerID,
"title": p.Title,
"current_published_version": p.CurrentPublishedVersion,
"created_at": p.CreatedAt,
"updated_at": p.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
@@ -207,10 +209,8 @@ func (p *Policy) Update(
UPDATE
policies
SET
name = @name,
status = @status,
content = @content,
review_date = @review_date,
title = @title,
current_published_version = @current_published_version,
owner_id = @owner_id,
updated_at = @updated_at
WHERE %s
@@ -219,13 +219,11 @@ WHERE %s
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"policy_id": p.ID,
"updated_at": time.Now(),
"name": p.Name,
"content": p.Content,
"status": p.Status,
"review_date": p.ReviewDate,
"owner_id": p.OwnerID,
"policy_id": p.ID,
"updated_at": time.Now(),
"title": p.Title,
"current_published_version": p.CurrentPublishedVersion,
"owner_id": p.OwnerID,
}
maps.Copy(args, scope.SQLArguments())
@@ -251,10 +249,8 @@ WITH plcs AS (
p.tenant_id,
p.organization_id,
p.owner_id,
p.name,
p.content,
p.status,
p.review_date,
p.title,
p.current_published_version,
p.created_at,
p.updated_at
FROM
@@ -268,10 +264,8 @@ SELECT
id,
organization_id,
owner_id,
name,
content,
status,
review_date,
title,
current_published_version,
created_at,
updated_at
FROM
@@ -314,10 +308,8 @@ WITH plcs AS (
p.tenant_id,
p.organization_id,
p.owner_id,
p.name,
p.content,
p.status,
p.review_date,
p.title,
p.current_published_version,
p.created_at,
p.updated_at
FROM
@@ -331,10 +323,8 @@ SELECT
id,
organization_id,
owner_id,
name,
content,
status,
review_date,
title,
current_published_version,
created_at,
updated_at
FROM

View File

@@ -20,7 +20,7 @@ type (
const (
PolicyOrderFieldCreatedAt PolicyOrderField = "CREATED_AT"
PolicyOrderFieldName PolicyOrderField = "NAME"
PolicyOrderFieldTitle PolicyOrderField = "TITLE"
)
func (p PolicyOrderField) Column() string {

View File

@@ -25,7 +25,7 @@ type (
const (
PolicyStatusDraft PolicyStatus = iota
PolicyStatusActive
PolicyStatusPublished
)
func (ps PolicyStatus) MarshalText() ([]byte, error) {
@@ -38,8 +38,8 @@ func (ps *PolicyStatus) UnmarshalText(data []byte) error {
switch val {
case PolicyStatusDraft.String():
*ps = PolicyStatusDraft
case PolicyStatusActive.String():
*ps = PolicyStatusActive
case PolicyStatusPublished.String():
*ps = PolicyStatusPublished
default:
return fmt.Errorf("invalid PolicyStatus value: %q", val)
}
@@ -53,8 +53,8 @@ func (ps PolicyStatus) String() string {
switch ps {
case PolicyStatusDraft:
val = "DRAFT"
case PolicyStatusActive:
val = "ACTIVE"
case PolicyStatusPublished:
val = "PUBLISHED"
}
return val

View File

@@ -0,0 +1,344 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
PolicyVersion struct {
ID gid.GID `db:"id"`
PolicyID gid.GID `db:"policy_id"`
VersionNumber int `db:"version_number"`
Content string `db:"content"`
Changelog string `db:"changelog"`
CreatedBy gid.GID `db:"created_by"`
Status PolicyStatus `db:"status"`
PublishedBy *gid.GID `db:"published_by"`
PublishedAt *time.Time `db:"published_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
PolicyVersions []*PolicyVersion
)
func (p *PolicyVersions) LoadByPolicyID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
policyID gid.GID,
cursor *page.Cursor[PolicyVersionOrderField],
) error {
q := `
SELECT
id,
policy_id,
version_number,
content,
changelog,
created_by,
status,
published_by,
published_at,
created_at,
updated_at
FROM
policy_versions
WHERE
%s
AND policy_id = @policy_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"policy_id": policyID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query policy versions: %w", err)
}
policyVersions, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[PolicyVersion])
if err != nil {
return fmt.Errorf("cannot collect policy versions: %w", err)
}
*p = policyVersions
return nil
}
func (p PolicyVersion) CursorKey(orderBy PolicyVersionOrderField) page.CursorKey {
switch orderBy {
case PolicyVersionOrderFieldCreatedAt:
return page.NewCursorKey(p.ID, p.CreatedAt)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (p *PolicyVersion) LoadByID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
policyVersionID gid.GID,
) error {
q := `
SELECT
id,
policy_id,
version_number,
content,
changelog,
created_by,
status,
published_by,
published_at,
created_at,
updated_at
FROM
policy_versions
WHERE
%s
AND id = @policy_version_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"policy_version_id": policyVersionID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query policy versions: %w", err)
}
policyVersion, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[PolicyVersion])
if err != nil {
return fmt.Errorf("cannot collect policy version: %w", err)
}
*p = policyVersion
return nil
}
func (p PolicyVersion) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO policy_versions (
tenant_id,
id,
policy_id,
version_number,
content,
changelog,
created_by,
status,
created_at,
updated_at
) VALUES (
@tenant_id,
@id,
@policy_id,
@version_number,
@content,
@changelog,
@created_by,
@status,
@created_at,
@updated_at
)
`
now := time.Now()
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"id": p.ID,
"policy_id": p.PolicyID,
"version_number": p.VersionNumber,
"content": p.Content,
"changelog": p.Changelog,
"created_by": p.CreatedBy,
"status": PolicyStatusDraft,
"created_at": now,
"updated_at": now,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("error creating/updating policy version: %w", err)
}
return nil
}
func (p *PolicyVersion) LoadByPolicyIDAndVersionNumber(
ctx context.Context,
conn pg.Conn,
scope Scoper,
policyID gid.GID,
versionNumber int,
) error {
q := `
SELECT
id,
policy_id,
version_number,
content,
changelog,
created_by,
status,
published_by,
published_at,
created_at,
updated_at
FROM
policy_versions
WHERE
%s
AND policy_id = @policy_id
AND version_number = @version_number
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"policy_id": policyID,
"version_number": versionNumber,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query policy versions: %w", err)
}
policyVersion, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[PolicyVersion])
if err != nil {
return fmt.Errorf("cannot collect policy version: %w", err)
}
*p = policyVersion
return nil
}
func (p *PolicyVersion) LoadLatestVersion(
ctx context.Context,
conn pg.Conn,
scope Scoper,
policyID gid.GID,
) error {
q := `
SELECT
id,
policy_id,
version_number,
content,
changelog,
created_by,
status,
published_by,
published_at,
created_at,
updated_at
FROM
policy_versions
WHERE
%s
AND policy_id = @policy_id
ORDER BY created_at DESC
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"policy_id": policyID,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query policy versions: %w", err)
}
policyVersion, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[PolicyVersion])
if err != nil {
return fmt.Errorf("cannot collect policy version: %w", err)
}
*p = policyVersion
return nil
}
func (p PolicyVersion) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE policy_versions SET
changelog = @changelog,
status = @status,
content = @content,
published_by = @published_by,
published_at = @published_at,
updated_at = @updated_at
WHERE %s
AND id = @policy_version_id;`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"policy_version_id": p.ID,
"changelog": p.Changelog,
"status": p.Status,
"content": p.Content,
"published_by": p.PublishedBy,
"published_at": p.PublishedAt,
"updated_at": p.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot update policy version: %w", err)
}
return nil
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
type (
PolicyVersionOrderField string
)
const (
PolicyVersionOrderFieldCreatedAt PolicyVersionOrderField = "CREATED_AT"
PolicyVersionOrderFieldVersion PolicyVersionOrderField = "VERSION"
)
func (p PolicyVersionOrderField) Column() string {
return string(p)
}
func (p PolicyVersionOrderField) String() string {
return string(p)
}
func (p PolicyVersionOrderField) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *PolicyVersionOrderField) UnmarshalText(text []byte) error {
*p = PolicyVersionOrderField(text)
return nil
}

View File

@@ -0,0 +1,197 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
PolicyVersionSignature struct {
ID gid.GID `json:"id"`
PolicyVersionID gid.GID `json:"policy_version_id"`
State PolicyVersionSignatureState `json:"state"`
SignedBy gid.GID `json:"signed_by"`
SignedAt *time.Time `json:"signed_at"`
RequestedAt time.Time `json:"requested_at"`
RequestedBy gid.GID `json:"requested_by"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
PolicyVersionSignatures []*PolicyVersionSignature
)
func (pvs PolicyVersionSignature) CursorKey(orderBy PolicyVersionSignatureOrderField) page.CursorKey {
switch orderBy {
case PolicyVersionSignatureOrderFieldCreatedAt:
return page.NewCursorKey(pvs.ID, pvs.CreatedAt)
case PolicyVersionSignatureOrderFieldSignedAt:
return page.NewCursorKey(pvs.ID, pvs.SignedAt)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (pvs *PolicyVersionSignature) LoadByID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
signatureID gid.GID,
) error {
q := `
SELECT
id,
policy_version_id,
state,
signed_by,
signed_at,
requested_at,
requested_by,
created_at,
updated_at
FROM
policy_version_signatures
WHERE
id = @policy_version_signature_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"policy_version_signature_id": signatureID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query policy version signature: %w", err)
}
policyVersionSignature, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[PolicyVersionSignature])
if err != nil {
return fmt.Errorf("cannot collect policy version signature: %w", err)
}
*pvs = policyVersionSignature
return nil
}
func (pvs PolicyVersionSignature) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO policy_version_signatures (
id,
tenant_id,
policy_version_id,
state,
signed_by,
signed_at,
requested_at,
requested_by,
created_at,
updated_at
) VALUES (
@id,
@tenant_id,
@policy_version_id,
@state,
@signed_by,
@signed_at,
@requested_at,
@requested_by,
@created_at,
@updated_at
)
`
args := pgx.StrictNamedArgs{
"id": pvs.ID,
"tenant_id": scope.GetTenantID(),
"policy_version_id": pvs.PolicyVersionID,
"state": pvs.State,
"signed_by": pvs.SignedBy,
"signed_at": pvs.SignedAt,
"requested_at": pvs.RequestedAt,
"requested_by": pvs.RequestedBy,
"created_at": pvs.CreatedAt,
"updated_at": pvs.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot insert policy version signature: %w", err)
}
return nil
}
func (pvss *PolicyVersionSignatures) LoadByPolicyVersionID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
policyVersionID gid.GID,
cursor *page.Cursor[PolicyVersionSignatureOrderField],
) error {
q := `
SELECT
id,
policy_version_id,
state,
signed_by,
signed_at,
requested_at,
requested_by,
created_at,
updated_at
FROM
policy_version_signatures
WHERE
%s
AND policy_version_id = @policy_version_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"policy_version_id": policyVersionID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query policy version signatures: %w", err)
}
policyVersionSignatures, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[PolicyVersionSignature])
if err != nil {
return fmt.Errorf("cannot collect policy version signatures: %w", err)
}
*pvss = policyVersionSignatures
return nil
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
type (
PolicyVersionSignatureOrderField string
)
const (
PolicyVersionSignatureOrderFieldCreatedAt PolicyVersionSignatureOrderField = "CREATED_AT"
PolicyVersionSignatureOrderFieldSignedAt PolicyVersionSignatureOrderField = "SIGNED_AT"
)
func (p PolicyVersionSignatureOrderField) Column() string {
return string(p)
}
func (p PolicyVersionSignatureOrderField) String() string {
return string(p)
}
func (p PolicyVersionSignatureOrderField) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *PolicyVersionSignatureOrderField) UnmarshalText(text []byte) error {
*p = PolicyVersionSignatureOrderField(text)
return nil
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"database/sql/driver"
"fmt"
)
type (
PolicyVersionSignatureState string
)
const (
PolicyVersionSignatureStateRequested PolicyVersionSignatureState = "REQUESTED"
PolicyVersionSignatureStateSigned PolicyVersionSignatureState = "SIGNED"
)
func (pvs PolicyVersionSignatureState) MarshalText() ([]byte, error) {
return []byte(pvs.String()), nil
}
func (pvs *PolicyVersionSignatureState) UnmarshalText(data []byte) error {
val := string(data)
switch val {
case PolicyVersionSignatureStateRequested.String():
*pvs = PolicyVersionSignatureStateRequested
case PolicyVersionSignatureStateSigned.String():
*pvs = PolicyVersionSignatureStateSigned
default:
return fmt.Errorf("invalid MesureState value: %q", val)
}
return nil
}
func (pvs PolicyVersionSignatureState) String() string {
var val string
switch pvs {
case PolicyVersionSignatureStateRequested:
val = "REQUESTED"
case PolicyVersionSignatureStateSigned:
val = "SIGNED"
}
return val
}
func (pvs *PolicyVersionSignatureState) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for MesureState, expected string got %T", value)
}
return pvs.UnmarshalText([]byte(val))
}
func (pvs PolicyVersionSignatureState) Value() (driver.Value, error) {
return pvs.String(), nil
}

View File

@@ -71,7 +71,6 @@ func (s PeopleService) Get(
func (s PeopleService) GetByUserID(
ctx context.Context,
organizationID gid.GID,
userID gid.GID,
) (*coredata.People, error) {
people := &coredata.People{}
@@ -79,7 +78,7 @@ func (s PeopleService) GetByUserID(
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return people.LoadByUserID(ctx, conn, s.svc.scope, organizationID, userID)
return people.LoadByUserID(ctx, conn, s.svc.scope, userID)
},
)

View File

@@ -18,21 +18,21 @@ type PolicyService struct {
type (
CreatePolicyRequest struct {
OrganizationID gid.GID
Name string
Status coredata.PolicyStatus
Title string
Content string
ReviewDate *time.Time
OwnerID gid.GID
CreatedBy gid.GID
}
UpdatePolicyRequest struct {
ID gid.GID
ExpectedVersion int
Name *string
Content *string
Status *coredata.PolicyStatus
ReviewDate *time.Time
OwnerID *gid.GID
UpdatePolicyVersionRequest struct {
ID gid.GID
Content string
}
RequestSignatureRequest struct {
PolicyVersionID gid.GID
RequestedBy gid.GID
Signatory gid.GID
}
)
@@ -56,38 +56,203 @@ func (s *PolicyService) Get(
return policy, nil
}
func (s *PolicyService) PublishVersion(
ctx context.Context,
policyID gid.GID,
publishedBy gid.GID,
) (*coredata.Policy, *coredata.PolicyVersion, error) {
policy := &coredata.Policy{}
policyVersion := &coredata.PolicyVersion{}
now := time.Now()
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := policy.LoadByID(ctx, tx, s.svc.scope, policyID); err != nil {
return fmt.Errorf("cannot load policy %q: %w", policyID, err)
}
if err := policyVersion.LoadLatestVersion(ctx, tx, s.svc.scope, policyID); err != nil {
return fmt.Errorf("cannot load current draft: %w", err)
}
if policyVersion.Status != coredata.PolicyStatusDraft {
return fmt.Errorf("cannot publish version")
}
policy.CurrentPublishedVersion = &policyVersion.VersionNumber
policy.UpdatedAt = now
policyVersion.Status = coredata.PolicyStatusPublished
policyVersion.PublishedAt = &now
policyVersion.PublishedBy = &publishedBy
policyVersion.UpdatedAt = now
if err := policy.Update(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot update policy: %w", err)
}
if err := policyVersion.Update(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot update policy version: %w", err)
}
return nil
},
)
if err != nil {
return nil, nil, err
}
return policy, policyVersion, nil
}
func (s *PolicyService) Create(
ctx context.Context,
req CreatePolicyRequest,
) (*coredata.Policy, error) {
) (*coredata.Policy, *coredata.PolicyVersion, error) {
now := time.Now()
policyID, err := gid.NewGID(s.svc.scope.GetTenantID(), coredata.PolicyEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create policy global id: %w", err)
return nil, nil, fmt.Errorf("cannot create policy global id: %w", err)
}
policyVersionID, err := gid.NewGID(s.svc.scope.GetTenantID(), coredata.PolicyVersionEntityType)
if err != nil {
return nil, nil, fmt.Errorf("cannot create policy version global id: %w", err)
}
organization := &coredata.Organization{}
policy := &coredata.Policy{
ID: policyID,
OrganizationID: req.OrganizationID,
OwnerID: req.OwnerID,
Name: req.Name,
Content: req.Content,
Status: req.Status,
ReviewDate: req.ReviewDate,
Title: req.Title,
CreatedAt: now,
UpdatedAt: now,
}
policyVersion := &coredata.PolicyVersion{
ID: policyVersionID,
PolicyID: policyID,
VersionNumber: 1,
Content: req.Content,
CreatedBy: req.CreatedBy,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := policy.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert policy: %w", err)
}
if err := policyVersion.Update(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot create policy version: %w", err)
}
return nil
},
)
if err != nil {
return nil, nil, err
}
return policy, policyVersion, nil
}
func (s *PolicyService) UpdateVersion(
ctx context.Context,
req UpdatePolicyVersionRequest,
) (*coredata.PolicyVersion, error) {
policyVersion := &coredata.PolicyVersion{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := policyVersion.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil {
return fmt.Errorf("cannot load policy version %q: %w", req.ID, err)
}
if policyVersion.Status != coredata.PolicyStatusDraft {
return fmt.Errorf("cannot update published version")
}
policyVersion.Content = req.Content
policyVersion.UpdatedAt = time.Now()
if err := policyVersion.Update(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot update policy version: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return policyVersion, nil
}
func (s *PolicyService) GetVersionSignature(
ctx context.Context,
signatureID gid.GID,
) (*coredata.PolicyVersionSignature, error) {
policyVersionSignature := &coredata.PolicyVersionSignature{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return policyVersionSignature.LoadByID(ctx, conn, s.svc.scope, signatureID)
},
)
if err != nil {
return nil, err
}
return policyVersionSignature, nil
}
func (s *PolicyService) RequestSignature(
ctx context.Context,
req RequestSignatureRequest,
) (*coredata.PolicyVersionSignature, error) {
policyVersionSignatureID, err := gid.NewGID(s.svc.scope.GetTenantID(), coredata.PolicyVersionSignatureEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create policy version signature global id: %w", err)
}
policyVersion, err := s.GetVersion(ctx, req.PolicyVersionID)
if err != nil {
return nil, fmt.Errorf("cannot get policy version: %w", err)
}
if policyVersion.Status != coredata.PolicyStatusPublished {
return nil, fmt.Errorf("cannot request signature for unpublished version")
}
now := time.Now()
policyVersionSignature := &coredata.PolicyVersionSignature{
ID: policyVersionSignatureID,
PolicyVersionID: req.PolicyVersionID,
State: coredata.PolicyVersionSignatureStateRequested,
RequestedBy: req.RequestedBy,
RequestedAt: now,
SignedBy: req.Signatory,
SignedAt: nil,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID); err != nil {
return fmt.Errorf("cannot load organization %q: %w", req.OrganizationID, err)
}
if err := policy.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert policy: %w", err)
if err := policyVersionSignature.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert policy version signature: %w", err)
}
return nil
@@ -98,54 +263,77 @@ func (s *PolicyService) Create(
return nil, err
}
return policy, nil
return policyVersionSignature, nil
}
func (s *PolicyService) Update(
func (s *PolicyService) ListSignatures(
ctx context.Context,
req UpdatePolicyRequest,
) (*coredata.Policy, error) {
policy := &coredata.Policy{}
policyVersionID gid.GID,
cursor *page.Cursor[coredata.PolicyVersionSignatureOrderField],
) (*page.Page[*coredata.PolicyVersionSignature, coredata.PolicyVersionSignatureOrderField], error) {
var policyVersionSignatures coredata.PolicyVersionSignatures
err := s.svc.pg.WithTx(
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := policy.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil {
return fmt.Errorf("cannot load policy %q: %w", req.ID, err)
return policyVersionSignatures.LoadByPolicyVersionID(ctx, conn, s.svc.scope, policyVersionID, cursor)
},
)
if err != nil {
return nil, err
}
return page.NewPage(policyVersionSignatures, cursor), nil
}
func (s *PolicyService) CreateDraft(
ctx context.Context,
policyID gid.GID,
createdBy gid.GID,
) (*coredata.PolicyVersion, error) {
draftVersionID, err := gid.NewGID(s.svc.scope.GetTenantID(), coredata.PolicyVersionEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create policy version global id: %w", err)
}
latestVersion := &coredata.PolicyVersion{}
draftVersion := &coredata.PolicyVersion{}
now := time.Now()
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := latestVersion.LoadLatestVersion(ctx, conn, s.svc.scope, policyID); err != nil {
return fmt.Errorf("cannot load latest version: %w", err)
}
if req.Name != nil {
policy.Name = *req.Name
if latestVersion.Status != coredata.PolicyStatusPublished {
return fmt.Errorf("cannot create draft from unpublished version")
}
if req.Content != nil {
policy.Content = *req.Content
}
draftVersion.ID = draftVersionID
draftVersion.PolicyID = policyID
draftVersion.VersionNumber = latestVersion.VersionNumber + 1
draftVersion.Content = latestVersion.Content
draftVersion.Status = coredata.PolicyStatusDraft
draftVersion.CreatedBy = createdBy
draftVersion.CreatedAt = now
draftVersion.UpdatedAt = now
if req.Status != nil {
policy.Status = *req.Status
}
if req.ReviewDate != nil {
policy.ReviewDate = req.ReviewDate
}
if req.OwnerID != nil {
policy.OwnerID = *req.OwnerID
}
if err := policy.Update(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot update policy: %w", err)
if err := draftVersion.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot create draft: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return policy, nil
return draftVersion, nil
}
func (s *PolicyService) Delete(
@@ -162,6 +350,47 @@ func (s *PolicyService) Delete(
)
}
func (s *PolicyService) ListVersions(
ctx context.Context,
policyID gid.GID,
cursor *page.Cursor[coredata.PolicyVersionOrderField],
) (*page.Page[*coredata.PolicyVersion, coredata.PolicyVersionOrderField], error) {
var policyVersions coredata.PolicyVersions
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return policyVersions.LoadByPolicyID(ctx, conn, s.svc.scope, policyID, cursor)
},
)
if err != nil {
return nil, err
}
return page.NewPage(policyVersions, cursor), nil
}
func (s *PolicyService) GetVersion(
ctx context.Context,
policyVersionID gid.GID,
) (*coredata.PolicyVersion, error) {
policyVersion := &coredata.PolicyVersion{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return policyVersion.LoadByID(ctx, conn, s.svc.scope, policyVersionID)
},
)
if err != nil {
return nil, err
}
return policyVersion, nil
}
func (s *PolicyService) ListByOrganizationID(
ctx context.Context,
organizationID gid.GID,

View File

@@ -95,8 +95,8 @@ enum PolicyStatus
@goModel(model: "github.com/getprobo/probo/pkg/coredata.PolicyStatus") {
DRAFT
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.PolicyStatusDraft")
ACTIVE
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.PolicyStatusActive")
PUBLISHED
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.PolicyStatusPublished")
}
enum EvidenceType
@@ -184,7 +184,14 @@ enum TaskOrderField
enum PolicyOrderField
@goModel(model: "github.com/getprobo/probo/pkg/coredata.PolicyOrderField") {
NAME
TITLE
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyOrderFieldTitle"
)
CREATED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyOrderFieldCreatedAt"
)
}
enum RiskOrderField
@@ -260,6 +267,18 @@ enum BusinessImpact
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.BusinessImpactCritical")
}
enum PolicyVersionOrderField
@goModel(model: "github.com/getprobo/probo/pkg/coredata.PolicyVersionOrderField") {
VERSION
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyVersionOrderFieldVersion"
)
CREATED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyVersionOrderFieldCreatedAt"
)
}
# Order Input Types
input UserOrder
@goModel(
@@ -359,6 +378,18 @@ input ConnectorOrder {
direction: OrderDirection!
}
input PolicyVersionOrder
@goModel(
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.PolicyVersionOrderBy"
) {
direction: OrderDirection!
field: PolicyVersionOrderField!
}
input PolicyVersionFilter {
status: PolicyStatus
}
# Core Types
type Organization implements Node {
id: ID!
@@ -631,12 +662,20 @@ type Evidence implements Node {
type Policy implements Node {
id: ID!
name: String!
status: PolicyStatus!
content: String!
reviewDate: Datetime
title: String!
description: String!
currentPublishedVersion: Int
owner: People! @goField(forceResolver: true)
versions(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: PolicyVersionOrder
filter: PolicyVersionFilter
): PolicyVersionConnection! @goField(forceResolver: true)
controls(
first: Int
after: CursorKey
@@ -852,6 +891,16 @@ type VendorRiskAssessmentEdge {
node: VendorRiskAssessment!
}
type PolicyVersionConnection {
edges: [PolicyVersionEdge!]!
pageInfo: PageInfo!
}
type PolicyVersionEdge {
cursor: CursorKey!
node: PolicyVersion!
}
# Root Types
type Query {
node(id: ID!): Node!
@@ -952,8 +1001,11 @@ type Mutation {
# Policy mutations
createPolicy(input: CreatePolicyInput!): CreatePolicyPayload!
updatePolicy(input: UpdatePolicyInput!): UpdatePolicyPayload!
deletePolicy(input: DeletePolicyInput!): DeletePolicyPayload!
publishPolicyVersion(input: PublishPolicyVersionInput!): PublishPolicyVersionPayload!
createDraftPolicyVersion(input: CreateDraftPolicyVersionInput!): CreateDraftPolicyVersionPayload!
updatePolicyVersion(input: UpdatePolicyVersionInput!): UpdatePolicyVersionPayload!
requestSignature(input: RequestSignatureInput!): RequestSignaturePayload!
createVendorRiskAssessment(input: CreateVendorRiskAssessmentInput!): CreateVendorRiskAssessmentPayload!
}
@@ -1224,20 +1276,17 @@ input DeleteVendorComplianceReportInput {
input CreatePolicyInput {
organizationId: ID!
name: String!
title: String!
content: String!
status: PolicyStatus!
reviewDate: Datetime
ownerId: ID!
}
input UpdatePolicyInput {
id: ID!
name: String
title: String
content: String
status: PolicyStatus
reviewDate: Datetime
ownerId: ID
createdBy: ID
}
input DeletePolicyInput {
@@ -1414,6 +1463,7 @@ type DeleteVendorComplianceReportPayload {
type CreatePolicyPayload {
policyEdge: PolicyEdge!
policyVersionEdge: PolicyVersionEdge!
}
type UpdatePolicyPayload {
@@ -1489,4 +1539,112 @@ input DeleteMesureInput {
type DeleteMesurePayload {
deletedMesureId: ID!
}
}
type PolicyVersion implements Node {
id: ID!
policy: Policy! @goField(forceResolver: true)
status: PolicyStatus!
version: Int!
content: String!
changelog: String!
signatures(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: PolicyVersionSignatureOrder
): PolicyVersionSignatureConnection! @goField(forceResolver: true)
publishedBy: People @goField(forceResolver: true)
publishedAt: Datetime
createdAt: Datetime!
updatedAt: Datetime!
}
type PolicyVersionSignatureConnection {
edges: [PolicyVersionSignatureEdge!]!
pageInfo: PageInfo!
}
type PolicyVersionSignatureEdge {
cursor: CursorKey!
node: PolicyVersionSignature!
}
input PolicyVersionSignatureOrder {
field: PolicyVersionSignatureOrderField!
direction: OrderDirection!
}
enum PolicyVersionSignatureState
@goModel(model: "github.com/getprobo/probo/pkg/coredata.PolicyVersionSignatureState") {
REQUESTED
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyVersionSignatureStateRequested"
)
SIGNED
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyVersionSignatureStateSigned"
)
}
enum PolicyVersionSignatureOrderField
@goModel(model: "github.com/getprobo/probo/pkg/coredata.PolicyVersionSignatureOrderField") {
CREATED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyVersionSignatureOrderFieldCreatedAt"
)
SIGNED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.PolicyVersionSignatureOrderFieldSignedAt"
)
}
type PolicyVersionSignature implements Node {
id: ID!
policyVersion: PolicyVersion! @goField(forceResolver: true)
state: PolicyVersionSignatureState!
signedBy: People! @goField(forceResolver: true)
signedAt: Datetime
requestedAt: Datetime!
requestedBy: People! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
input RequestSignatureInput {
policyVersionId: ID!
signatoryId: ID!
}
type RequestSignaturePayload {
policyVersionSignatureEdge: PolicyVersionSignatureEdge!
}
input PublishPolicyVersionInput {
policyId: ID!
}
type PublishPolicyVersionPayload {
policyVersion: PolicyVersion!
policy: Policy!
}
type CreateDraftPolicyVersionPayload {
policyVersionEdge: PolicyVersionEdge!
}
input CreateDraftPolicyVersionInput {
policyID: ID!
}
input UpdatePolicyVersionInput {
policyVersionId: ID!
content: String!
}
type UpdatePolicyVersionPayload {
policyVersion: PolicyVersion!
}

File diff suppressed because it is too large Load Diff

View File

@@ -44,12 +44,10 @@ func NewPolicyEdge(policy *coredata.Policy, orderBy coredata.PolicyOrderField) *
func NewPolicy(policy *coredata.Policy) *Policy {
return &Policy{
ID: policy.ID,
Name: policy.Name,
Content: policy.Content,
CreatedAt: policy.CreatedAt,
UpdatedAt: policy.UpdatedAt,
Status: policy.Status,
ReviewDate: policy.ReviewDate,
ID: policy.ID,
Title: policy.Title,
CurrentPublishedVersion: policy.CurrentPublishedVersion,
CreatedAt: policy.CreatedAt,
UpdatedAt: policy.UpdatedAt,
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/page"
)
type (
PolicyVersionOrderBy OrderBy[coredata.PolicyVersionOrderField]
)
func NewPolicyVersionConnection(page *page.Page[*coredata.PolicyVersion, coredata.PolicyVersionOrderField]) *PolicyVersionConnection {
edges := make([]*PolicyVersionEdge, len(page.Data))
for i, policyVersion := range page.Data {
edges[i] = NewPolicyVersionEdge(policyVersion, page.Cursor.OrderBy.Field)
}
return &PolicyVersionConnection{
Edges: edges,
PageInfo: NewPageInfo(page),
}
}
func NewPolicyVersionEdge(policyVersion *coredata.PolicyVersion, orderBy coredata.PolicyVersionOrderField) *PolicyVersionEdge {
return &PolicyVersionEdge{
Cursor: policyVersion.CursorKey(orderBy),
Node: NewPolicyVersion(policyVersion),
}
}
func NewPolicyVersion(policyVersion *coredata.PolicyVersion) *PolicyVersion {
return &PolicyVersion{
ID: policyVersion.ID,
Version: policyVersion.VersionNumber,
Content: policyVersion.Content,
Status: policyVersion.Status,
PublishedAt: policyVersion.PublishedAt,
Changelog: policyVersion.Changelog,
CreatedAt: policyVersion.CreatedAt,
UpdatedAt: policyVersion.UpdatedAt,
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/page"
)
type (
PolicyVersionSignatureOrderBy OrderBy[coredata.PolicyVersionSignatureOrderField]
)
func NewPolicyVersionSignatureConnection(page *page.Page[*coredata.PolicyVersionSignature, coredata.PolicyVersionSignatureOrderField]) *PolicyVersionSignatureConnection {
edges := make([]*PolicyVersionSignatureEdge, len(page.Data))
for i, policyVersionSignature := range page.Data {
edges[i] = NewPolicyVersionSignatureEdge(policyVersionSignature, page.Cursor.OrderBy.Field)
}
return &PolicyVersionSignatureConnection{
Edges: edges,
PageInfo: NewPageInfo(page),
}
}
func NewPolicyVersionSignatureEdge(policyVersionSignature *coredata.PolicyVersionSignature, orderBy coredata.PolicyVersionSignatureOrderField) *PolicyVersionSignatureEdge {
return &PolicyVersionSignatureEdge{
Cursor: policyVersionSignature.CursorKey(orderBy),
Node: NewPolicyVersionSignature(policyVersionSignature),
}
}
func NewPolicyVersionSignature(policyVersionSignature *coredata.PolicyVersionSignature) *PolicyVersionSignature {
return &PolicyVersionSignature{
ID: policyVersionSignature.ID,
State: policyVersionSignature.State,
SignedAt: policyVersionSignature.SignedAt,
RequestedAt: policyVersionSignature.RequestedAt,
CreatedAt: policyVersionSignature.CreatedAt,
UpdatedAt: policyVersionSignature.UpdatedAt,
}
}

View File

@@ -104,6 +104,14 @@ type CreateControlPolicyMappingPayload struct {
Success bool `json:"success"`
}
type CreateDraftPolicyVersionInput struct {
PolicyID gid.GID `json:"policyID"`
}
type CreateDraftPolicyVersionPayload struct {
PolicyVersionEdge *PolicyVersionEdge `json:"policyVersionEdge"`
}
type CreateEvidenceInput struct {
TaskID gid.GID `json:"taskId"`
Name string `json:"name"`
@@ -159,16 +167,15 @@ type CreatePeoplePayload struct {
}
type CreatePolicyInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
Content string `json:"content"`
Status coredata.PolicyStatus `json:"status"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
OwnerID gid.GID `json:"ownerId"`
OrganizationID gid.GID `json:"organizationId"`
Title string `json:"title"`
Content string `json:"content"`
OwnerID gid.GID `json:"ownerId"`
}
type CreatePolicyPayload struct {
PolicyEdge *PolicyEdge `json:"policyEdge"`
PolicyEdge *PolicyEdge `json:"policyEdge"`
PolicyVersionEdge *PolicyVersionEdge `json:"policyVersionEdge"`
}
type CreateRiskInput struct {
@@ -556,15 +563,15 @@ type PeopleEdge struct {
}
type Policy struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
Status coredata.PolicyStatus `json:"status"`
Content string `json:"content"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
Owner *People `json:"owner"`
Controls *ControlConnection `json:"controls"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
CurrentPublishedVersion *int `json:"currentPublishedVersion,omitempty"`
Owner *People `json:"owner"`
Versions *PolicyVersionConnection `json:"versions"`
Controls *ControlConnection `json:"controls"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Policy) IsNode() {}
@@ -580,6 +587,76 @@ type PolicyEdge struct {
Node *Policy `json:"node"`
}
type PolicyVersion struct {
ID gid.GID `json:"id"`
Policy *Policy `json:"policy"`
Status coredata.PolicyStatus `json:"status"`
Version int `json:"version"`
Content string `json:"content"`
Changelog string `json:"changelog"`
Signatures *PolicyVersionSignatureConnection `json:"signatures"`
PublishedBy *People `json:"publishedBy,omitempty"`
PublishedAt *time.Time `json:"publishedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (PolicyVersion) IsNode() {}
func (this PolicyVersion) GetID() gid.GID { return this.ID }
type PolicyVersionConnection struct {
Edges []*PolicyVersionEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type PolicyVersionEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *PolicyVersion `json:"node"`
}
type PolicyVersionFilter struct {
Status *coredata.PolicyStatus `json:"status,omitempty"`
}
type PolicyVersionSignature struct {
ID gid.GID `json:"id"`
PolicyVersion *PolicyVersion `json:"policyVersion"`
State coredata.PolicyVersionSignatureState `json:"state"`
SignedBy *People `json:"signedBy"`
SignedAt *time.Time `json:"signedAt,omitempty"`
RequestedAt time.Time `json:"requestedAt"`
RequestedBy *People `json:"requestedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (PolicyVersionSignature) IsNode() {}
func (this PolicyVersionSignature) GetID() gid.GID { return this.ID }
type PolicyVersionSignatureConnection struct {
Edges []*PolicyVersionSignatureEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type PolicyVersionSignatureEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *PolicyVersionSignature `json:"node"`
}
type PolicyVersionSignatureOrder struct {
Field coredata.PolicyVersionSignatureOrderField `json:"field"`
Direction page.OrderDirection `json:"direction"`
}
type PublishPolicyVersionInput struct {
PolicyID gid.GID `json:"policyId"`
}
type PublishPolicyVersionPayload struct {
PolicyVersion *PolicyVersion `json:"policyVersion"`
Policy *Policy `json:"policy"`
}
type Query struct {
}
@@ -603,6 +680,15 @@ type RequestEvidencePayload struct {
EvidenceEdge *EvidenceEdge `json:"evidenceEdge"`
}
type RequestSignatureInput struct {
PolicyVersionID gid.GID `json:"policyVersionId"`
SignatoryID gid.GID `json:"signatoryId"`
}
type RequestSignaturePayload struct {
PolicyVersionSignatureEdge *PolicyVersionSignatureEdge `json:"policyVersionSignatureEdge"`
}
type Risk struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
@@ -720,18 +806,26 @@ type UpdatePeoplePayload struct {
}
type UpdatePolicyInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Content *string `json:"content,omitempty"`
Status *coredata.PolicyStatus `json:"status,omitempty"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
ID gid.GID `json:"id"`
Title *string `json:"title,omitempty"`
Content *string `json:"content,omitempty"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
CreatedBy *gid.GID `json:"createdBy,omitempty"`
}
type UpdatePolicyPayload struct {
Policy *Policy `json:"policy"`
}
type UpdatePolicyVersionInput struct {
PolicyVersionID gid.GID `json:"policyVersionId"`
Content string `json:"content"`
}
type UpdatePolicyVersionPayload struct {
PolicyVersion *PolicyVersion `json:"policyVersion"`
}
type UpdateRiskInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`

View File

@@ -1008,41 +1008,29 @@ func (r *mutationResolver) DeleteVendorComplianceReport(ctx context.Context, inp
func (r *mutationResolver) CreatePolicy(ctx context.Context, input types.CreatePolicyInput) (*types.CreatePolicyPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.OrganizationID.TenantID())
policy, err := svc.Policies.Create(ctx, probo.CreatePolicyRequest{
OrganizationID: input.OrganizationID,
Name: input.Name,
Content: input.Content,
Status: input.Status,
ReviewDate: input.ReviewDate,
OwnerID: input.OwnerID,
})
user := UserFromContext(ctx)
people, err := svc.Peoples.GetByUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("cannot get people: %w", err))
}
policy, policyVersion, err := svc.Policies.Create(
ctx,
probo.CreatePolicyRequest{
OrganizationID: input.OrganizationID,
Title: input.Title,
OwnerID: input.OwnerID,
Content: input.Content,
CreatedBy: people.ID,
},
)
if err != nil {
panic(fmt.Errorf("cannot create policy: %w", err))
}
return &types.CreatePolicyPayload{
PolicyEdge: types.NewPolicyEdge(policy, coredata.PolicyOrderFieldCreatedAt),
}, nil
}
// UpdatePolicy is the resolver for the updatePolicy field.
func (r *mutationResolver) UpdatePolicy(ctx context.Context, input types.UpdatePolicyInput) (*types.UpdatePolicyPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.ID.TenantID())
policy, err := svc.Policies.Update(ctx, probo.UpdatePolicyRequest{
ID: input.ID,
Name: input.Name,
Content: input.Content,
Status: input.Status,
ReviewDate: input.ReviewDate,
OwnerID: input.OwnerID,
})
if err != nil {
panic(fmt.Errorf("cannot update policy: %w", err))
}
return &types.UpdatePolicyPayload{
Policy: types.NewPolicy(policy),
PolicyEdge: types.NewPolicyEdge(policy, coredata.PolicyOrderFieldTitle),
PolicyVersionEdge: types.NewPolicyVersionEdge(policyVersion, coredata.PolicyVersionOrderFieldCreatedAt),
}, nil
}
@@ -1060,16 +1048,96 @@ func (r *mutationResolver) DeletePolicy(ctx context.Context, input types.DeleteP
}, nil
}
// PublishPolicyVersion is the resolver for the publishPolicyVersion field.
func (r *mutationResolver) PublishPolicyVersion(ctx context.Context, input types.PublishPolicyVersionInput) (*types.PublishPolicyVersionPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.PolicyID.TenantID())
user := UserFromContext(ctx)
people, err := svc.Peoples.GetByUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("cannot get people: %w", err))
}
policy, policyVersion, err := svc.Policies.PublishVersion(ctx, input.PolicyID, people.ID)
if err != nil {
panic(fmt.Errorf("cannot publish policy version: %w", err))
}
return &types.PublishPolicyVersionPayload{
PolicyVersion: types.NewPolicyVersion(policyVersion),
Policy: types.NewPolicy(policy),
}, nil
}
// CreateDraftPolicyVersion is the resolver for the createDraftPolicyVersion field.
func (r *mutationResolver) CreateDraftPolicyVersion(ctx context.Context, input types.CreateDraftPolicyVersionInput) (*types.CreateDraftPolicyVersionPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.PolicyID.TenantID())
user := UserFromContext(ctx)
people, err := svc.Peoples.GetByUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("cannot get people: %w", err))
}
policyVersion, err := svc.Policies.CreateDraft(ctx, input.PolicyID, people.ID)
if err != nil {
panic(fmt.Errorf("cannot create draft policy version: %w", err))
}
return &types.CreateDraftPolicyVersionPayload{
PolicyVersionEdge: types.NewPolicyVersionEdge(policyVersion, coredata.PolicyVersionOrderFieldCreatedAt),
}, nil
}
// UpdatePolicyVersion is the resolver for the updatePolicyVersion field.
func (r *mutationResolver) UpdatePolicyVersion(ctx context.Context, input types.UpdatePolicyVersionInput) (*types.UpdatePolicyVersionPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.PolicyVersionID.TenantID())
policyVersion, err := svc.Policies.UpdateVersion(ctx, probo.UpdatePolicyVersionRequest{
ID: input.PolicyVersionID,
Content: input.Content,
})
if err != nil {
panic(fmt.Errorf("cannot update policy version: %w", err))
}
return &types.UpdatePolicyVersionPayload{
PolicyVersion: types.NewPolicyVersion(policyVersion),
}, nil
}
// RequestSignature is the resolver for the requestSignature field.
func (r *mutationResolver) RequestSignature(ctx context.Context, input types.RequestSignatureInput) (*types.RequestSignaturePayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.PolicyVersionID.TenantID())
user := UserFromContext(ctx)
people, err := svc.Peoples.GetByUserID(ctx, user.ID)
if err != nil {
panic(fmt.Errorf("cannot get people: %w", err))
}
policyVersionSignature, err := svc.Policies.RequestSignature(
ctx,
probo.RequestSignatureRequest{
PolicyVersionID: input.PolicyVersionID,
RequestedBy: people.ID,
Signatory: input.SignatoryID,
},
)
if err != nil {
panic(fmt.Errorf("cannot request signature: %w", err))
}
return &types.RequestSignaturePayload{
PolicyVersionSignatureEdge: types.NewPolicyVersionSignatureEdge(policyVersionSignature, coredata.PolicyVersionSignatureOrderFieldCreatedAt),
}, nil
}
// CreateVendorRiskAssessment is the resolver for the createVendorRiskAssessment field.
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.VendorID.TenantID())
fmt.Println("input.AssessedBy", input.AssessedBy)
fmt.Println("input.ExpiresAt", input.ExpiresAt)
fmt.Println("input.DataSensitivity", input.DataSensitivity)
fmt.Println("input.BusinessImpact", input.BusinessImpact)
fmt.Println("input.Notes", input.Notes)
vendorRiskAssessment, err := svc.Vendors.CreateRiskAssessment(
ctx,
probo.CreateVendorRiskAssessmentRequest{
@@ -1225,7 +1293,7 @@ func (r *organizationResolver) Policies(ctx context.Context, obj *types.Organiza
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.PolicyOrderField]{
Field: coredata.PolicyOrderFieldName,
Field: coredata.PolicyOrderFieldTitle,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
@@ -1313,6 +1381,31 @@ func (r *policyResolver) Owner(ctx context.Context, obj *types.Policy) (*types.P
return types.NewPeople(owner), nil
}
// Versions is the resolver for the versions field.
func (r *policyResolver) Versions(ctx context.Context, obj *types.Policy, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PolicyVersionOrderBy, filter *types.PolicyVersionFilter) (*types.PolicyVersionConnection, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.PolicyVersionOrderField]{
Field: coredata.PolicyVersionOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.PolicyVersionOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Policies.ListVersions(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list policy versions: %w", err))
}
return types.NewPolicyVersionConnection(page), nil
}
// Controls is the resolver for the controls field.
func (r *policyResolver) Controls(ctx context.Context, obj *types.Policy, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
@@ -1338,6 +1431,120 @@ func (r *policyResolver) Controls(ctx context.Context, obj *types.Policy, first
return types.NewControlConnection(page), nil
}
// Policy is the resolver for the policy field.
func (r *policyVersionResolver) Policy(ctx context.Context, obj *types.PolicyVersion) (*types.Policy, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
policyVersion, err := svc.Policies.GetVersion(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get policy version: %w", err))
}
policy, err := svc.Policies.Get(ctx, policyVersion.PolicyID)
if err != nil {
panic(fmt.Errorf("cannot get policy: %w", err))
}
return types.NewPolicy(policy), nil
}
// Signatures is the resolver for the signatures field.
func (r *policyVersionResolver) Signatures(ctx context.Context, obj *types.PolicyVersion, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PolicyVersionSignatureOrder) (*types.PolicyVersionSignatureConnection, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.PolicyVersionSignatureOrderField]{
Field: coredata.PolicyVersionSignatureOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.PolicyVersionSignatureOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Policies.ListSignatures(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list policy version signatures: %w", err))
}
return types.NewPolicyVersionSignatureConnection(page), nil
}
// PublishedBy is the resolver for the publishedBy field.
func (r *policyVersionResolver) PublishedBy(ctx context.Context, obj *types.PolicyVersion) (*types.People, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
policyVersion, err := svc.Policies.GetVersion(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get policy version: %w", err))
}
if policyVersion.PublishedBy == nil {
return nil, nil
}
people, err := svc.Peoples.Get(ctx, *policyVersion.PublishedBy)
if err != nil {
panic(fmt.Errorf("cannot get people: %w", err))
}
return types.NewPeople(people), nil
}
// PolicyVersion is the resolver for the policyVersion field.
func (r *policyVersionSignatureResolver) PolicyVersion(ctx context.Context, obj *types.PolicyVersionSignature) (*types.PolicyVersion, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
policyVersionSignature, err := svc.Policies.GetVersionSignature(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get policy version signature: %w", err))
}
policyVersion, err := svc.Policies.GetVersion(ctx, policyVersionSignature.PolicyVersionID)
if err != nil {
panic(fmt.Errorf("cannot get policy version: %w", err))
}
return types.NewPolicyVersion(policyVersion), nil
}
// SignedBy is the resolver for the signedBy field.
func (r *policyVersionSignatureResolver) SignedBy(ctx context.Context, obj *types.PolicyVersionSignature) (*types.People, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
policyVersionSignature, err := svc.Policies.GetVersionSignature(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get policy version signature: %w", err))
}
people, err := svc.Peoples.Get(ctx, policyVersionSignature.SignedBy)
if err != nil {
panic(fmt.Errorf("cannot get people: %w", err))
}
return types.NewPeople(people), nil
}
// RequestedBy is the resolver for the requestedBy field.
func (r *policyVersionSignatureResolver) RequestedBy(ctx context.Context, obj *types.PolicyVersionSignature) (*types.People, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())
policyVersionSignature, err := svc.Policies.GetVersionSignature(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot get policy version signature: %w", err))
}
people, err := svc.Peoples.Get(ctx, policyVersionSignature.RequestedBy)
if err != nil {
panic(fmt.Errorf("cannot get people: %w", err))
}
return types.NewPeople(people), nil
}
// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
svc := GetTenantService(ctx, r.proboSvc, id.TenantID())
@@ -1417,6 +1624,18 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
panic(fmt.Errorf("cannot get vendor compliance report: %w", err))
}
return types.NewVendorComplianceReport(vendorComplianceReport), nil
case coredata.PolicyVersionEntityType:
policyVersion, err := svc.Policies.GetVersion(ctx, id)
if err != nil {
panic(fmt.Errorf("cannot get policy version: %w", err))
}
return types.NewPolicyVersion(policyVersion), nil
case coredata.PolicyVersionSignatureEntityType:
policyVersionSignature, err := svc.Policies.GetVersionSignature(ctx, id)
if err != nil {
panic(fmt.Errorf("cannot get policy version signature: %w", err))
}
return types.NewPolicyVersionSignature(policyVersionSignature), nil
default:
}
@@ -1579,7 +1798,7 @@ func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *in
func (r *userResolver) People(ctx context.Context, obj *types.User, organizationID gid.GID) (*types.People, error) {
svc := GetTenantService(ctx, r.proboSvc, organizationID.TenantID())
people, err := svc.Peoples.GetByUserID(ctx, organizationID, obj.ID)
people, err := svc.Peoples.GetByUserID(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("failed to get people: %w", err))
}
@@ -1778,6 +1997,14 @@ func (r *Resolver) Organization() schema.OrganizationResolver { return &organiza
// Policy returns schema.PolicyResolver implementation.
func (r *Resolver) Policy() schema.PolicyResolver { return &policyResolver{r} }
// PolicyVersion returns schema.PolicyVersionResolver implementation.
func (r *Resolver) PolicyVersion() schema.PolicyVersionResolver { return &policyVersionResolver{r} }
// PolicyVersionSignature returns schema.PolicyVersionSignatureResolver implementation.
func (r *Resolver) PolicyVersionSignature() schema.PolicyVersionSignatureResolver {
return &policyVersionSignatureResolver{r}
}
// Query returns schema.QueryResolver implementation.
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
@@ -1813,6 +2040,8 @@ type mesureResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type policyResolver struct{ *Resolver }
type policyVersionResolver struct{ *Resolver }
type policyVersionSignatureResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type riskResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }