Move document title ownership from document to version
Title is now owned by document_versions, following the same pattern as classification and document_type. The documents.title column is made nullable with a TODO to drop it. Backend loads title from a latest_versions CTE for ordering purposes only. The frontend resolves title from the latest version, and DocumentTitleForm now operates on DocumentVersion using UpdateDocumentVersion mutation. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -43,11 +43,11 @@ import { LinkedDocumentDialog } from "./LinkedDocumentsDialog";
|
||||
const linkedDocumentFragment = graphql`
|
||||
fragment LinkedDocumentsCardFragment on Document {
|
||||
id
|
||||
title
|
||||
versions(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
documentType
|
||||
status
|
||||
}
|
||||
@@ -217,7 +217,7 @@ function DocumentRow(props: {
|
||||
height={36}
|
||||
className="border-4 border-highlight rounded box-content"
|
||||
/>
|
||||
{document.title}
|
||||
{document.versions.edges[0].node.title}
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
|
||||
@@ -71,10 +71,10 @@ const documentsFragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
documentType
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ function LinkedDocumentsDialogContent(props: Omit<Props, "children">) {
|
||||
|
||||
const filteredDocuments = useMemo(() => {
|
||||
return documents.filter(document =>
|
||||
document.title.toLowerCase().includes(search.toLowerCase()),
|
||||
document.versions.edges[0].node.title.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
}, [documents, search]);
|
||||
|
||||
@@ -192,7 +192,7 @@ function DocumentRow(props: RowProps) {
|
||||
className="py-4 flex items-center gap-4 hover:bg-subtle cursor-pointer px-6 w-full h-[100px]"
|
||||
onClick={() => onClick(props.document.id)}
|
||||
>
|
||||
{props.document.title}
|
||||
{props.document.versions.edges[0].node.title}
|
||||
<DocumentTypeBadge type={props.document.versions.edges[0].node.documentType} />
|
||||
<Button
|
||||
disabled={props.disabled}
|
||||
|
||||
@@ -44,10 +44,10 @@ const documentAccessFragment = graphql`
|
||||
status
|
||||
document {
|
||||
id
|
||||
title
|
||||
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
documentType
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ function toDocumentAccessInfo(
|
||||
return {
|
||||
persisted: node.id !== node.document.id,
|
||||
variant: "info",
|
||||
name: node.document.title,
|
||||
name: node.document.versions?.edges[0]?.node.title ?? "",
|
||||
type: "document",
|
||||
typeLabel: __("Document"),
|
||||
category: node.document.versions?.edges[0]?.node.documentType ?? "",
|
||||
|
||||
@@ -35,7 +35,9 @@ export const documentLayoutQuery = graphql`
|
||||
__typename
|
||||
... on DocumentVersion {
|
||||
id
|
||||
title
|
||||
status
|
||||
...DocumentTitleFormFragment
|
||||
...DocumentActionsDropdown_versionFragment
|
||||
...DocumentLayoutDrawer_versionFragment
|
||||
signatures(first: 0 filter: { activeContract: true }) {
|
||||
@@ -64,14 +66,12 @@ export const documentLayoutQuery = graphql`
|
||||
__typename
|
||||
... on Document {
|
||||
id
|
||||
title
|
||||
status
|
||||
canPublish: permission(action: "core:document-version:publish")
|
||||
...PublishDialog_documentFragment
|
||||
controlInfo: controls(first: 0) {
|
||||
totalCount
|
||||
}
|
||||
...DocumentTitleFormFragment
|
||||
...DocumentActionsDropdown_documentFragment
|
||||
...DocumentLayoutDrawer_documentFragment
|
||||
# We use this on /documents/:documentId
|
||||
@@ -79,7 +79,9 @@ export const documentLayoutQuery = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
status
|
||||
...DocumentTitleFormFragment
|
||||
...DocumentActionsDropdown_versionFragment
|
||||
...DocumentLayoutDrawer_versionFragment
|
||||
signatures(first: 0 filter: { activeContract: true }) {
|
||||
@@ -158,7 +160,7 @@ export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQ
|
||||
to: `/organizations/${organizationId}/documents`,
|
||||
},
|
||||
{
|
||||
label: document.title,
|
||||
label: currentVersion.title,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -182,7 +184,7 @@ export function DocumentLayout(props: { queryRef: PreloadedQuery<DocumentLayoutQ
|
||||
</div>
|
||||
|
||||
<PageHeader
|
||||
title={<DocumentTitleForm fKey={document} />}
|
||||
title={<DocumentTitleForm fKey={currentVersion} />}
|
||||
/>
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -34,7 +34,6 @@ import { CurrentUser } from "#/providers/CurrentUser";
|
||||
const documentFragment = graphql`
|
||||
fragment DocumentActionsDropdown_documentFragment on Document {
|
||||
id
|
||||
title
|
||||
status
|
||||
canUpdate: permission(action: "core:document:update")
|
||||
canArchive: permission(action: "core:document:archive")
|
||||
@@ -45,6 +44,7 @@ const documentFragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
status
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ const unarchiveDocumentMutation = graphql`
|
||||
const versionFragment = graphql`
|
||||
fragment DocumentActionsDropdown_versionFragment on DocumentVersion {
|
||||
id
|
||||
title
|
||||
major
|
||||
minor
|
||||
status
|
||||
@@ -226,7 +227,7 @@ export function DocumentActionsDropdown(props: {
|
||||
{
|
||||
message: sprintf(
|
||||
__("This will archive the document \"%s\". It will no longer be editable."),
|
||||
document.title,
|
||||
lastVersion.title,
|
||||
),
|
||||
variant: "danger",
|
||||
label: __("Archive"),
|
||||
@@ -272,7 +273,7 @@ export function DocumentActionsDropdown(props: {
|
||||
__(
|
||||
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
||||
),
|
||||
document.title,
|
||||
lastVersion.title,
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -306,7 +307,7 @@ export function DocumentActionsDropdown(props: {
|
||||
"This will permanently delete the draft version %s of \"%s\". This action cannot be undone.",
|
||||
),
|
||||
`${version.major}.${version.minor}`,
|
||||
document.title,
|
||||
lastVersion.title,
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -340,7 +341,7 @@ export function DocumentActionsDropdown(props: {
|
||||
if (data.exportDocumentVersionPDF) {
|
||||
const link = window.document.createElement("a");
|
||||
link.href = data.exportDocumentVersionPDF.data;
|
||||
link.download = `${document.title}-v${version.major}.${version.minor}.pdf`;
|
||||
link.download = `${version.title}-v${version.major}.${version.minor}.pdf`;
|
||||
window.document.body.appendChild(link);
|
||||
link.click();
|
||||
window.document.body.removeChild(link);
|
||||
|
||||
@@ -25,7 +25,6 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
const fragment = graphql`
|
||||
fragment DocumentListItemFragment on Document {
|
||||
id
|
||||
title
|
||||
updatedAt
|
||||
canDelete: permission(action: "core:document:delete")
|
||||
defaultApprovers {
|
||||
@@ -36,6 +35,7 @@ const fragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
status
|
||||
major
|
||||
minor
|
||||
@@ -135,7 +135,7 @@ export function DocumentListItem(props: {
|
||||
__(
|
||||
"This will permanently delete the document \"%s\". This action cannot be undone.",
|
||||
),
|
||||
document.title,
|
||||
lastVersion.title,
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -149,7 +149,7 @@ export function DocumentListItem(props: {
|
||||
<Checkbox checked={checked} onChange={onCheck} />
|
||||
</Td>
|
||||
<Td className="min-w-0">
|
||||
<div className="flex gap-4 items-center">{document.title}</div>
|
||||
<div className="flex gap-4 items-center">{lastVersion.title}</div>
|
||||
</Td>
|
||||
<Td className="w-24">
|
||||
<Badge variant={statusVariant[lastVersion.status]}>
|
||||
|
||||
@@ -12,22 +12,22 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { formatError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Button, IconCheckmark1, IconCrossLargeX, IconPencil, Input } from "@probo/ui";
|
||||
import { Button, IconCheckmark1, IconCrossLargeX, IconPencil, Input, useToast } from "@probo/ui";
|
||||
import { useState } from "react";
|
||||
import { useFragment } from "react-relay";
|
||||
import { useFragment, useMutation } from "react-relay";
|
||||
import { graphql } from "relay-runtime";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { DocumentTitleFormFragment$key } from "#/__generated__/core/DocumentTitleFormFragment.graphql";
|
||||
import type { DocumentTitleFormMutation } from "#/__generated__/core/DocumentTitleFormMutation.graphql";
|
||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
|
||||
|
||||
const updateDocumentTitleMutation = graphql`
|
||||
mutation DocumentTitleFormMutation($input: UpdateDocumentInput!) {
|
||||
updateDocument(input: $input) {
|
||||
document {
|
||||
const updateDocumentVersionTitleMutation = graphql`
|
||||
mutation DocumentTitleFormMutation($input: UpdateDocumentVersionInput!) {
|
||||
updateDocumentVersion(input: $input) {
|
||||
documentVersion {
|
||||
...DocumentTitleFormFragment
|
||||
}
|
||||
}
|
||||
@@ -35,10 +35,11 @@ const updateDocumentTitleMutation = graphql`
|
||||
`;
|
||||
|
||||
const fragment = graphql`
|
||||
fragment DocumentTitleFormFragment on Document {
|
||||
fragment DocumentTitleFormFragment on DocumentVersion {
|
||||
id
|
||||
title
|
||||
canUpdate: permission(action: "core:document:update")
|
||||
status
|
||||
canUpdate: permission(action: "core:document-version:update")
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -50,38 +51,40 @@ export function DocumentTitleForm(props: { fKey: DocumentTitleFormFragment$key }
|
||||
const { fKey } = props;
|
||||
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
|
||||
const document = useFragment<DocumentTitleFormFragment$key>(fragment, fKey);
|
||||
const [updateDocument, isUpdatingDocument]
|
||||
= useMutationWithToasts<DocumentTitleFormMutation>(
|
||||
updateDocumentTitleMutation,
|
||||
{
|
||||
successMessage: __("Document updated successfully."),
|
||||
errorMessage: __("Failed to update document"),
|
||||
},
|
||||
);
|
||||
const version = useFragment<DocumentTitleFormFragment$key>(fragment, fKey);
|
||||
const [updateDocumentVersion, isUpdating]
|
||||
= useMutation<DocumentTitleFormMutation>(updateDocumentVersionTitleMutation);
|
||||
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const { register, handleSubmit, reset } = useFormWithSchema(
|
||||
schema,
|
||||
{
|
||||
defaultValues: {
|
||||
title: document.title,
|
||||
title: version.title,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleUpdateTitle = async (data: { title: string }) => {
|
||||
await updateDocument({
|
||||
const handleUpdateTitle = (data: { title: string }) => {
|
||||
updateDocumentVersion({
|
||||
variables: {
|
||||
input: {
|
||||
id: document.id,
|
||||
documentVersionId: version.id,
|
||||
title: data.title,
|
||||
},
|
||||
},
|
||||
onSuccess: () => {
|
||||
onCompleted(_, errors) {
|
||||
if (errors?.length) {
|
||||
toast({ title: __("Error"), description: formatError(__("Failed to update document"), errors), variant: "error" });
|
||||
return;
|
||||
}
|
||||
setIsEditingTitle(false);
|
||||
},
|
||||
onError(error) {
|
||||
toast({ title: __("Error"), description: error.message, variant: "error" });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,7 +110,7 @@ export function DocumentTitleForm(props: { fKey: DocumentTitleFormFragment$key }
|
||||
variant="quaternary"
|
||||
icon={IconCheckmark1}
|
||||
onClick={() => void handleSubmit(handleUpdateTitle)()}
|
||||
disabled={isUpdatingDocument}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
<Button
|
||||
variant="quaternary"
|
||||
@@ -121,8 +124,8 @@ export function DocumentTitleForm(props: { fKey: DocumentTitleFormFragment$key }
|
||||
)
|
||||
: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{document.title}</span>
|
||||
{document.canUpdate && (
|
||||
<span>{version.title}</span>
|
||||
{version.canUpdate && version.status === "DRAFT" && (
|
||||
<Button
|
||||
variant="quaternary"
|
||||
icon={IconPencil}
|
||||
|
||||
Reference in New Issue
Block a user