Implement debounced auto save
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
93
apps/console/src/__generated__/core/DocumentDescriptionPage_updateContentMutation.graphql.ts
generated
Normal file
93
apps/console/src/__generated__/core/DocumentDescriptionPage_updateContentMutation.graphql.ts
generated
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @generated SignedSource<<1f7c6a92ab7920b470a03799e04f4ea3>>
|
||||
* @lightSyntaxTransform
|
||||
* @nogrep
|
||||
*/
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
import { ConcreteRequest } from 'relay-runtime';
|
||||
export type UpdateDocumentVersionContentInput = {
|
||||
content: string;
|
||||
id: string;
|
||||
};
|
||||
export type DocumentDescriptionPage_updateContentMutation$variables = {
|
||||
input: UpdateDocumentVersionContentInput;
|
||||
};
|
||||
export type DocumentDescriptionPage_updateContentMutation$data = {
|
||||
readonly updateDocumentVersionContent: {
|
||||
readonly content: string;
|
||||
};
|
||||
};
|
||||
export type DocumentDescriptionPage_updateContentMutation = {
|
||||
response: DocumentDescriptionPage_updateContentMutation$data;
|
||||
variables: DocumentDescriptionPage_updateContentMutation$variables;
|
||||
};
|
||||
|
||||
const node: ConcreteRequest = (function(){
|
||||
var v0 = [
|
||||
{
|
||||
"defaultValue": null,
|
||||
"kind": "LocalArgument",
|
||||
"name": "input"
|
||||
}
|
||||
],
|
||||
v1 = [
|
||||
{
|
||||
"alias": null,
|
||||
"args": [
|
||||
{
|
||||
"kind": "Variable",
|
||||
"name": "input",
|
||||
"variableName": "input"
|
||||
}
|
||||
],
|
||||
"concreteType": "UpdateDocumentVersionContentPayload",
|
||||
"kind": "LinkedField",
|
||||
"name": "updateDocumentVersionContent",
|
||||
"plural": false,
|
||||
"selections": [
|
||||
{
|
||||
"alias": null,
|
||||
"args": null,
|
||||
"kind": "ScalarField",
|
||||
"name": "content",
|
||||
"storageKey": null
|
||||
}
|
||||
],
|
||||
"storageKey": null
|
||||
}
|
||||
];
|
||||
return {
|
||||
"fragment": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Fragment",
|
||||
"metadata": null,
|
||||
"name": "DocumentDescriptionPage_updateContentMutation",
|
||||
"selections": (v1/*: any*/),
|
||||
"type": "Mutation",
|
||||
"abstractKey": null
|
||||
},
|
||||
"kind": "Request",
|
||||
"operation": {
|
||||
"argumentDefinitions": (v0/*: any*/),
|
||||
"kind": "Operation",
|
||||
"name": "DocumentDescriptionPage_updateContentMutation",
|
||||
"selections": (v1/*: any*/)
|
||||
},
|
||||
"params": {
|
||||
"cacheID": "2a43a5cf619ca1d475b3e480a132878b",
|
||||
"id": null,
|
||||
"metadata": {},
|
||||
"name": "DocumentDescriptionPage_updateContentMutation",
|
||||
"operationKind": "mutation",
|
||||
"text": "mutation DocumentDescriptionPage_updateContentMutation(\n $input: UpdateDocumentVersionContentInput!\n) {\n updateDocumentVersionContent(input: $input) {\n content\n }\n}\n"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
(node as any).hash = "2bfee42c8fe67762416dec052c5f051b";
|
||||
|
||||
export default node;
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2025-2026 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.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { useFormWithSchema } from "../useFormWithSchema";
|
||||
|
||||
export const documentSchema = z.object({
|
||||
title: z.string().min(1, "Title is required"),
|
||||
content: z.string().min(1, "Content is required"),
|
||||
documentType: z.enum(["OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"]),
|
||||
classification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]),
|
||||
});
|
||||
|
||||
export const useDocumentForm = () => {
|
||||
return useFormWithSchema(documentSchema, {
|
||||
defaultValues: {
|
||||
documentType: "POLICY",
|
||||
classification: "INTERNAL",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -23,18 +23,17 @@ import {
|
||||
Input,
|
||||
Label,
|
||||
PropertyRow,
|
||||
Textarea,
|
||||
useDialogRef,
|
||||
} from "@probo/ui";
|
||||
import { type ReactNode } from "react";
|
||||
import { graphql } from "relay-runtime";
|
||||
import type { z } from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CreateDocumentDialogMutation } from "#/__generated__/core/CreateDocumentDialogMutation.graphql";
|
||||
import { ControlledField } from "#/components/form/ControlledField";
|
||||
import { DocumentClassificationOptions } from "#/components/form/DocumentClassificationOptions";
|
||||
import { DocumentTypeOptions } from "#/components/form/DocumentTypeOptions";
|
||||
import { documentSchema, useDocumentForm } from "#/hooks/forms/useDocumentForm";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
@@ -65,6 +64,13 @@ const createDocumentMutation = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const documentSchema = z.object({
|
||||
title: z.string().min(1, "Title is required"),
|
||||
approverIds: z.array(z.string()).min(1, "At least one approver is required"),
|
||||
documentType: z.enum(["OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"]),
|
||||
classification: z.enum(["PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Dialog to create or update a document
|
||||
*/
|
||||
@@ -72,8 +78,15 @@ export function CreateDocumentDialog({ trigger, connection }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
|
||||
const { control, handleSubmit, register, formState, reset }
|
||||
= useDocumentForm();
|
||||
const { control, handleSubmit, register, formState, reset } = useFormWithSchema(
|
||||
documentSchema,
|
||||
{
|
||||
defaultValues: {
|
||||
documentType: "POLICY",
|
||||
classification: "INTERNAL",
|
||||
},
|
||||
},
|
||||
);
|
||||
const errors = formState.errors ?? {};
|
||||
const [createDocument, isLoading]
|
||||
= useMutationWithToasts<CreateDocumentDialogMutation>(createDocumentMutation);
|
||||
@@ -115,14 +128,6 @@ export function CreateDocumentDialog({ trigger, connection }: Props) {
|
||||
placeholder={__("Document title")}
|
||||
{...register("title")}
|
||||
/>
|
||||
<Textarea
|
||||
id="content"
|
||||
variant="ghost"
|
||||
autogrow
|
||||
placeholder={__("Add content")}
|
||||
aria-label={__("Content")}
|
||||
{...register("content")}
|
||||
/>
|
||||
</div>
|
||||
{/* Properties form */}
|
||||
<div className="py-5 px-6 bg-subtle">
|
||||
|
||||
@@ -12,10 +12,15 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { RichEditor } from "@probo/ui";
|
||||
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { RichEditor, useToast } from "@probo/ui";
|
||||
import { useCallback } from "react";
|
||||
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { useDebounceCallback } from "usehooks-ts";
|
||||
|
||||
import type { DocumentDescriptionPage_updateContentMutation } from "#/__generated__/core/DocumentDescriptionPage_updateContentMutation.graphql";
|
||||
import type { DocumentDescriptionPageQuery } from "#/__generated__/core/DocumentDescriptionPageQuery.graphql";
|
||||
|
||||
export const documentDescriptionPageQuery = graphql`
|
||||
@@ -24,6 +29,7 @@ export const documentDescriptionPageQuery = graphql`
|
||||
version: node(id: $versionId) @include(if: $versionSpecified) {
|
||||
__typename
|
||||
... on DocumentVersion {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
@@ -34,6 +40,7 @@ export const documentDescriptionPageQuery = graphql`
|
||||
lastVersion: versions(first: 1 orderBy: { field: CREATED_AT, direction: DESC }) @skip(if: $versionSpecified) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
@@ -43,9 +50,20 @@ export const documentDescriptionPageQuery = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
const updateContentMutation = graphql`
|
||||
mutation DocumentDescriptionPage_updateContentMutation($input: UpdateDocumentVersionContentInput!) {
|
||||
updateDocumentVersionContent(input: $input) {
|
||||
content
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function DocumentDescriptionPage(props: { queryRef: PreloadedQuery<DocumentDescriptionPageQuery> }) {
|
||||
const { queryRef } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { document, version } = usePreloadedQuery<DocumentDescriptionPageQuery>(
|
||||
documentDescriptionPageQuery,
|
||||
queryRef,
|
||||
@@ -54,12 +72,54 @@ export function DocumentDescriptionPage(props: { queryRef: PreloadedQuery<Docume
|
||||
throw new Error("invalid type for node");
|
||||
}
|
||||
|
||||
// const lastVersion = document.lastVersion?.edges[0].node;
|
||||
// const currentVersion = lastVersion ?? version as NonNullable<typeof lastVersion | typeof version>;
|
||||
const lastVersion = document.lastVersion?.edges[0].node;
|
||||
const currentVersion = lastVersion ?? version as NonNullable<typeof lastVersion | typeof version>;
|
||||
|
||||
const [updateContent, _] = useMutation<DocumentDescriptionPage_updateContentMutation>(updateContentMutation);
|
||||
|
||||
const handleUpdate = useDebounceCallback(
|
||||
useCallback((content: string) => {
|
||||
updateContent({
|
||||
variables: {
|
||||
input: {
|
||||
id: currentVersion.id,
|
||||
content,
|
||||
},
|
||||
},
|
||||
onCompleted: (_, errors) => {
|
||||
if (errors?.length) {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: formatError(__("Content not saved"), errors),
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: __("Success"),
|
||||
description: __("Content saved"),
|
||||
variant: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: __("Error"),
|
||||
description: error.message ?? __("Content not saved"),
|
||||
variant: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [currentVersion.id, updateContent, toast, __]),
|
||||
500,
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RichEditor />
|
||||
<RichEditor
|
||||
content={currentVersion.content}
|
||||
onChange={handleUpdate}
|
||||
/>
|
||||
{/* <Markdown content={currentVersion.content} /> */}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user