Add policies listing page

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Jonathan
2025-05-26 13:29:48 +02:00
committed by Sacha Al Himdani
parent 7903bd3a77
commit abe0ce1dde
36 changed files with 3343 additions and 138 deletions

View File

@@ -20,11 +20,12 @@ export function ControlledField({
name={name}
render={({ field }) => (
<>
{/* @ts-expect-error field is too dynamic */}
<Field
{...props}
{...field}
// TODO : Find a better way to handle this case (comparing number and string for select create issues)
value={field.value ? field.value.toString() : null}
value={field.value ? field.value.toString() : ""}
onValueChange={field.onChange}
/>
</>
@@ -48,6 +49,7 @@ export function ControlledSelect({
{...props}
{...field}
onValueChange={field.onChange}
value={field.value ?? ""}
/>
)}
/>

View File

@@ -72,8 +72,9 @@ function PeopleSelectWithQuery({
variant="editor"
placeholder={__("Select an owner")}
onValueChange={field.onChange}
key={people?.length ?? 0}
key={people?.length.toString() ?? "0"}
{...field}
value={field.value ?? ""}
>
{people?.map((p) => (
<Option key={p.id} value={p.id} className="flex gap-2">

View File

@@ -0,0 +1,76 @@
/**
* @generated SignedSource<<3ffcc7618d81b47e39ca5731fe75e2ed>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type usePolicyFormFragment$data = {
readonly description: string;
readonly id: string;
readonly owner: {
readonly id: string;
};
readonly title: string;
readonly " $fragmentType": "usePolicyFormFragment";
};
export type usePolicyFormFragment$key = {
readonly " $data"?: usePolicyFormFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"usePolicyFormFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "usePolicyFormFragment",
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v0/*: any*/)
],
"storageKey": null
}
],
"type": "Policy",
"abstractKey": null
};
})();
(node as any).hash = "619d6845e45fb8c052a8989ea546414d";
export default node;

View File

@@ -0,0 +1,12 @@
import { z } from "zod";
import { useFormWithSchema } from "../useFormWithSchema";
export const policySchema = z.object({
title: z.string(),
content: z.string(),
ownerId: z.string(),
});
export const usePolicyForm = () => {
return useFormWithSchema(policySchema, {});
};

View File

@@ -0,0 +1,46 @@
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "../useMutationWithToasts";
import type { PolicyGraphDeleteMutation } from "./__generated__/PolicyGraphDeleteMutation.graphql";
export const policiesQuery = graphql`
query PolicyGraphListQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
id
...PoliciesPageListFragment
}
}
`;
const deletePolicyMutation = graphql`
mutation PolicyGraphDeleteMutation(
$input: DeletePolicyInput!
$connections: [ID!]!
) {
deletePolicy(input: $input) {
deletedPolicyId @deleteEdge(connections: $connections)
}
}
`;
export function useDeletePolicyMutation() {
const { __ } = useTranslate();
return useMutationWithToasts<PolicyGraphDeleteMutation>(
deletePolicyMutation,
{
successMessage: __("Policy deleted successfully."),
errorMessage: __("Failed to delete policy. Please try again."),
}
);
}
export const policyNodeQuery = graphql`
query PolicyGraphNodeQuery($policyId: ID!) {
node(id: $policyId) {
... on Policy {
...PolicyPagePolicyFragment
}
}
}
`;

View File

@@ -0,0 +1,132 @@
/**
* @generated SignedSource<<baab5b247999631abac20d0f5ab13925>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeletePolicyInput = {
policyId: string;
};
export type PolicyGraphDeleteMutation$variables = {
connections: ReadonlyArray<string>;
input: DeletePolicyInput;
};
export type PolicyGraphDeleteMutation$data = {
readonly deletePolicy: {
readonly deletedPolicyId: string;
};
};
export type PolicyGraphDeleteMutation = {
response: PolicyGraphDeleteMutation$data;
variables: PolicyGraphDeleteMutation$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": "PolicyGraphDeleteMutation",
"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": "PolicyGraphDeleteMutation",
"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": "d635ed7113bd8ad32056f5b4fbb95b22",
"id": null,
"metadata": {},
"name": "PolicyGraphDeleteMutation",
"operationKind": "mutation",
"text": "mutation PolicyGraphDeleteMutation(\n $input: DeletePolicyInput!\n) {\n deletePolicy(input: $input) {\n deletedPolicyId\n }\n}\n"
}
};
})();
(node as any).hash = "f5178185f41864d20506abc44b37fa67";
export default node;

View File

@@ -0,0 +1,375 @@
/**
* @generated SignedSource<<26cd4feab2589e8b641b339c977905a3>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PolicyGraphListQuery$variables = {
organizationId: string;
};
export type PolicyGraphListQuery$data = {
readonly organization: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"PoliciesPageListFragment">;
};
};
export type PolicyGraphListQuery = {
response: PolicyGraphListQuery$data;
variables: PolicyGraphListQuery$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": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v4 = {
"kind": "Literal",
"name": "first",
"value": 100
},
v5 = [
(v4/*: any*/),
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
}
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "PolicyGraphListQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "PoliciesPageListFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "PolicyGraphListQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v5/*: any*/),
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "policies",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"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
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "PolicyVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": [
(v4/*: 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
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "signatures(first:100)"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
},
(v3/*: 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
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": "policies(first:100,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "PoliciesPageFragment_policies",
"kind": "LinkedHandle",
"name": "policies"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "838223ae76604de3893662f5a496c1d3",
"id": null,
"metadata": {},
"name": "PolicyGraphListQuery",
"operationKind": "query",
"text": "query PolicyGraphListQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...PoliciesPageListFragment\n }\n}\n\nfragment PoliciesPageListFragment on Organization {\n policies(first: 100, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...PoliciesPageRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n}\n\nfragment PoliciesPageRowFragment on Policy {\n id\n title\n description\n updatedAt\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "29761736de791476004e17ad21659a59";
export default node;

View File

@@ -0,0 +1,277 @@
/**
* @generated SignedSource<<88a40781a78a282ebde97d4cb73bb996>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PolicyGraphNodeQuery$variables = {
policyId: string;
};
export type PolicyGraphNodeQuery$data = {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"PolicyPagePolicyFragment">;
};
};
export type PolicyGraphNodeQuery = {
response: PolicyGraphNodeQuery$data;
variables: PolicyGraphNodeQuery$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
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "PolicyGraphNodeQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PolicyPagePolicyFragment"
}
],
"type": "Policy",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "PolicyGraphNodeQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "PolicyVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "publishedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PolicyVersionSignatureConnection",
"kind": "LinkedField",
"name": "signatures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "signatures(first:100)"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"type": "Policy",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "061b3441def7dfcf73881e57077e1738",
"id": null,
"metadata": {},
"name": "PolicyGraphNodeQuery",
"operationKind": "query",
"text": "query PolicyGraphNodeQuery(\n $policyId: ID!\n) {\n node(id: $policyId) {\n __typename\n ... on Policy {\n ...PolicyPagePolicyFragment\n }\n id\n }\n}\n\nfragment PolicyPagePolicyFragment on Policy {\n id\n title\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n content\n status\n publishedAt\n version\n updatedAt\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "c06b24e46b33741678e07787ab1c69f3";
export default node;

View File

@@ -4,9 +4,3 @@
@import "tw-animate-css";
@source "../../../packages/ui/src";
@source "../../../packages/helpers/src";
*:focus-visible {
box-shadow: var(--shadow-focus);
outline: none;
border-radius: 0.25rem;
}

View File

@@ -8,12 +8,8 @@
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from "relay-runtime";
export type MeasureState =
| "IMPLEMENTED"
| "IN_PROGRESS"
| "NOT_APPLICABLE"
| "NOT_STARTED";
import { ReaderFragment } from 'relay-runtime';
export type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED" | "%future added value";
import { FragmentRefs } from "relay-runtime";
export type MeasuresPageFragment$data = {
readonly measures: {
@@ -35,135 +31,137 @@ export type MeasuresPageFragment$key = {
};
const node: ReaderFragment = {
argumentDefinitions: [],
kind: "Fragment",
metadata: {
connection: [
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
count: null,
cursor: null,
direction: "forward",
path: ["measures"],
},
],
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"measures"
]
}
]
},
name: "MeasuresPageFragment",
selections: [
"name": "MeasuresPageFragment",
"selections": [
{
alias: "measures",
args: null,
concreteType: "MeasureConnection",
kind: "LinkedField",
name: "__MeasuresGraphListQuery__measures_connection",
plural: false,
selections: [
"alias": "measures",
"args": null,
"concreteType": "MeasureConnection",
"kind": "LinkedField",
"name": "__MeasuresGraphListQuery__measures_connection",
"plural": false,
"selections": [
{
alias: null,
args: null,
concreteType: "MeasureEdge",
kind: "LinkedField",
name: "edges",
plural: true,
selections: [
"alias": null,
"args": null,
"concreteType": "MeasureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
alias: null,
args: null,
concreteType: "Measure",
kind: "LinkedField",
name: "node",
plural: false,
selections: [
"alias": null,
"args": null,
"concreteType": "Measure",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
alias: null,
args: null,
kind: "ScalarField",
name: "id",
storageKey: null,
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
alias: null,
args: null,
kind: "ScalarField",
name: "name",
storageKey: null,
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
alias: null,
args: null,
kind: "ScalarField",
name: "category",
storageKey: null,
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
alias: null,
args: null,
kind: "ScalarField",
name: "state",
storageKey: null,
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
},
{
alias: null,
args: null,
kind: "ScalarField",
name: "__typename",
storageKey: null,
},
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
storageKey: null,
"storageKey": null
},
{
alias: null,
args: null,
kind: "ScalarField",
name: "cursor",
storageKey: null,
},
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
storageKey: null,
"storageKey": null
},
{
alias: null,
args: null,
concreteType: "PageInfo",
kind: "LinkedField",
name: "pageInfo",
plural: false,
selections: [
"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": "endCursor",
"storageKey": null
},
{
alias: null,
args: null,
kind: "ScalarField",
name: "hasNextPage",
storageKey: null,
},
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
}
],
storageKey: null,
"storageKey": null
},
{
kind: "ClientExtension",
selections: [
"kind": "ClientExtension",
"selections": [
{
alias: null,
args: null,
kind: "ScalarField",
name: "__id",
storageKey: null,
},
],
},
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
storageKey: null,
},
"storageKey": null
}
],
type: "Organization",
abstractKey: null,
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "6a45bee844bf79dc4540e20edf10ad78";

View File

@@ -0,0 +1,139 @@
import { useTranslate } from "@probo/i18n";
import {
Badge,
Breadcrumb,
Button,
Dialog,
DialogContent,
DialogFooter,
Input,
Label,
PropertyRow,
Textarea,
} from "@probo/ui";
import { useState, type ReactNode } from "react";
import { graphql } from "relay-runtime";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { usePolicyForm } from "../../../hooks/forms/usePolicyForm";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { FormPolicyDialogMutation } from "./__generated__/FormPolicyDialogMutation.graphql";
import { PeopleSelect } from "/components/form/PeopleSelect";
type Props = {
trigger?: ReactNode;
open?: boolean;
onSuccess?: () => void;
connection: string;
};
const createPolicyMutation = graphql`
mutation FormPolicyDialogMutation(
$input: CreatePolicyInput!
$connections: [ID!]!
) {
createPolicy(input: $input) {
policyEdge @prependEdge(connections: $connections) {
node {
...PoliciesPageRowFragment
}
}
}
}
`;
/**
* Dialog to create or update a policy
*/
export default function FormPolicyDialog({
trigger,
onSuccess,
open,
connection,
}: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { control, handleSubmit, register, formState, reset } = usePolicyForm();
const errors = formState.errors ?? {};
const [createPolicy, isLoading] =
useMutationWithToasts<FormPolicyDialogMutation>(createPolicyMutation);
const onSubmit = handleSubmit((data) => {
createPolicy({
variables: {
input: {
...data,
organizationId,
},
connections: [connection!],
},
successMessage: __("Policy created successfully."),
errorMessage: __("Failed to create policy. Please try again."),
onSuccess: () => {
setOpen(false);
reset();
onSuccess?.();
},
});
});
const [isOpen, setOpen] = useState(!!open);
return (
<Dialog
open={isOpen}
onOpenChange={setOpen}
trigger={trigger}
title={<Breadcrumb items={[__("Policies"), __("New Policy")]} />}
>
<form onSubmit={onSubmit}>
<DialogContent className="grid grid-cols-[1fr_420px]">
<div className="py-8 px-10 space-y-4">
<Input
id="title"
aria-label={__("Title")}
required
variant="title"
placeholder={__("Policy title")}
{...register("title")}
/>
<Textarea
id="content"
variant="ghost"
autogrow
placeholder={__("Add description")}
aria-label={__("Description")}
{...register("content")}
/>
</div>
{/* Properties form */}
<div className="py-5 px-6 bg-subtle">
<Label>{__("Properties")}</Label>
<PropertyRow label={__("Status")}>
<Badge variant="neutral" size="md">
{__("Draft")}
</Badge>
</PropertyRow>
<PropertyRow
id="ownerId"
label={__("Owner")}
error={errors.ownerId?.message}
>
<PeopleSelect
name="ownerId"
control={control}
organization={organizationId}
/>
</PropertyRow>
</div>
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={isLoading}>
{__("Create policy")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,223 @@
import { useTranslate } from "@probo/i18n";
import {
PageHeader,
Table,
Tbody,
Thead,
Tr,
Th,
Td,
Avatar,
Badge,
ActionDropdown,
DropdownItem,
IconPencil,
ConfirmDialog,
IconTrashCan,
Button,
IconPlusLarge,
} from "@probo/ui";
import {
useFragment,
usePreloadedQuery,
type PreloadedQuery,
} from "react-relay";
import { graphql } from "relay-runtime";
import type { PolicyGraphListQuery } from "/hooks/graph/__generated__/PolicyGraphListQuery.graphql";
import {
policiesQuery,
useDeletePolicyMutation,
} from "/hooks/graph/PolicyGraph";
import type { PoliciesPageListFragment$key } from "./__generated__/PoliciesPageListFragment.graphql";
import { usePageTitle } from "@probo/hooks";
import { sprintf } from "@probo/helpers";
import FormPolicyDialog from "./FormPolicyDialog";
import type { PoliciesPageRowFragment$key } from "./__generated__/PoliciesPageRowFragment.graphql";
const policiesFragment = graphql`
fragment PoliciesPageListFragment on Organization {
policies(
first: 100
after: null
last: null
before: null
orderBy: { direction: DESC, field: CREATED_AT }
) @connection(key: "PoliciesPageFragment_policies") {
__id
edges {
node {
id
...PoliciesPageRowFragment
}
}
}
}
`;
type Props = {
queryRef: PreloadedQuery<PolicyGraphListQuery>;
};
export default function PoliciesPage(props: Props) {
const { __ } = useTranslate();
const organization = usePreloadedQuery(
policiesQuery,
props.queryRef
).organization;
const data = useFragment<PoliciesPageListFragment$key>(
policiesFragment,
organization
);
const policies = data.policies.edges.map((edge) => edge.node);
const connectionId = data.policies.__id;
usePageTitle(__("Policies"));
return (
<div className="space-y-6">
<PageHeader
title={__("Policies")}
description={__("Manage your organization’s policies")}
>
<FormPolicyDialog
connection={connectionId}
trigger={<Button icon={IconPlusLarge}>{__("New policy")}</Button>}
/>
</PageHeader>
<Table>
<Thead>
<Tr>
<Th>{__("Name")}</Th>
<Th>{__("Status")}</Th>
<Th>{__("Owner")}</Th>
<Th>{__("Last update")}</Th>
<Th>{__("Signatures")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{policies.map((policy) => (
<PolicyRow
key={policy.id}
policy={policy}
organizationId={organization.id}
connectionId={connectionId}
/>
))}
</Tbody>
</Table>
</div>
);
}
const rowFragment = graphql`
fragment PoliciesPageRowFragment on Policy {
id
title
description
updatedAt
owner {
id
fullName
}
versions(first: 1) {
edges {
node {
id
status
signatures(first: 100) {
edges {
node {
id
state
}
}
}
}
}
}
}
`;
function PolicyRow({
policy: policyKey,
organizationId,
connectionId,
}: {
policy: PoliciesPageRowFragment$key;
organizationId: string;
connectionId: string;
}) {
const policy = useFragment<PoliciesPageRowFragment$key>(
rowFragment,
policyKey
);
const lastVersion = policy.versions.edges[0].node;
const isDraft = lastVersion.status === "DRAFT";
const { __, dateFormat } = useTranslate();
const signatures = lastVersion.signatures.edges.map((edge) => edge.node);
const signedCount = signatures.filter(
(signature) => signature.state === "SIGNED"
).length;
const [deletePolicy, isDeleting] = useDeletePolicyMutation();
const handleDelete = () => {
deletePolicy({
variables: {
input: { policyId: policy.id },
connections: [connectionId],
},
});
};
return (
<Tr to={`/organizations/${organizationId}/policies/${policy.id}`}>
<Td>{policy.title}</Td>
<Td>
<Badge variant={isDraft ? "neutral" : "success"}>
{isDraft ? __("Draft") : __("Published")}
</Badge>
</Td>
<Td>
<div className="flex gap-2 items-center">
<Avatar name={policy.owner.fullName} />
{policy.owner.fullName}
</div>
</Td>
<Td>
{dateFormat(policy.updatedAt, {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
})}
</Td>
<Td>
{signedCount}/{signatures.length}
</Td>
<Td noLink width={50} className="text-end">
<ActionDropdown>
<DropdownItem icon={IconPencil}>{__("Edit")}</DropdownItem>
<ConfirmDialog
message={sprintf(
__(
'This will permanently delete the policy "%s". This action cannot be undone.'
),
policy.title
)}
onConfirm={handleDelete}
>
<DropdownItem
disabled={isDeleting}
variant="danger"
icon={IconTrashCan}
>
{__("Delete")}
</DropdownItem>
</ConfirmDialog>
</ActionDropdown>
</Td>
</Tr>
);
}

View File

@@ -0,0 +1,177 @@
import type { PreloadedQuery } from "react-relay";
import type { PolicyGraphNodeQuery } from "/hooks/graph/__generated__/PolicyGraphNodeQuery.graphql";
import {
graphql,
loadQuery,
useFragment,
usePreloadedQuery,
} from "react-relay";
import { policyNodeQuery } from "/hooks/graph/PolicyGraph";
import { usePageTitle } from "@probo/hooks";
import type { PolicyPagePolicyFragment$key } from "./__generated__/PolicyPagePolicyFragment.graphql";
import { useTranslate } from "@probo/i18n";
import {
PageHeader,
Breadcrumb,
IconCheckmark1,
Markdown,
PropertyRow,
Drawer,
Badge,
Avatar,
} from "@probo/ui";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { Button } from "@probo/ui";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
type Props = {
queryRef: PreloadedQuery<PolicyGraphNodeQuery>;
};
const policyFragment = graphql`
fragment PolicyPagePolicyFragment on Policy {
id
title
owner {
id
fullName
}
versions(first: 1) {
edges {
node {
id
content
status
publishedAt
version
updatedAt
signatures(first: 100) {
edges {
node {
id
state
}
}
}
}
}
}
}
`;
const publishPolicyVersionMutation = graphql`
mutation PolicyPagePublishMutation($input: PublishPolicyVersionInput!) {
publishPolicyVersion(input: $input) {
policy {
id
}
}
}
`;
export default function PolicyPage(props: Props) {
const node = usePreloadedQuery(policyNodeQuery, props.queryRef).node;
const policy = useFragment(
policyFragment,
node as PolicyPagePolicyFragment$key
);
const { __, dateFormat } = useTranslate();
const organizationId = useOrganizationId();
const lastVersion = policy.versions.edges[0].node;
const isDraft = lastVersion.status === "DRAFT";
const [publishPolicyVersion, isPublishing] = useMutationWithToasts(
publishPolicyVersionMutation,
{
successMessage: __("Policy published successfully."),
errorMessage: __("Failed to publish policy. Please try again."),
}
);
usePageTitle(policy.title);
const handlePublish = () => {
publishPolicyVersion({
variables: {
input: { policyId: policy.id },
},
onSuccess: () => {
// Refresh the whole query to get the new version
loadQuery(
props.queryRef.environment,
policyNodeQuery,
props.queryRef.variables,
{ fetchPolicy: "network-only" }
);
},
});
};
return (
<>
<div className="space-y-6">
<div className="flex justify-between items-center mb-4">
<Breadcrumb
items={[
{
label: __("Policies"),
to: `/organizations/${organizationId}/policies`,
},
{
label: policy.title,
},
]}
/>
<div className="flex gap-2">
{isDraft && (
<Button
onClick={handlePublish}
icon={IconCheckmark1}
disabled={isPublishing}
>
{__("Publish")}
</Button>
)}
</div>
</div>
<PageHeader title={policy.title} />
<Markdown content={lastVersion.content} />
</div>
<Drawer>
<div className="text-base text-txt-primary font-medium mb-4">
{__("Properties")}
</div>
<PropertyRow label={__("Owner")}>
<Badge variant="highlight" size="md" className="gap-2">
<Avatar name={policy.owner?.fullName ?? ""} />
{policy.owner?.fullName}
</Badge>
</PropertyRow>
<PropertyRow label={__("Status")}>
<Badge
variant={lastVersion.status === "DRAFT" ? "highlight" : "success"}
size="md"
className="gap-2"
>
{lastVersion.status === "DRAFT" ? __("Draft") : __("Published")}
</Badge>
</PropertyRow>
<PropertyRow label={__("Version")}>
<div className="text-sm text-txt-secondary">
{lastVersion.version}
</div>
</PropertyRow>
<PropertyRow label={__("Last modified")}>
<div className="text-sm text-txt-secondary">
{dateFormat(lastVersion.updatedAt)}
</div>
</PropertyRow>
{lastVersion.publishedAt && (
<PropertyRow label={__("Published Date")}>
<div className="text-sm text-txt-secondary">
{dateFormat(lastVersion.publishedAt)}
</div>
</PropertyRow>
)}
</Drawer>
</>
);
}

View File

@@ -0,0 +1,322 @@
/**
* @generated SignedSource<<418554f5765131f61a3dfed83db0da76>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type CreatePolicyInput = {
content: string;
organizationId: string;
ownerId: string;
title: string;
};
export type FormPolicyDialogMutation$variables = {
connections: ReadonlyArray<string>;
input: CreatePolicyInput;
};
export type FormPolicyDialogMutation$data = {
readonly createPolicy: {
readonly policyEdge: {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"PoliciesPageRowFragment">;
};
};
};
};
export type FormPolicyDialogMutation = {
response: FormPolicyDialogMutation$data;
variables: FormPolicyDialogMutation$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
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "FormPolicyDialogMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreatePolicyPayload",
"kind": "LinkedField",
"name": "createPolicy",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyEdge",
"kind": "LinkedField",
"name": "policyEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PoliciesPageRowFragment"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "FormPolicyDialogMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreatePolicyPayload",
"kind": "LinkedField",
"name": "createPolicy",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyEdge",
"kind": "LinkedField",
"name": "policyEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"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": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "PolicyVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PolicyVersionSignatureConnection",
"kind": "LinkedField",
"name": "signatures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "signatures(first:100)"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"filters": null,
"handle": "prependEdge",
"key": "",
"kind": "LinkedHandle",
"name": "policyEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "5967e987ab20d7823e7456a22aec54f2",
"id": null,
"metadata": {},
"name": "FormPolicyDialogMutation",
"operationKind": "mutation",
"text": "mutation FormPolicyDialogMutation(\n $input: CreatePolicyInput!\n) {\n createPolicy(input: $input) {\n policyEdge {\n node {\n ...PoliciesPageRowFragment\n id\n }\n }\n }\n}\n\nfragment PoliciesPageRowFragment on Policy {\n id\n title\n description\n updatedAt\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "c2612f85b9170d81cb9fb4908f752069";
export default node;

View File

@@ -0,0 +1,173 @@
/**
* @generated SignedSource<<cd6428905d88d31ab94cb3cbcd9fcd2c>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PoliciesPageListFragment$data = {
readonly policies: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"PoliciesPageRowFragment">;
};
}>;
};
readonly " $fragmentType": "PoliciesPageListFragment";
};
export type PoliciesPageListFragment$key = {
readonly " $data"?: PoliciesPageListFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"PoliciesPageListFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "bidirectional",
"path": [
"policies"
]
}
]
},
"name": "PoliciesPageListFragment",
"selections": [
{
"alias": "policies",
"args": [
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "DESC",
"field": "CREATED_AT"
}
}
],
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "__PoliciesPageFragment_policies_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "PoliciesPageRowFragment"
},
{
"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
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasPreviousPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "startCursor",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "ClientExtension",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__id",
"storageKey": null
}
]
}
],
"storageKey": "__PoliciesPageFragment_policies_connection(orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "2f34601ba53a0900494a1bb0d8be1773";
export default node;

View File

@@ -0,0 +1,204 @@
/**
* @generated SignedSource<<76980bd62a1a415efbb27298d27874da>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type PolicyStatus = "DRAFT" | "PUBLISHED" | "%future added value";
export type PolicyVersionSignatureState = "REQUESTED" | "SIGNED" | "%future added value";
import { FragmentRefs } from "relay-runtime";
export type PoliciesPageRowFragment$data = {
readonly description: string;
readonly id: string;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly title: string;
readonly updatedAt: any;
readonly versions: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly signatures: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly state: PolicyVersionSignatureState;
};
}>;
};
readonly status: PolicyStatus;
};
}>;
};
readonly " $fragmentType": "PoliciesPageRowFragment";
};
export type PoliciesPageRowFragment$key = {
readonly " $data"?: PoliciesPageRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"PoliciesPageRowFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "PoliciesPageRowFragment",
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"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": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "PolicyVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PolicyVersionSignatureConnection",
"kind": "LinkedField",
"name": "signatures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "signatures(first:100)"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"type": "Policy",
"abstractKey": null
};
})();
(node as any).hash = "b50c23e038e757685dfc7535825a389c";
export default node;

View File

@@ -0,0 +1,220 @@
/**
* @generated SignedSource<<c59210c06978720ee4b52c41f0337ecf>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type PolicyStatus = "DRAFT" | "PUBLISHED" | "%future added value";
export type PolicyVersionSignatureState = "REQUESTED" | "SIGNED" | "%future added value";
import { FragmentRefs } from "relay-runtime";
export type PolicyPagePolicyFragment$data = {
readonly id: string;
readonly owner: {
readonly fullName: string;
readonly id: string;
};
readonly title: string;
readonly versions: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly content: string;
readonly id: string;
readonly publishedAt: any | null | undefined;
readonly signatures: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly state: PolicyVersionSignatureState;
};
}>;
};
readonly status: PolicyStatus;
readonly updatedAt: any;
readonly version: number;
};
}>;
};
readonly " $fragmentType": "PolicyPagePolicyFragment";
};
export type PolicyPagePolicyFragment$key = {
readonly " $data"?: PolicyPagePolicyFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"PolicyPagePolicyFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "PolicyPagePolicyFragment",
"selections": [
(v0/*: any*/),
{
"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*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "PolicyVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "content",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "publishedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "version",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PolicyVersionSignatureConnection",
"kind": "LinkedField",
"name": "signatures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "signatures(first:100)"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"type": "Policy",
"abstractKey": null
};
})();
(node as any).hash = "6b70d6d05d6e1081afae758389d070b2";
export default node;

View File

@@ -0,0 +1,249 @@
/**
* @generated SignedSource<<f3471e799b803d8bd39ad2a76603716e>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PolicyPagePolicyQuery$variables = {
id: string;
};
export type PolicyPagePolicyQuery$data = {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"PolicyPagePolicyFragment">;
};
};
export type PolicyPagePolicyQuery = {
response: PolicyPagePolicyQuery$data;
variables: PolicyPagePolicyQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "id"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "id"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "PolicyPagePolicyQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PolicyPagePolicyFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "PolicyPagePolicyQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "PolicyVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PolicyVersionSignatureConnection",
"kind": "LinkedField",
"name": "signatures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "signatures(first:100)"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"type": "Policy",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "a7dd624a52cc7f0193b7670476c9de0d",
"id": null,
"metadata": {},
"name": "PolicyPagePolicyQuery",
"operationKind": "query",
"text": "query PolicyPagePolicyQuery(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PolicyPagePolicyFragment\n id\n }\n}\n\nfragment PolicyPagePolicyFragment on Policy {\n id\n title\n description\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "a1317341ce16c9f3cfd8f6a6a620c077";
export default node;

View File

@@ -0,0 +1,249 @@
/**
* @generated SignedSource<<9647dfc253447fe799b8178349c656d0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type PolicyPagePolicyRefetch$variables = {
id: string;
};
export type PolicyPagePolicyRefetch$data = {
readonly node: {
readonly " $fragmentSpreads": FragmentRefs<"PolicyPagePolicyFragment">;
};
};
export type PolicyPagePolicyRefetch = {
response: PolicyPagePolicyRefetch$data;
variables: PolicyPagePolicyRefetch$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "id"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "id"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "PolicyPagePolicyRefetch",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PolicyPagePolicyFragment"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "PolicyPagePolicyRefetch",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "owner",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "PolicyVersionConnection",
"kind": "LinkedField",
"name": "versions",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersion",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 100
}
],
"concreteType": "PolicyVersionSignatureConnection",
"kind": "LinkedField",
"name": "signatures",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignatureEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "PolicyVersionSignature",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "signatures(first:100)"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "versions(first:1)"
}
],
"type": "Policy",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "6eec3f5bb581dbce013634d36d9b43d9",
"id": null,
"metadata": {},
"name": "PolicyPagePolicyRefetch",
"operationKind": "query",
"text": "query PolicyPagePolicyRefetch(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PolicyPagePolicyFragment\n id\n }\n}\n\nfragment PolicyPagePolicyFragment on Policy {\n id\n title\n description\n owner {\n id\n fullName\n }\n versions(first: 1) {\n edges {\n node {\n id\n status\n signatures(first: 100) {\n edges {\n node {\n id\n state\n }\n }\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "e553858c09ded278d1d30aaad4f7ede9";
export default node;

View File

@@ -0,0 +1,105 @@
/**
* @generated SignedSource<<c68450dee24a94b20d5c4985a83511e0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PublishPolicyVersionInput = {
policyId: string;
};
export type PolicyPagePublishMutation$variables = {
input: PublishPolicyVersionInput;
};
export type PolicyPagePublishMutation$data = {
readonly publishPolicyVersion: {
readonly policy: {
readonly id: string;
};
};
};
export type PolicyPagePublishMutation = {
response: PolicyPagePublishMutation$data;
variables: PolicyPagePublishMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "PublishPolicyVersionPayload",
"kind": "LinkedField",
"name": "publishPolicyVersion",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"kind": "LinkedField",
"name": "policy",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "PolicyPagePublishMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "PolicyPagePublishMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "4279dd7a9a12d8d5fb6e07c1875de00a",
"id": null,
"metadata": {},
"name": "PolicyPagePublishMutation",
"operationKind": "mutation",
"text": "mutation PolicyPagePublishMutation(\n $input: PublishPolicyVersionInput!\n) {\n publishPolicyVersion(input: $input) {\n policy {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "8ee6cdf68342db34ec04aab82313eff0";
export default node;

View File

@@ -13,6 +13,7 @@ import { type PreloadedQuery } from "react-relay";
import { useCleanup } from "./hooks/useDelayedEffect.ts";
import { riskRoutes } from "./routes/riskRoutes.ts";
import { measureRoutes } from "./routes/measureRoutes.ts";
import { policiesRoutes } from "./routes/policiesRoutes.ts";
function ErrorBoundary() {
const error = useRouteError();
@@ -72,6 +73,7 @@ const routes = [
},
...riskRoutes,
...measureRoutes,
...policiesRoutes,
],
},
] satisfies AppRoute[];

View File

@@ -0,0 +1,24 @@
import { lazy } from "react";
import { loadQuery } from "react-relay";
import type { AppRoute } from "/routes.tsx";
import { relayEnvironment } from "/providers/RelayProviders";
import { policiesQuery } from "/hooks/graph/PolicyGraph";
import { policyNodeQuery } from "/hooks/graph/PolicyGraph";
import { PageSkeleton } from "/components/skeletons/PageSkeleton";
export const policiesRoutes = [
{
path: "policies",
fallback: PageSkeleton,
queryLoader: ({ organizationId }) =>
loadQuery(relayEnvironment, policiesQuery, { organizationId }),
Component: lazy(() => import("/pages/organizations/policies/PoliciesPage")),
},
{
path: "policies/:policyId",
fallback: PageSkeleton,
queryLoader: ({ policyId }) =>
loadQuery(relayEnvironment, policyNodeQuery, { policyId }),
Component: lazy(() => import("/pages/organizations/policies/PolicyPage")),
},
] satisfies AppRoute[];

View File

@@ -56,7 +56,12 @@ export function useTranslate() {
dateFormat: (
date: Date | string | null | undefined,
options?: Intl.DateTimeFormatOptions
options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
}
) => {
if (!date) {
return "";

View File

@@ -21,14 +21,15 @@
"@radix-ui/react-tabs": "^1.1.12",
"@tailwindcss/vite": "^4.1.7",
"clsx": "^2.1.1",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.7",
"tw-animate-css": "^1.3.0",
"zustand": "^5.0.4"
},
"devDependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"@chromatic-com/storybook": "^3.2.6",
"@eslint/js": "^9.25.0",
"@probo/prettier": "1.0.0",
@@ -38,6 +39,7 @@
"@storybook/react": "^8.6.13",
"@storybook/react-vite": "^8.6.13",
"@storybook/test": "^8.6.13",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
@@ -50,6 +52,8 @@
"globals": "^16.0.0",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.0",
"storybook": "^8.6.13",
"typescript": "~5.8.3",

View File

@@ -19,7 +19,7 @@ export const button = tv({
"bg-tertiary text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed focus:shadow-focus",
quaternary:
"bg-highlight text-txt-primary hover:bg-highlight-hover active:bg-highlight-pressed",
danger: "bg-danger-plain text-invert hover:bg-danger-hover shadow-base hover:shadow-hover active:bg-danger-pressed focus:shadow-focus border border-border-danger",
danger: "bg-danger-plain text-txt-invert hover:bg-danger-hover shadow-base hover:shadow-hover active:bg-danger-pressed focus:shadow-focus border border-border-danger",
},
disabled: {
true: "opacity-60 cursor-default",

View File

@@ -16,3 +16,10 @@ export const Disabled: Story = {
disabled: true,
},
};
export const Title: Story = {
args: {
variant: "title",
placeholder: "Policy title",
},
};

View File

@@ -7,14 +7,24 @@ type Props = {
disabled?: boolean;
icon?: FC<IconProps>;
onValueChange?: (value: string) => void;
variant?: "bordered" | "ghost" | "title";
} & InputHTMLAttributes<HTMLInputElement>;
export const input = tv({
base: "py-[6px] bg-secondary border border-border-mid rounded-[10px] hover:border-border-strong focus:shadow-focus text-sm px-3 w-full bg-secondary disabled:bg-transparent focus:outline-none data-[focus=true]:shadow-focus",
base: "w-full disabled:bg-transparent",
variants: {
invalid: {
true: "border-border-danger",
},
variant: {
bordered:
"py-[6px] bg-secondary border border-border-mid rounded-[10px] hover:border-border-strong text-sm px-3 text-txt-primary",
ghost: "text-base text-txt-secondary",
title: "text-2xl font-semibold text-txt-primary",
},
},
defaultVariants: {
variant: "bordered",
},
});
@@ -35,6 +45,7 @@ export function Input({
>
<IconComponent size={16} className="text-txt-secondary" />
<input
{...props}
onFocus={(e) => {
setFocus(true);
props.onFocus?.(e);
@@ -45,7 +56,6 @@ export function Input({
}}
aria-invalid={invalid}
className="w-full outline-none"
{...props}
onChange={(e) => {
onValueChange?.(e.currentTarget.value);
props.onChange?.(e);
@@ -54,5 +64,15 @@ export function Input({
</div>
);
}
return <input aria-invalid={invalid} className={input(props)} {...props} />;
return (
<input
{...props}
aria-invalid={invalid}
className={input(props)}
onChange={(e) => {
onValueChange?.(e.currentTarget.value);
props.onChange?.(e);
}}
/>
);
}

View File

@@ -0,0 +1,75 @@
import { Markdown } from "./Markdown";
import type { Meta, StoryObj } from "@storybook/react";
export default {
title: "Atoms/Markdown",
component: Markdown,
argTypes: {},
} satisfies Meta<typeof Markdown>;
type Story = StoryObj<typeof Markdown>;
export const Default: Story = {
args: {
children: `# Siquis facies post
## Venit Ianigenam egressus a tamen terra frater
Lorem markdownum hoste omnis auxilium et mille caeruleum coeperunt rebus Iunonis
te tibi? Neque in ille mane? Mugitus terra Phineu pendebat lanugine fundae regi
vestigia.
if (finder_pad > path_browser_beta * 5) {
powerDtd += 5;
} else {
cluster += markup_golden;
radcab(progressive(addressIoIde), basic(cacheSoftware, sample,
hdmiKilobyteRootkit));
bitmapBoot(utility_dot, 1);
}
var pcmcia_e = newlineKerningOutput(lifoMbr(graphicsScareware), 5,
t.codec_isdn_digitize.dot(php_lun, 5));
componentRootkit.ipx_heap_hdtv = algorithm;
if (-5) {
southbridge_zone.rateOasis(61 + nvram_windows_drive);
}
Formam meo. *Coniuge nos* in circuit mihi: ad: ominibus data. Cum necopina deus
dona ferendo est. Tua lato fessa monitis terris inconcessisque redeunt delphines
dumque tremebunda moenia et limus.
1. Evolvere nescis
2. Tum magna
3. Proque vapor serum valens hasta visa in
4. Plus dolentius teneas et foedataque poenam alimentaque
## Pervenit sepulcris sponte inferias tolle
Suarum Phrygum noscere et umeris causam. Cum illa stratoque taedis actum
subiectis undae parvis gratissima membra. Faciebant numerare Clarium nomen fit
et aliena decet ergo silvas somnum, conpositas Granico, rettulit deos, erat.
Rupit quoque sparsumque eodem culpae exequialia potentem refert?
> Tamen enim armataque cuncta fuit legit numinis. Labentibus equam torvos et
> pudet liquidis exululat et iussis ferret. Mea contenta in Midan, nam, o
> Lacinia potuit digredimur corpora. Aut scabrae, monte in fons pomi **se
> hiatu** anhelitus! Taurum fugiunt inpius summa aut crimina Parcite e corpus
> altius humus, iacto non *corpus capillos aequore*!
Lacubus distant, nec dixit **parili ore** te exspectatus radii certe. Ostendere
disparibus campus; spondere toris; quod tolerare umbras, nunc arma conprecor
nec. *Penthea* si **viri** paulatim cornu; **si** musco monitis artes? Parte vox
parit utinam solum futurum vecta, taloque attonito meae virga lanaeque ait
tantus locoque illa aurum sortitus atra. Dentibus *per telum horrentia*
abstulerat arces frementes scripta viridi: celare principium mihi.
- Nonne facinus quamvis sanguinis Arcadiae posuit
- Tabellis cepi Hectora
- Rigido percurrens damus
Fecissem quem degravat quibus movere inmotus sacer Iovem, sic *urbis mala
quoque*. Tereu ut tuos sed recessit petit patriaeque subit palude subposuere
certum: tempora, telisque. In quaesitique habitavit nostris Scylaceaque potest
omnia pastoribus meminisse ignara. Sed pando functaque perenni gemitus tibi.`,
},
};

View File

@@ -0,0 +1,20 @@
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
type Props = {
content: string;
};
export function Markdown({ content }: Props) {
return (
<div className="prose prose-neutral">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{content}
</ReactMarkdown>
</div>
);
}

View File

@@ -33,7 +33,6 @@ const select = tv({
"flex justify-between items-center data-placeholder:text-txt-tertiary whitespace-nowrap cursor-pointer *:first:contents",
content:
"z-100 shadow-mid rounded-[10px] bg-level-1 p-1 animate-in fade-in slide-in-from-top-2 overflow-y-auto overflow-y-auto",
option: "flex gap-2 items-center h-8 text-sm font-medium text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed cursor-pointer px-[10px]",
icon: "-mr-1",
},
variants: {
@@ -71,8 +70,6 @@ const select = tv({
},
});
const { trigger, option, content, icon } = select();
export function Select({
placeholder,
children,
@@ -80,9 +77,13 @@ export function Select({
value,
...props
}: Props) {
const { trigger, content, icon } = select({
...props,
});
return (
<Root onValueChange={onValueChange} value={value}>
<Trigger {...props} className={trigger({ ...props })}>
<Trigger {...props} className={trigger()}>
<Value placeholder={placeholder} />
<Icon className={icon()}>
<IconChevronGrabberVertical size={16} />
@@ -115,7 +116,12 @@ export function Select({
export function Option({ children, ...props }: ComponentProps<typeof Item>) {
const hasSingleChildren = Children.count(children) <= 1;
return (
<Item {...props} className={option(props)}>
<Item
{...props}
className={
"flex gap-2 items-center h-8 text-sm font-medium text-txt-primary hover:bg-tertiary-hover active:bg-tertiary-pressed cursor-pointer px-[10px]"
}
>
<ItemText asChild>
<span
className={

View File

@@ -10,3 +10,8 @@ export default {
type Story = StoryObj<typeof Textarea>;
export const Default: Story = {};
export const Autogrow: Story = {
args: {
autogrow: true,
},
};

View File

@@ -1,13 +1,39 @@
import type { TextareaHTMLAttributes } from "react";
import { tv } from "tailwind-variants";
import {
useRef,
type FormEventHandler,
type TextareaHTMLAttributes,
type RefObject,
} from "react";
import { input } from "../Input/Input";
import clsx from "clsx";
type Props = TextareaHTMLAttributes<HTMLTextAreaElement>;
export const textarea = tv({
base: input({ class: "min-h-20" }),
});
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
variant?: "bordered" | "ghost" | "title";
autogrow?: boolean;
ref?: RefObject<HTMLTextAreaElement>;
};
export function Textarea(props: Props) {
return <textarea {...props} className={textarea(props)} />;
const textbox = useRef<HTMLTextAreaElement>(null);
const { autogrow, variant, ref: propsRef, ...restProps } = props;
const ref = propsRef || textbox;
const adjustHeight: FormEventHandler<HTMLTextAreaElement> = (e) => {
props.onInput?.(e);
if (!autogrow || !ref.current) return;
ref.current.style.height = "inherit";
const paddingY = 2;
ref.current.style.height = `${ref.current.scrollHeight + paddingY * 2}px`;
};
return (
<textarea
{...restProps}
ref={ref}
onInput={adjustHeight}
className={input({
...props,
className: clsx("min-h-20", props.className),
})}
/>
);
}

View File

@@ -67,7 +67,10 @@ export function Layout({ header, sidebar, children }: Props) {
);
}
export function Drawer({ children }: PropsWithChildren) {
export function Drawer({
children,
className,
}: PropsWithChildren<{ className?: string }>) {
const { setDrawer } = useContext(LayoutContext);
useEffect(() => {
setDrawer(true);
@@ -76,7 +79,12 @@ export function Drawer({ children }: PropsWithChildren) {
};
}, []);
return createPortal(
<aside className="absolute pt-20 top-0 right-0 w-105 px-6 pb-8 border-border-solid border-l h-screen">
<aside
className={clsx(
"absolute pt-20 top-0 right-0 w-105 px-6 pb-8 border-border-solid border-l h-screen",
className,
)}
>
{children}
</aside>,
document.body,

View File

@@ -54,7 +54,9 @@ export function Dialog({
)}
>
<div className="flex justify-between items-center p-3 border-b border-b-border-low">
<Title className="text-sm font-medium">{title}</Title>
<Title className="text-sm font-medium text-txt-primary">
{title}
</Title>
<Close asChild>
<Button
tabIndex={-1}

View File

@@ -31,6 +31,7 @@ export { Label } from "./Atoms/Label/Label";
export { PropertyRow } from "./Atoms/PropertyRow/PropertyRow";
export { Table, Thead, Tr, Tbody, Td, Th } from "./Atoms/Table/Table";
export { Tabs, TabLink } from "./Atoms/Tabs/Tabs";
export { Markdown } from "./Atoms/Markdown/Markdown";
// Molecules
export {

View File

@@ -1,3 +1,6 @@
@plugin "@tailwindcss/typography";
@config "../tailwind.config.js";
@theme {
--font-sans: "Geist", "sans-serif";
--color-*: initial;
@@ -31,6 +34,7 @@
--color-active: #e4f7c7;
--color-accent: #141e12;
--color-danger: #ffefef;
--color-danger-plain: #ffefef;
--color-danger-dark: #dc3d43;
--color-success: #eefadc;
--color-info: #edf6ff;
@@ -173,7 +177,7 @@
/* Text colors */
--color-txt-primary: #eceeec;
--color-invert: #151715;
--color-txt-invert: var(--color-invert);
--color-txt-invert: #fcfdfc;
--color-txt-secondary: #9aa299;
--color-txt-tertiary: #778175;
--color-txt-quaternary: #4c514b;
@@ -194,6 +198,7 @@
--color-active: rgba(239, 254, 226, 0.08);
--color-accent: #c4f042;
--color-danger: rgba(254, 58, 61, 0.13);
--color-danger-plain: rgba(254, 58, 61, 0.13);
--color-danger-dark: rgba(255, 89, 95, 0.94);
--color-success: rgba(184, 253, 106, 0.06);
--color-info: rgba(22, 119, 254, 0.14);
@@ -235,3 +240,24 @@
--color-danger-pressed: rgba(254, 58, 61, 0.5);
}
}
*:focus-visible {
box-shadow: var(--shadow-focus);
outline: none;
border-radius: 0.25rem;
}
::-webkit-scrollbar {
width: 7px;
height: 7px;
}
::-webkit-scrollbar-track {
background: var(--color-level-1);
padding: 1px;
}
::-webkit-scrollbar-thumb {
background: var(--color-txt-tertiary);
border-radius: 4px;
}