Add major.minor document versioning
Introduce a two-part version scheme (major.minor) for documents. Drafts start at 0.1 and increment minor on each new draft. Publishing as minor keeps the current version, publishing as major bumps to the next major.0. Both current_published_major and current_published_minor are tracked on the document for exact version lookups. Signatures and approval quorums aggregate across all versions sharing the same major number using CTE joins. Approval page mutations spread the decision fragment so Relay updates the version row state without requiring a page refresh. GraphQL, MCP, and service layer expose separate publishMajor and publishMinor mutations instead of a single mutation with a type enum. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -16,7 +16,7 @@ const fragment = graphql`
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
currentPublishedVersion
|
currentPublishedMajor
|
||||||
...CompliancePageDocumentListItem_documentFragment
|
...CompliancePageDocumentListItem_documentFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export function CompliancePageDocumentList(props: { fragmentRef: CompliancePageD
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
|
|
||||||
const { compliancePage, documents } = useFragment<CompliancePageDocumentListFragment$key>(fragment, fragmentRef);
|
const { compliancePage, documents } = useFragment<CompliancePageDocumentListFragment$key>(fragment, fragmentRef);
|
||||||
const publishedDocuments = documents.edges.filter(({ node }) => node.currentPublishedVersion != null);
|
const publishedDocuments = documents.edges.filter(({ node }) => node.currentPublishedMajor != null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-[10px]">
|
<div className="space-y-[10px]">
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ const unarchiveDocumentMutation = graphql`
|
|||||||
const versionFragment = graphql`
|
const versionFragment = graphql`
|
||||||
fragment DocumentActionsDropdown_versionFragment on DocumentVersion {
|
fragment DocumentActionsDropdown_versionFragment on DocumentVersion {
|
||||||
id
|
id
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
status
|
status
|
||||||
canDeleteDraft: permission(action: "core:document-version:delete-draft")
|
canDeleteDraft: permission(action: "core:document-version:delete-draft")
|
||||||
}
|
}
|
||||||
@@ -229,7 +230,7 @@ export function DocumentActionsDropdownn(props: {
|
|||||||
__(
|
__(
|
||||||
"This will permanently delete the draft version %s of \"%s\". This action cannot be undone.",
|
"This will permanently delete the draft version %s of \"%s\". This action cannot be undone.",
|
||||||
),
|
),
|
||||||
version.version,
|
`${version.major}.${version.minor}`,
|
||||||
document.title,
|
document.title,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -259,7 +260,7 @@ export function DocumentActionsDropdownn(props: {
|
|||||||
if (data.exportDocumentVersionPDF) {
|
if (data.exportDocumentVersionPDF) {
|
||||||
const link = window.document.createElement("a");
|
const link = window.document.createElement("a");
|
||||||
link.href = data.exportDocumentVersionPDF.data;
|
link.href = data.exportDocumentVersionPDF.data;
|
||||||
link.download = `${document.title}-v${version.version}.pdf`;
|
link.download = `${document.title}-v${version.major}.${version.minor}.pdf`;
|
||||||
window.document.body.appendChild(link);
|
window.document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
window.document.body.removeChild(link);
|
window.document.body.removeChild(link);
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ const versionFragment = graphql`
|
|||||||
fragment DocumentLayoutDrawer_versionFragment on DocumentVersion {
|
fragment DocumentLayoutDrawer_versionFragment on DocumentVersion {
|
||||||
id
|
id
|
||||||
classification
|
classification
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
status
|
status
|
||||||
updatedAt
|
updatedAt
|
||||||
publishedAt
|
publishedAt
|
||||||
@@ -202,7 +203,9 @@ export function DocumentLayoutDrawer(props: {
|
|||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
<PropertyRow label={__("Version")}>
|
<PropertyRow label={__("Version")}>
|
||||||
<div className="text-sm text-txt-secondary">
|
<div className="text-sm text-txt-secondary">
|
||||||
{version.version}
|
{version.major}
|
||||||
|
.
|
||||||
|
{version.minor}
|
||||||
</div>
|
</div>
|
||||||
</PropertyRow>
|
</PropertyRow>
|
||||||
<PropertyRow label={__("Last modified")}>
|
<PropertyRow label={__("Last modified")}>
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ const fragment = graphql`
|
|||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
approvalQuorums(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
approvalQuorums(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@@ -148,7 +149,9 @@ export function DocumentListItem(props: {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td className="w-20">
|
<Td className="w-20">
|
||||||
v
|
v
|
||||||
{lastVersion.version}
|
{lastVersion.major}
|
||||||
|
.
|
||||||
|
{lastVersion.minor}
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="w-28">
|
<Td className="w-28">
|
||||||
{getDocumentTypeLabel(__, document.documentType)}
|
{getDocumentTypeLabel(__, document.documentType)}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
|
|||||||
const fragment = graphql`
|
const fragment = graphql`
|
||||||
fragment DocumentVersionsDropdownItemFragment on DocumentVersion {
|
fragment DocumentVersionsDropdownItemFragment on DocumentVersion {
|
||||||
id
|
id
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
status
|
status
|
||||||
publishedAt
|
publishedAt
|
||||||
updatedAt
|
updatedAt
|
||||||
@@ -51,7 +52,9 @@ export function DocumentVersionsDropdownItem(props: {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="text-base text-txt-primary whitespace-nowrap font-bold text-center">
|
<div className="text-base text-txt-primary whitespace-nowrap font-bold text-center">
|
||||||
{version.version}
|
{version.major}
|
||||||
|
.
|
||||||
|
{version.minor}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-[2px] overflow-hidden">
|
<div className="flex-1 space-y-[2px] overflow-hidden">
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { graphql } from "relay-runtime";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { PublishDialog_documentFragment$key } from "#/__generated__/core/PublishDialog_documentFragment.graphql";
|
import type { PublishDialog_documentFragment$key } from "#/__generated__/core/PublishDialog_documentFragment.graphql";
|
||||||
import type { PublishDialog_publishMutation } from "#/__generated__/core/PublishDialog_publishMutation.graphql";
|
import type { PublishDialog_publishMajorMutation } from "#/__generated__/core/PublishDialog_publishMajorMutation.graphql";
|
||||||
|
import type { PublishDialog_publishMinorMutation } from "#/__generated__/core/PublishDialog_publishMinorMutation.graphql";
|
||||||
import type { PublishDialog_requestApprovalMutation } from "#/__generated__/core/PublishDialog_requestApprovalMutation.graphql";
|
import type { PublishDialog_requestApprovalMutation } from "#/__generated__/core/PublishDialog_requestApprovalMutation.graphql";
|
||||||
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
import { PeopleMultiSelectField } from "#/components/form/PeopleMultiSelectField";
|
||||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
@@ -62,9 +63,24 @@ const documentFragment = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const publishMutation = graphql`
|
const publishMajorMutation = graphql`
|
||||||
mutation PublishDialog_publishMutation($input: PublishDocumentVersionInput!) {
|
mutation PublishDialog_publishMajorMutation($input: PublishMajorDocumentVersionInput!) {
|
||||||
publishDocumentVersion(input: $input) {
|
publishMajorDocumentVersion(input: $input) {
|
||||||
|
document {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
}
|
||||||
|
documentVersion {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const publishMinorMutation = graphql`
|
||||||
|
mutation PublishDialog_publishMinorMutation($input: PublishMinorDocumentVersionInput!) {
|
||||||
|
publishMinorDocumentVersion(input: $input) {
|
||||||
document {
|
document {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
@@ -160,21 +176,22 @@ export function PublishDialog({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const [publishVersion, isPublishing] = useMutation<PublishDialog_publishMutation>(publishMutation);
|
const [publishMajor, isPublishingMajor]
|
||||||
const [requestApproval, isRequesting] = useMutation<PublishDialog_requestApprovalMutation>(requestApprovalMutation);
|
= useMutation<PublishDialog_publishMajorMutation>(publishMajorMutation);
|
||||||
|
const [publishMinor, isPublishingMinor]
|
||||||
|
= useMutation<PublishDialog_publishMinorMutation>(publishMinorMutation);
|
||||||
|
const [requestApproval, isRequesting]
|
||||||
|
= useMutation<PublishDialog_requestApprovalMutation>(requestApprovalMutation);
|
||||||
|
|
||||||
const isBusy = isPublishing || isRequesting;
|
const isBusy = isPublishingMajor || isPublishingMinor || isRequesting;
|
||||||
const approverIds = watch("approverIds");
|
const approverIds = watch("approverIds");
|
||||||
const actionRef = useRef<"publish" | "request-approval">("publish");
|
const actionRef = useRef<"publish" | "publish-minor" | "request-approval">("publish");
|
||||||
|
|
||||||
const handlePublish = (data: z.infer<typeof schema>) => {
|
const onPublishCompleted = (_: unknown, errors: ReadonlyArray<{ message: string }> | null) => {
|
||||||
publishVersion({
|
|
||||||
variables: { input: { documentId, changelog: data.changelog } },
|
|
||||||
onCompleted(_, errors) {
|
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: __("Error"),
|
||||||
description: formatError(__("Failed to publish document"), errors),
|
description: formatError(__("Failed to publish document"), [...errors]),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -186,10 +203,25 @@ export function PublishDialog({
|
|||||||
dialogRef.current?.close();
|
dialogRef.current?.close();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
onError(error) {
|
|
||||||
|
const onPublishError = (error: Error) => {
|
||||||
toast({ title: __("Error"), description: error.message, variant: "error" });
|
toast({ title: __("Error"), description: error.message, variant: "error" });
|
||||||
},
|
};
|
||||||
|
|
||||||
|
const handlePublishMajor = (data: z.infer<typeof schema>) => {
|
||||||
|
publishMajor({
|
||||||
|
variables: { input: { documentId, changelog: data.changelog } },
|
||||||
|
onCompleted: onPublishCompleted,
|
||||||
|
onError: onPublishError,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePublishMinor = (data: z.infer<typeof schema>) => {
|
||||||
|
publishMinor({
|
||||||
|
variables: { input: { documentId, changelog: data.changelog } },
|
||||||
|
onCompleted: onPublishCompleted,
|
||||||
|
onError: onPublishError,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,7 +263,9 @@ export function PublishDialog({
|
|||||||
<form
|
<form
|
||||||
onSubmit={e => void handleSubmit((data) => {
|
onSubmit={e => void handleSubmit((data) => {
|
||||||
if (actionRef.current === "publish") {
|
if (actionRef.current === "publish") {
|
||||||
handlePublish(data);
|
handlePublishMajor(data);
|
||||||
|
} else if (actionRef.current === "publish-minor") {
|
||||||
|
handlePublishMinor(data);
|
||||||
} else {
|
} else {
|
||||||
onRequestApproval(data);
|
onRequestApproval(data);
|
||||||
}
|
}
|
||||||
@@ -284,19 +318,15 @@ export function PublishDialog({
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
{hasPendingApproval
|
|
||||||
? (
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
icon={IconUpload}
|
icon={IconUpload}
|
||||||
onClick={() => { actionRef.current = "publish"; }}
|
onClick={() => { actionRef.current = "publish-minor"; }}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
>
|
>
|
||||||
{__("Publish now")}
|
{__("Publish as minor")}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
|
||||||
: (
|
|
||||||
<>
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -306,6 +336,7 @@ export function PublishDialog({
|
|||||||
>
|
>
|
||||||
{__("Publish now")}
|
{__("Publish now")}
|
||||||
</Button>
|
</Button>
|
||||||
|
{!hasPendingApproval && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
icon={IconSend}
|
icon={IconSend}
|
||||||
@@ -314,7 +345,6 @@ export function PublishDialog({
|
|||||||
>
|
>
|
||||||
{__("Request approval")}
|
{__("Request approval")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
|
IconUpload,
|
||||||
IconWarning,
|
IconWarning,
|
||||||
Textarea,
|
Textarea,
|
||||||
useDialogRef,
|
useDialogRef,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { type ReactNode } from "react";
|
import { type ReactNode, useRef } from "react";
|
||||||
import { useMutation } from "react-relay";
|
import { useMutation } from "react-relay";
|
||||||
import { graphql } from "relay-runtime";
|
import { graphql } from "relay-runtime";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { PublishDocumentsDialogMutation } from "#/__generated__/core/PublishDocumentsDialogMutation.graphql";
|
import type { PublishDocumentsDialog_majorMutation } from "#/__generated__/core/PublishDocumentsDialog_majorMutation.graphql";
|
||||||
|
import type { PublishDocumentsDialog_minorMutation } from "#/__generated__/core/PublishDocumentsDialog_minorMutation.graphql";
|
||||||
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -24,11 +26,27 @@ type Props = {
|
|||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const documentsPublishMutation = graphql`
|
const publishMajorMutation = graphql`
|
||||||
mutation PublishDocumentsDialogMutation(
|
mutation PublishDocumentsDialog_majorMutation(
|
||||||
$input: BulkPublishDocumentVersionsInput!
|
$input: BulkPublishDocumentVersionsInput!
|
||||||
) {
|
) {
|
||||||
bulkPublishDocumentVersions(input: $input) {
|
bulkPublishMajorDocumentVersions(input: $input) {
|
||||||
|
documentVersions {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
documents {
|
||||||
|
id
|
||||||
|
...DocumentListItemFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const publishMinorMutation = graphql`
|
||||||
|
mutation PublishDocumentsDialog_minorMutation(
|
||||||
|
$input: BulkPublishDocumentVersionsInput!
|
||||||
|
) {
|
||||||
|
bulkPublishMinorDocumentVersions(input: $input) {
|
||||||
documentVersions {
|
documentVersions {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
@@ -48,12 +66,18 @@ export function PublishDocumentsDialog({
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
|
const actionRef = useRef<"major" | "minor">("major");
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
changelog: z.string().min(1, __("Changelog is required")),
|
changelog: z.string().min(1, __("Changelog is required")),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [publishMutation, isPublishing] = useMutation<PublishDocumentsDialogMutation>(documentsPublishMutation);
|
const [publishMajor, isPublishingMajor]
|
||||||
|
= useMutation<PublishDocumentsDialog_majorMutation>(publishMajorMutation);
|
||||||
|
const [publishMinor, isPublishingMinor]
|
||||||
|
= useMutation<PublishDocumentsDialog_minorMutation>(publishMinorMutation);
|
||||||
|
|
||||||
|
const isBusy = isPublishingMajor || isPublishingMinor;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -65,19 +89,11 @@ export function PublishDocumentsDialog({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: z.infer<typeof schema>) => {
|
const onCompleted = (_: unknown, errors: ReadonlyArray<{ message: string }> | null) => {
|
||||||
publishMutation({
|
|
||||||
variables: {
|
|
||||||
input: {
|
|
||||||
documentIds,
|
|
||||||
changelog: data.changelog,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onCompleted(_, errors) {
|
|
||||||
if (errors?.length) {
|
if (errors?.length) {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: __("Error"),
|
||||||
description: formatError(__("Failed to publish documents"), errors),
|
description: formatError(__("Failed to publish documents"), [...errors]),
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -89,15 +105,29 @@ export function PublishDocumentsDialog({
|
|||||||
dialogRef.current?.close();
|
dialogRef.current?.close();
|
||||||
onSave();
|
onSave();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
onError(error) {
|
|
||||||
|
const onError = (error: Error) => {
|
||||||
toast({
|
toast({
|
||||||
title: __("Error"),
|
title: __("Error"),
|
||||||
description: error.message,
|
description: error.message,
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (data: z.infer<typeof schema>) => {
|
||||||
|
const variables = {
|
||||||
|
input: {
|
||||||
|
documentIds,
|
||||||
|
changelog: data.changelog,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (actionRef.current === "minor") {
|
||||||
|
publishMinor({ variables, onCompleted, onError });
|
||||||
|
} else {
|
||||||
|
publishMajor({ variables, onCompleted, onError });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -135,7 +165,20 @@ export function PublishDocumentsDialog({
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button type="submit" disabled={isPublishing}>
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
|
icon={IconUpload}
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => { actionRef.current = "minor"; }}
|
||||||
|
>
|
||||||
|
{__("Publish as minor")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => { actionRef.current = "major"; }}
|
||||||
|
>
|
||||||
{sprintf(__("Publish %s documents"), documentIds.length)}
|
{sprintf(__("Publish %s documents"), documentIds.length)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ const createDraftDocument = graphql`
|
|||||||
content
|
content
|
||||||
status
|
status
|
||||||
publishedAt
|
publishedAt
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
updatedAt
|
updatedAt
|
||||||
signatures(first: 100) {
|
signatures(first: 100) {
|
||||||
edges {
|
edges {
|
||||||
|
|||||||
@@ -70,9 +70,11 @@ const documentFragment = graphql`
|
|||||||
const versionRowFragment = graphql`
|
const versionRowFragment = graphql`
|
||||||
fragment DocumentApprovePageVersionRowFragment on EmployeeDocumentVersion {
|
fragment DocumentApprovePageVersionRowFragment on EmployeeDocumentVersion {
|
||||||
id
|
id
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
publishedAt
|
publishedAt
|
||||||
approvalDecision {
|
approvalDecision {
|
||||||
|
id
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,10 +98,7 @@ const approveDocumentVersionMutation = graphql`
|
|||||||
) {
|
) {
|
||||||
approveDocumentVersion(input: $input) {
|
approveDocumentVersion(input: $input) {
|
||||||
approvalDecision {
|
approvalDecision {
|
||||||
id
|
...DocumentApprovePageDecisionFragment
|
||||||
state
|
|
||||||
decidedAt
|
|
||||||
comment
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,10 +110,7 @@ const rejectDocumentVersionMutation = graphql`
|
|||||||
) {
|
) {
|
||||||
rejectDocumentVersion(input: $input) {
|
rejectDocumentVersion(input: $input) {
|
||||||
approvalDecision {
|
approvalDecision {
|
||||||
id
|
...DocumentApprovePageDecisionFragment
|
||||||
state
|
|
||||||
decidedAt
|
|
||||||
comment
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,14 +188,14 @@ function VersionRow({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{versionData.publishedAt
|
{versionData.publishedAt
|
||||||
? `v${versionData.version} - ${(() => {
|
? `v${versionData.major}.${versionData.minor} - ${(() => {
|
||||||
const date = new Date(versionData.publishedAt);
|
const date = new Date(versionData.publishedAt);
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
return `${day}/${month}/${year}`;
|
return `${day}/${month}/${year}`;
|
||||||
})()}`
|
})()}`
|
||||||
: `v${versionData.version}`}
|
: `v${versionData.major}.${versionData.minor}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const fragment = graphql`
|
|||||||
fragment VersionRowFragment on EmployeeDocumentVersion {
|
fragment VersionRowFragment on EmployeeDocumentVersion {
|
||||||
# eslint-disable-next-line relay/unused-fields
|
# eslint-disable-next-line relay/unused-fields
|
||||||
id
|
id
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
signed
|
signed
|
||||||
publishedAt
|
publishedAt
|
||||||
}
|
}
|
||||||
@@ -55,14 +56,14 @@ export function VersionRow({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{versionData.publishedAt
|
{versionData.publishedAt
|
||||||
? `v${versionData.version} - ${(() => {
|
? `v${versionData.major}.${versionData.minor} - ${(() => {
|
||||||
const date = new Date(versionData.publishedAt);
|
const date = new Date(versionData.publishedAt);
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
return `${day}/${month}/${year}`;
|
return `${day}/${month}/${year}`;
|
||||||
})()}`
|
})()}`
|
||||||
: `v${versionData.version}`}
|
: `v${versionData.major}.${versionData.minor}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
|
|||||||
@@ -223,7 +223,8 @@ func TestDocumentVersion_PublishVersion(t *testing.T) {
|
|||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
version
|
major
|
||||||
|
minor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,7 +240,8 @@ func TestDocumentVersion_PublishVersion(t *testing.T) {
|
|||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Version int `json:"version"`
|
Major int `json:"major"`
|
||||||
|
Minor int `json:"minor"`
|
||||||
} `json:"node"`
|
} `json:"node"`
|
||||||
} `json:"edges"`
|
} `json:"edges"`
|
||||||
} `json:"versions"`
|
} `json:"versions"`
|
||||||
@@ -251,7 +253,8 @@ func TestDocumentVersion_PublishVersion(t *testing.T) {
|
|||||||
require.NotEmpty(t, result.Node.Versions.Edges)
|
require.NotEmpty(t, result.Node.Versions.Edges)
|
||||||
|
|
||||||
assert.Equal(t, "PUBLISHED", result.Node.Versions.Edges[0].Node.Status)
|
assert.Equal(t, "PUBLISHED", result.Node.Versions.Edges[0].Node.Status)
|
||||||
assert.Equal(t, 1, result.Node.Versions.Edges[0].Node.Version)
|
assert.Equal(t, 1, result.Node.Versions.Edges[0].Node.Major)
|
||||||
|
assert.Equal(t, 0, result.Node.Versions.Edges[0].Node.Minor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDocumentVersion_CreateDraft(t *testing.T) {
|
func TestDocumentVersion_CreateDraft(t *testing.T) {
|
||||||
@@ -435,8 +438,8 @@ func TestDocumentVersion_BulkPublish(t *testing.T) {
|
|||||||
approveTestDocument(t, owner, docID2)
|
approveTestDocument(t, owner, docID2)
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
mutation BulkPublishDocumentVersions($input: BulkPublishDocumentVersionsInput!) {
|
mutation BulkPublishMajorDocumentVersions($input: BulkPublishDocumentVersionsInput!) {
|
||||||
bulkPublishDocumentVersions(input: $input) {
|
bulkPublishMajorDocumentVersions(input: $input) {
|
||||||
documentVersions {
|
documentVersions {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
@@ -446,12 +449,12 @@ func TestDocumentVersion_BulkPublish(t *testing.T) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
BulkPublishDocumentVersions struct {
|
BulkPublishMajorDocumentVersions struct {
|
||||||
DocumentVersions []struct {
|
DocumentVersions []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
} `json:"documentVersions"`
|
} `json:"documentVersions"`
|
||||||
} `json:"bulkPublishDocumentVersions"`
|
} `json:"bulkPublishMajorDocumentVersions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := owner.Execute(query, map[string]any{
|
err := owner.Execute(query, map[string]any{
|
||||||
@@ -462,8 +465,8 @@ func TestDocumentVersion_BulkPublish(t *testing.T) {
|
|||||||
}, &result)
|
}, &result)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 2, len(result.BulkPublishDocumentVersions.DocumentVersions))
|
assert.Equal(t, 2, len(result.BulkPublishMajorDocumentVersions.DocumentVersions))
|
||||||
for _, dv := range result.BulkPublishDocumentVersions.DocumentVersions {
|
for _, dv := range result.BulkPublishMajorDocumentVersions.DocumentVersions {
|
||||||
assert.Equal(t, "PUBLISHED", dv.Status)
|
assert.Equal(t, "PUBLISHED", dv.Status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ type (
|
|||||||
Title string `db:"title"`
|
Title string `db:"title"`
|
||||||
DocumentType DocumentType `db:"document_type"`
|
DocumentType DocumentType `db:"document_type"`
|
||||||
Classification DocumentClassification `db:"classification"`
|
Classification DocumentClassification `db:"classification"`
|
||||||
CurrentPublishedVersion *int `db:"current_published_version"`
|
CurrentPublishedMajor *int `db:"current_published_major"`
|
||||||
|
CurrentPublishedMinor *int `db:"current_published_minor"`
|
||||||
TrustCenterVisibility TrustCenterVisibility `db:"trust_center_visibility"`
|
TrustCenterVisibility TrustCenterVisibility `db:"trust_center_visibility"`
|
||||||
Status DocumentStatus `db:"status"`
|
Status DocumentStatus `db:"status"`
|
||||||
ArchivedAt *time.Time `db:"archived_at"`
|
ArchivedAt *time.Time `db:"archived_at"`
|
||||||
@@ -128,7 +129,8 @@ SELECT
|
|||||||
title,
|
title,
|
||||||
document_type,
|
document_type,
|
||||||
classification,
|
classification,
|
||||||
current_published_version,
|
current_published_major,
|
||||||
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
status,
|
status,
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -181,7 +183,8 @@ SELECT
|
|||||||
title,
|
title,
|
||||||
document_type,
|
document_type,
|
||||||
classification,
|
classification,
|
||||||
current_published_version,
|
current_published_major,
|
||||||
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
status,
|
status,
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -235,7 +238,8 @@ SELECT
|
|||||||
title,
|
title,
|
||||||
document_type,
|
document_type,
|
||||||
classification,
|
classification,
|
||||||
current_published_version,
|
current_published_major,
|
||||||
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
status,
|
status,
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -318,7 +322,8 @@ SELECT
|
|||||||
title,
|
title,
|
||||||
document_type,
|
document_type,
|
||||||
classification,
|
classification,
|
||||||
current_published_version,
|
current_published_major,
|
||||||
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
status,
|
status,
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -370,7 +375,8 @@ SELECT
|
|||||||
title,
|
title,
|
||||||
document_type,
|
document_type,
|
||||||
classification,
|
classification,
|
||||||
current_published_version,
|
current_published_major,
|
||||||
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
status,
|
status,
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -424,7 +430,8 @@ WITH published_documents AS (
|
|||||||
documents d
|
documents d
|
||||||
LEFT JOIN document_versions dv
|
LEFT JOIN document_versions dv
|
||||||
ON dv.document_id = d.id
|
ON dv.document_id = d.id
|
||||||
AND dv.version_number = d.current_published_version
|
AND dv.major = d.current_published_major
|
||||||
|
AND dv.minor = d.current_published_minor
|
||||||
WHERE
|
WHERE
|
||||||
d.deleted_at IS NULL
|
d.deleted_at IS NULL
|
||||||
AND d.organization_id = @organization_id
|
AND d.organization_id = @organization_id
|
||||||
@@ -435,7 +442,8 @@ SELECT
|
|||||||
COALESCE(published_title, title) AS title,
|
COALESCE(published_title, title) AS title,
|
||||||
document_type,
|
document_type,
|
||||||
classification,
|
classification,
|
||||||
current_published_version,
|
current_published_major,
|
||||||
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
status,
|
status,
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -484,7 +492,8 @@ INSERT INTO
|
|||||||
title,
|
title,
|
||||||
document_type,
|
document_type,
|
||||||
classification,
|
classification,
|
||||||
current_published_version,
|
current_published_major,
|
||||||
|
current_published_minor,
|
||||||
trust_center_visibility,
|
trust_center_visibility,
|
||||||
status,
|
status,
|
||||||
archived_at,
|
archived_at,
|
||||||
@@ -498,7 +507,8 @@ VALUES (
|
|||||||
@title,
|
@title,
|
||||||
@document_type,
|
@document_type,
|
||||||
@classification,
|
@classification,
|
||||||
@current_published_version,
|
@current_published_major,
|
||||||
|
@current_published_minor,
|
||||||
@trust_center_visibility,
|
@trust_center_visibility,
|
||||||
@status,
|
@status,
|
||||||
@archived_at,
|
@archived_at,
|
||||||
@@ -514,7 +524,8 @@ VALUES (
|
|||||||
"title": p.Title,
|
"title": p.Title,
|
||||||
"document_type": p.DocumentType,
|
"document_type": p.DocumentType,
|
||||||
"classification": p.Classification,
|
"classification": p.Classification,
|
||||||
"current_published_version": p.CurrentPublishedVersion,
|
"current_published_major": p.CurrentPublishedMajor,
|
||||||
|
"current_published_minor": p.CurrentPublishedMinor,
|
||||||
"trust_center_visibility": p.TrustCenterVisibility,
|
"trust_center_visibility": p.TrustCenterVisibility,
|
||||||
"status": p.Status,
|
"status": p.Status,
|
||||||
"archived_at": p.ArchivedAt,
|
"archived_at": p.ArchivedAt,
|
||||||
@@ -572,7 +583,8 @@ UPDATE
|
|||||||
documents
|
documents
|
||||||
SET
|
SET
|
||||||
title = @title,
|
title = @title,
|
||||||
current_published_version = @current_published_version,
|
current_published_major = @current_published_major,
|
||||||
|
current_published_minor = @current_published_minor,
|
||||||
document_type = @document_type,
|
document_type = @document_type,
|
||||||
classification = @classification,
|
classification = @classification,
|
||||||
trust_center_visibility = @trust_center_visibility,
|
trust_center_visibility = @trust_center_visibility,
|
||||||
@@ -590,7 +602,8 @@ WHERE
|
|||||||
"document_id": p.ID,
|
"document_id": p.ID,
|
||||||
"updated_at": time.Now(),
|
"updated_at": time.Now(),
|
||||||
"title": p.Title,
|
"title": p.Title,
|
||||||
"current_published_version": p.CurrentPublishedVersion,
|
"current_published_major": p.CurrentPublishedMajor,
|
||||||
|
"current_published_minor": p.CurrentPublishedMinor,
|
||||||
"document_type": p.DocumentType,
|
"document_type": p.DocumentType,
|
||||||
"classification": p.Classification,
|
"classification": p.Classification,
|
||||||
"trust_center_visibility": p.TrustCenterVisibility,
|
"trust_center_visibility": p.TrustCenterVisibility,
|
||||||
@@ -666,7 +679,8 @@ SELECT
|
|||||||
scoped_documents.title,
|
scoped_documents.title,
|
||||||
scoped_documents.document_type,
|
scoped_documents.document_type,
|
||||||
scoped_documents.classification,
|
scoped_documents.classification,
|
||||||
scoped_documents.current_published_version,
|
scoped_documents.current_published_major,
|
||||||
|
scoped_documents.current_published_minor,
|
||||||
scoped_documents.trust_center_visibility,
|
scoped_documents.trust_center_visibility,
|
||||||
scoped_documents.status,
|
scoped_documents.status,
|
||||||
scoped_documents.archived_at,
|
scoped_documents.archived_at,
|
||||||
@@ -757,7 +771,8 @@ SELECT
|
|||||||
scoped_documents.title,
|
scoped_documents.title,
|
||||||
scoped_documents.document_type,
|
scoped_documents.document_type,
|
||||||
scoped_documents.classification,
|
scoped_documents.classification,
|
||||||
scoped_documents.current_published_version,
|
scoped_documents.current_published_major,
|
||||||
|
scoped_documents.current_published_minor,
|
||||||
scoped_documents.trust_center_visibility,
|
scoped_documents.trust_center_visibility,
|
||||||
scoped_documents.status,
|
scoped_documents.status,
|
||||||
scoped_documents.archived_at,
|
scoped_documents.archived_at,
|
||||||
@@ -874,28 +889,29 @@ func (p *Document) IsLastSignableVersionSignedByUserEmail(
|
|||||||
userEmail mail.Addr,
|
userEmail mail.Addr,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
q := `
|
q := `
|
||||||
WITH last_signable_version AS (
|
WITH max_signable_major AS (
|
||||||
|
SELECT MAX(dv.major) AS major
|
||||||
|
FROM document_versions dv
|
||||||
|
INNER JOIN document_version_signatures dvs ON dvs.document_version_id = dv.id
|
||||||
|
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
|
||||||
|
INNER JOIN identities i ON p.identity_id = i.id
|
||||||
|
WHERE dv.document_id = @document_id
|
||||||
|
AND i.email_address = @user_email::CITEXT
|
||||||
|
),
|
||||||
|
last_signable_version AS (
|
||||||
SELECT
|
SELECT
|
||||||
d.id AS document_id,
|
d.id AS document_id,
|
||||||
d.tenant_id,
|
d.tenant_id,
|
||||||
dv.version_number,
|
dv.major,
|
||||||
dvs.state
|
dvs.state
|
||||||
FROM documents d
|
FROM documents d
|
||||||
INNER JOIN document_versions dv ON dv.document_id = d.id
|
INNER JOIN document_versions dv ON dv.document_id = d.id
|
||||||
|
INNER JOIN max_signable_major msm ON dv.major = msm.major
|
||||||
INNER JOIN document_version_signatures dvs ON dvs.document_version_id = dv.id
|
INNER JOIN document_version_signatures dvs ON dvs.document_version_id = dv.id
|
||||||
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
|
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
|
||||||
INNER JOIN identities i ON p.identity_id = i.id
|
INNER JOIN identities i ON p.identity_id = i.id
|
||||||
WHERE d.id = @document_id
|
WHERE d.id = @document_id
|
||||||
AND i.email_address = @user_email::CITEXT
|
AND i.email_address = @user_email::CITEXT
|
||||||
AND dv.version_number = (
|
|
||||||
SELECT MAX(dv2.version_number)
|
|
||||||
FROM document_versions dv2
|
|
||||||
INNER JOIN document_version_signatures dvs2 ON dvs2.document_version_id = dv2.id
|
|
||||||
INNER JOIN iam_membership_profiles p2 ON dvs2.signed_by_profile_id = p2.id
|
|
||||||
INNER JOIN identities i2 ON p2.identity_id = i2.id
|
|
||||||
WHERE dv2.document_id = d.id
|
|
||||||
AND i2.email_address = @user_email::CITEXT
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
@@ -938,7 +954,7 @@ WITH viewer_decision AS (
|
|||||||
SELECT
|
SELECT
|
||||||
dvad.tenant_id,
|
dvad.tenant_id,
|
||||||
dvad.state,
|
dvad.state,
|
||||||
dv.version_number,
|
dv.major,
|
||||||
dvaq.created_at AS quorum_created_at
|
dvaq.created_at AS quorum_created_at
|
||||||
FROM documents d
|
FROM documents d
|
||||||
INNER JOIN document_versions dv ON dv.document_id = d.id
|
INNER JOIN document_versions dv ON dv.document_id = d.id
|
||||||
@@ -951,7 +967,7 @@ WITH viewer_decision AS (
|
|||||||
SELECT state
|
SELECT state
|
||||||
FROM viewer_decision
|
FROM viewer_decision
|
||||||
WHERE %s
|
WHERE %s
|
||||||
ORDER BY version_number DESC, quorum_created_at DESC
|
ORDER BY major DESC, quorum_created_at DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -131,8 +131,8 @@ func (f *DocumentFilter) SQLFragment() string {
|
|||||||
AND
|
AND
|
||||||
CASE
|
CASE
|
||||||
WHEN @published::boolean IS NULL THEN TRUE
|
WHEN @published::boolean IS NULL THEN TRUE
|
||||||
WHEN @published::boolean IS TRUE THEN current_published_version IS NOT NULL
|
WHEN @published::boolean IS TRUE THEN current_published_major IS NOT NULL
|
||||||
WHEN @published::boolean IS FALSE THEN current_published_version IS NULL
|
WHEN @published::boolean IS FALSE THEN current_published_major IS NULL
|
||||||
END
|
END
|
||||||
AND
|
AND
|
||||||
CASE
|
CASE
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ type (
|
|||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
DocumentID gid.GID `db:"document_id"`
|
DocumentID gid.GID `db:"document_id"`
|
||||||
Title string `db:"title"`
|
Title string `db:"title"`
|
||||||
VersionNumber int `db:"version_number"`
|
Major int `db:"major"`
|
||||||
|
Minor int `db:"minor"`
|
||||||
Classification DocumentClassification `db:"classification"`
|
Classification DocumentClassification `db:"classification"`
|
||||||
Content string `db:"content"`
|
Content string `db:"content"`
|
||||||
Changelog string `db:"changelog"`
|
Changelog string `db:"changelog"`
|
||||||
@@ -116,7 +117,8 @@ SELECT
|
|||||||
organization_id,
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
version_number,
|
major,
|
||||||
|
minor,
|
||||||
classification,
|
classification,
|
||||||
content,
|
content,
|
||||||
changelog,
|
changelog,
|
||||||
@@ -177,7 +179,8 @@ SELECT
|
|||||||
organization_id,
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
version_number,
|
major,
|
||||||
|
minor,
|
||||||
classification,
|
classification,
|
||||||
content,
|
content,
|
||||||
changelog,
|
changelog,
|
||||||
@@ -227,7 +230,8 @@ INSERT INTO document_versions (
|
|||||||
organization_id,
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
version_number,
|
major,
|
||||||
|
minor,
|
||||||
classification,
|
classification,
|
||||||
content,
|
content,
|
||||||
changelog,
|
changelog,
|
||||||
@@ -241,7 +245,8 @@ VALUES (
|
|||||||
@organization_id,
|
@organization_id,
|
||||||
@document_id,
|
@document_id,
|
||||||
@title,
|
@title,
|
||||||
@version_number,
|
@major,
|
||||||
|
@minor,
|
||||||
@classification,
|
@classification,
|
||||||
@content,
|
@content,
|
||||||
@changelog,
|
@changelog,
|
||||||
@@ -256,7 +261,8 @@ VALUES (
|
|||||||
"organization_id": dv.OrganizationID,
|
"organization_id": dv.OrganizationID,
|
||||||
"document_id": dv.DocumentID,
|
"document_id": dv.DocumentID,
|
||||||
"title": dv.Title,
|
"title": dv.Title,
|
||||||
"version_number": dv.VersionNumber,
|
"major": dv.Major,
|
||||||
|
"minor": dv.Minor,
|
||||||
"classification": dv.Classification,
|
"classification": dv.Classification,
|
||||||
"content": dv.Content,
|
"content": dv.Content,
|
||||||
"changelog": dv.Changelog,
|
"changelog": dv.Changelog,
|
||||||
@@ -270,7 +276,7 @@ VALUES (
|
|||||||
var pgErr *pgconn.PgError
|
var pgErr *pgconn.PgError
|
||||||
if errors.As(err, &pgErr) {
|
if errors.As(err, &pgErr) {
|
||||||
if pgErr.Code == "23505" {
|
if pgErr.Code == "23505" {
|
||||||
if pgErr.ConstraintName == "document_versions_document_id_version_number_key" || pgErr.ConstraintName == "document_one_draft_version_idx" {
|
if pgErr.ConstraintName == "document_versions_document_id_major_minor_key" || pgErr.ConstraintName == "document_one_draft_version_idx" {
|
||||||
return ErrResourceAlreadyExists
|
return ErrResourceAlreadyExists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,12 +287,13 @@ VALUES (
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dv *DocumentVersion) LoadByDocumentIDAndVersionNumber(
|
func (dv *DocumentVersion) LoadByDocumentIDAndVersion(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
documentID gid.GID,
|
documentID gid.GID,
|
||||||
versionNumber int,
|
major int,
|
||||||
|
minor int,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -294,7 +301,8 @@ SELECT
|
|||||||
organization_id,
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
version_number,
|
major,
|
||||||
|
minor,
|
||||||
classification,
|
classification,
|
||||||
content,
|
content,
|
||||||
changelog,
|
changelog,
|
||||||
@@ -307,7 +315,8 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND document_id = @document_id
|
AND document_id = @document_id
|
||||||
AND version_number = @version_number
|
AND major = @major
|
||||||
|
AND minor = @minor
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -315,7 +324,8 @@ LIMIT 1;
|
|||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"document_id": documentID,
|
"document_id": documentID,
|
||||||
"version_number": versionNumber,
|
"major": major,
|
||||||
|
"minor": minor,
|
||||||
}
|
}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
@@ -346,7 +356,8 @@ SELECT
|
|||||||
organization_id,
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
version_number,
|
major,
|
||||||
|
minor,
|
||||||
classification,
|
classification,
|
||||||
content,
|
content,
|
||||||
changelog,
|
changelog,
|
||||||
@@ -396,7 +407,8 @@ SELECT
|
|||||||
organization_id,
|
organization_id,
|
||||||
document_id,
|
document_id,
|
||||||
title,
|
title,
|
||||||
version_number,
|
major,
|
||||||
|
minor,
|
||||||
classification,
|
classification,
|
||||||
content,
|
content,
|
||||||
changelog,
|
changelog,
|
||||||
@@ -444,6 +456,8 @@ func (dv DocumentVersion) Update(
|
|||||||
q := `
|
q := `
|
||||||
UPDATE document_versions SET
|
UPDATE document_versions SET
|
||||||
title = @title,
|
title = @title,
|
||||||
|
major = @major,
|
||||||
|
minor = @minor,
|
||||||
changelog = @changelog,
|
changelog = @changelog,
|
||||||
status = @status,
|
status = @status,
|
||||||
content = @content,
|
content = @content,
|
||||||
@@ -459,6 +473,8 @@ WHERE %s
|
|||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"document_version_id": dv.ID,
|
"document_version_id": dv.ID,
|
||||||
"title": dv.Title,
|
"title": dv.Title,
|
||||||
|
"major": dv.Major,
|
||||||
|
"minor": dv.Minor,
|
||||||
"changelog": dv.Changelog,
|
"changelog": dv.Changelog,
|
||||||
"status": dv.Status,
|
"status": dv.Status,
|
||||||
"content": dv.Content,
|
"content": dv.Content,
|
||||||
|
|||||||
@@ -115,19 +115,26 @@ func (q *DocumentVersionApprovalQuorum) LoadLastByDocumentVersionID(
|
|||||||
documentVersionID gid.GID,
|
documentVersionID gid.GID,
|
||||||
) error {
|
) error {
|
||||||
query := `
|
query := `
|
||||||
|
WITH source_version AS (
|
||||||
|
SELECT document_id, major FROM document_versions WHERE id = @version_id
|
||||||
|
),
|
||||||
|
major_versions AS (
|
||||||
|
SELECT dv.id FROM document_versions dv
|
||||||
|
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
document_version_approval_quorums.id,
|
||||||
organization_id,
|
document_version_approval_quorums.organization_id,
|
||||||
version_id,
|
document_version_approval_quorums.version_id,
|
||||||
status,
|
document_version_approval_quorums.status,
|
||||||
created_at,
|
document_version_approval_quorums.created_at,
|
||||||
updated_at
|
document_version_approval_quorums.updated_at
|
||||||
FROM
|
FROM
|
||||||
document_version_approval_quorums
|
document_version_approval_quorums
|
||||||
|
INNER JOIN major_versions mv ON document_version_approval_quorums.version_id = mv.id
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND version_id = @version_id
|
ORDER BY document_version_approval_quorums.created_at DESC
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -162,18 +169,25 @@ func (q *DocumentVersionApprovalQuorums) LoadAllByDocumentVersionID(
|
|||||||
cursor *page.Cursor[DocumentVersionApprovalQuorumOrderField],
|
cursor *page.Cursor[DocumentVersionApprovalQuorumOrderField],
|
||||||
) error {
|
) error {
|
||||||
query := `
|
query := `
|
||||||
|
WITH source_version AS (
|
||||||
|
SELECT document_id, major FROM document_versions WHERE id = @version_id
|
||||||
|
),
|
||||||
|
major_versions AS (
|
||||||
|
SELECT dv.id FROM document_versions dv
|
||||||
|
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
document_version_approval_quorums.id,
|
||||||
organization_id,
|
document_version_approval_quorums.organization_id,
|
||||||
version_id,
|
document_version_approval_quorums.version_id,
|
||||||
status,
|
document_version_approval_quorums.status,
|
||||||
created_at,
|
document_version_approval_quorums.created_at,
|
||||||
updated_at
|
document_version_approval_quorums.updated_at
|
||||||
FROM
|
FROM
|
||||||
document_version_approval_quorums
|
document_version_approval_quorums
|
||||||
|
INNER JOIN major_versions mv ON document_version_approval_quorums.version_id = mv.id
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND version_id = @version_id
|
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -205,13 +219,20 @@ func (q *DocumentVersionApprovalQuorums) CountByDocumentVersionID(
|
|||||||
documentVersionID gid.GID,
|
documentVersionID gid.GID,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
query := `
|
query := `
|
||||||
|
WITH source_version AS (
|
||||||
|
SELECT document_id, major FROM document_versions WHERE id = @version_id
|
||||||
|
),
|
||||||
|
major_versions AS (
|
||||||
|
SELECT dv.id FROM document_versions dv
|
||||||
|
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(id)
|
COUNT(document_version_approval_quorums.id)
|
||||||
FROM
|
FROM
|
||||||
document_version_approval_quorums
|
document_version_approval_quorums
|
||||||
|
INNER JOIN major_versions mv ON document_version_approval_quorums.version_id = mv.id
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND version_id = @version_id
|
|
||||||
`
|
`
|
||||||
|
|
||||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||||
|
|||||||
@@ -236,21 +236,28 @@ func (pvss *DocumentVersionSignatures) LoadByDocumentVersionID(
|
|||||||
filter *DocumentVersionSignatureFilter,
|
filter *DocumentVersionSignatureFilter,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
|
WITH source_version AS (
|
||||||
|
SELECT document_id, major FROM document_versions WHERE id = @document_version_id
|
||||||
|
),
|
||||||
|
major_versions AS (
|
||||||
|
SELECT dv.id FROM document_versions dv
|
||||||
|
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
document_version_signatures.id,
|
||||||
organization_id,
|
document_version_signatures.organization_id,
|
||||||
document_version_id,
|
document_version_signatures.document_version_id,
|
||||||
state,
|
document_version_signatures.state,
|
||||||
signed_by_profile_id,
|
document_version_signatures.signed_by_profile_id,
|
||||||
signed_at,
|
document_version_signatures.signed_at,
|
||||||
requested_at,
|
document_version_signatures.requested_at,
|
||||||
created_at,
|
document_version_signatures.created_at,
|
||||||
updated_at
|
document_version_signatures.updated_at
|
||||||
FROM
|
FROM
|
||||||
document_version_signatures
|
document_version_signatures
|
||||||
|
INNER JOIN major_versions mv ON document_version_signatures.document_version_id = mv.id
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND document_version_id = @document_version_id
|
|
||||||
AND %s
|
AND %s
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
@@ -348,7 +355,14 @@ func (pvss *DocumentVersionSignaturesWithPeople) LoadByDocumentVersionIDWithPeop
|
|||||||
limit int,
|
limit int,
|
||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
WITH sigs AS (
|
WITH source_version AS (
|
||||||
|
SELECT document_id, major FROM document_versions WHERE id = @document_version_id
|
||||||
|
),
|
||||||
|
major_versions AS (
|
||||||
|
SELECT dv.id FROM document_versions dv
|
||||||
|
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
|
||||||
|
),
|
||||||
|
signatures_with_people AS (
|
||||||
SELECT
|
SELECT
|
||||||
dvs.id,
|
dvs.id,
|
||||||
dvs.organization_id,
|
dvs.organization_id,
|
||||||
@@ -360,16 +374,10 @@ WITH sigs AS (
|
|||||||
dvs.requested_at,
|
dvs.requested_at,
|
||||||
dvs.created_at,
|
dvs.created_at,
|
||||||
dvs.updated_at,
|
dvs.updated_at,
|
||||||
p.full_name as signed_by_full_name
|
p.full_name AS signed_by_full_name
|
||||||
FROM
|
FROM document_version_signatures dvs
|
||||||
document_version_signatures dvs
|
INNER JOIN major_versions mv ON dvs.document_version_id = mv.id
|
||||||
INNER JOIN
|
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
|
||||||
iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
|
|
||||||
WHERE
|
|
||||||
dvs.document_version_id = @document_version_id
|
|
||||||
ORDER BY
|
|
||||||
p.full_name ASC
|
|
||||||
LIMIT @limit
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@@ -383,9 +391,12 @@ SELECT
|
|||||||
updated_at,
|
updated_at,
|
||||||
signed_by_full_name
|
signed_by_full_name
|
||||||
FROM
|
FROM
|
||||||
sigs
|
signatures_with_people
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
|
ORDER BY
|
||||||
|
signed_by_full_name ASC
|
||||||
|
LIMIT @limit
|
||||||
`
|
`
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
@@ -419,18 +430,31 @@ func (pvs *DocumentVersionSignature) IsSignedByUserEmail(
|
|||||||
userEmail mail.Addr,
|
userEmail mail.Addr,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
q := `
|
q := `
|
||||||
SELECT EXISTS (
|
WITH source_version AS (
|
||||||
SELECT 1
|
SELECT document_id, major FROM document_versions WHERE id = @document_version_id
|
||||||
|
),
|
||||||
|
major_versions AS (
|
||||||
|
SELECT dv.id FROM document_versions dv
|
||||||
|
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
|
||||||
|
),
|
||||||
|
signed_emails AS (
|
||||||
|
SELECT dvs.id, dvs.tenant_id
|
||||||
FROM document_version_signatures dvs
|
FROM document_version_signatures dvs
|
||||||
|
INNER JOIN major_versions mv ON dvs.document_version_id = mv.id
|
||||||
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
|
INNER JOIN iam_membership_profiles p ON dvs.signed_by_profile_id = p.id
|
||||||
INNER JOIN identities i ON p.identity_id = i.id
|
INNER JOIN identities i ON p.identity_id = i.id
|
||||||
WHERE dvs.document_version_id = @document_version_id
|
WHERE i.email_address = @user_email::CITEXT
|
||||||
AND i.email_address = @user_email::CITEXT
|
|
||||||
AND dvs.state = 'SIGNED'
|
AND dvs.state = 'SIGNED'
|
||||||
AND dvs.tenant_id = @tenant_id
|
)
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM signed_emails
|
||||||
|
WHERE %s
|
||||||
) AS signed
|
) AS signed
|
||||||
`
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"document_version_id": documentVersionID,
|
"document_version_id": documentVersionID,
|
||||||
"user_email": userEmail,
|
"user_email": userEmail,
|
||||||
@@ -458,13 +482,20 @@ func (dvs *DocumentVersionSignatures) CountByDocumentVersionID(
|
|||||||
filter *DocumentVersionSignatureFilter,
|
filter *DocumentVersionSignatureFilter,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
q := `
|
q := `
|
||||||
|
WITH source_version AS (
|
||||||
|
SELECT document_id, major FROM document_versions WHERE id = @document_version_id
|
||||||
|
),
|
||||||
|
major_versions AS (
|
||||||
|
SELECT dv.id FROM document_versions dv
|
||||||
|
INNER JOIN source_version sv ON dv.document_id = sv.document_id AND dv.major = sv.major
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(id)
|
COUNT(document_version_signatures.id)
|
||||||
FROM
|
FROM
|
||||||
document_version_signatures
|
document_version_signatures
|
||||||
|
INNER JOIN major_versions mv ON document_version_signatures.document_version_id = mv.id
|
||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND document_version_id = @document_version_id
|
|
||||||
AND %s
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
11
pkg/coredata/migrations/20260326T120000Z.sql
Normal file
11
pkg/coredata/migrations/20260326T120000Z.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE document_versions DROP CONSTRAINT document_versions_document_id_version_number_key;
|
||||||
|
|
||||||
|
ALTER TABLE document_versions RENAME COLUMN version_number TO major;
|
||||||
|
ALTER TABLE document_versions ADD COLUMN minor INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE document_versions ALTER COLUMN minor DROP DEFAULT;
|
||||||
|
|
||||||
|
ALTER TABLE document_versions ADD CONSTRAINT document_versions_document_id_major_minor_key UNIQUE (document_id, major, minor);
|
||||||
|
|
||||||
|
ALTER TABLE documents RENAME COLUMN current_published_version TO current_published_major;
|
||||||
|
ALTER TABLE documents ADD COLUMN current_published_minor INTEGER;
|
||||||
|
UPDATE documents SET current_published_minor = 0 WHERE current_published_major IS NOT NULL;
|
||||||
@@ -218,7 +218,8 @@ type (
|
|||||||
DocumentData struct {
|
DocumentData struct {
|
||||||
Title string
|
Title string
|
||||||
Content string
|
Content string
|
||||||
Version int
|
Major int
|
||||||
|
Minor int
|
||||||
Classification Classification
|
Classification Classification
|
||||||
Approvers []string
|
Approvers []string
|
||||||
Description string
|
Description string
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func TestRenderHTML(t *testing.T) {
|
|||||||
data: DocumentData{
|
data: DocumentData{
|
||||||
Title: "Test Document",
|
Title: "Test Document",
|
||||||
Content: "# Main Title\n\nThis is **bold** text with *italic* formatting.",
|
Content: "# Main Title\n\nThis is **bold** text with *italic* formatting.",
|
||||||
Version: 1,
|
Major: 1,
|
||||||
Classification: ClassificationPublic,
|
Classification: ClassificationPublic,
|
||||||
Approvers: []string{"John Doe"},
|
Approvers: []string{"John Doe"},
|
||||||
PublishedAt: &now,
|
PublishedAt: &now,
|
||||||
@@ -57,7 +57,7 @@ func TestRenderHTML(t *testing.T) {
|
|||||||
"<h1>Main Title</h1>",
|
"<h1>Main Title</h1>",
|
||||||
"<strong>bold</strong>",
|
"<strong>bold</strong>",
|
||||||
"<em>italic</em>",
|
"<em>italic</em>",
|
||||||
"<td>1</td>",
|
"<td>1.0</td>",
|
||||||
"PUBLIC",
|
"PUBLIC",
|
||||||
"John Doe",
|
"John Doe",
|
||||||
"Alice Smith",
|
"Alice Smith",
|
||||||
@@ -383,7 +383,7 @@ func BenchmarkGenerateHTML(b *testing.B) {
|
|||||||
data := DocumentData{
|
data := DocumentData{
|
||||||
Title: "Benchmark Document",
|
Title: "Benchmark Document",
|
||||||
Content: "# Title\n\nThis is **bold** text with *italic* formatting.\n\n- Item 1\n- Item 2",
|
Content: "# Title\n\nThis is **bold** text with *italic* formatting.\n\n- Item 1\n- Item 2",
|
||||||
Version: 1,
|
Major: 1,
|
||||||
Classification: ClassificationPublic,
|
Classification: ClassificationPublic,
|
||||||
Approvers: []string{"John Doe"},
|
Approvers: []string{"John Doe"},
|
||||||
PublishedAt: &now,
|
PublishedAt: &now,
|
||||||
|
|||||||
@@ -404,7 +404,7 @@
|
|||||||
{{- end}}
|
{{- end}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Version:</td>
|
<td>Version:</td>
|
||||||
<td>{{.Version}}</td>
|
<td>{{.Major}}.{{.Minor}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{- if .PublishedAt}}
|
{{- if .PublishedAt}}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -960,26 +960,13 @@ func (s *DocumentApprovalService) publishVersion(
|
|||||||
return fmt.Errorf("cannot load document version: %w", err)
|
return fmt.Errorf("cannot load document version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
document := &coredata.Document{}
|
_, _, err := s.svc.Documents.publishMajorVersionInTx(
|
||||||
if err := document.LoadByID(ctx, tx, s.svc.scope, version.DocumentID); err != nil {
|
ctx,
|
||||||
return fmt.Errorf("cannot load document: %w", err)
|
tx,
|
||||||
}
|
version.DocumentID,
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
now := time.Now()
|
return err
|
||||||
document.CurrentPublishedVersion = &version.VersionNumber
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
version.Status = coredata.DocumentVersionStatusPublished
|
|
||||||
version.PublishedAt = &now
|
|
||||||
version.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := version.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return fmt.Errorf("cannot update document version: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ type (
|
|||||||
|
|
||||||
BulkPublishVersionsRequest struct {
|
BulkPublishVersionsRequest struct {
|
||||||
DocumentIDs []gid.GID
|
DocumentIDs []gid.GID
|
||||||
PublishedBy gid.GID
|
|
||||||
Changelog string
|
Changelog string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -337,11 +336,11 @@ func (s DocumentService) GenerateChangelog(
|
|||||||
return fmt.Errorf("cannot load document: %w", err)
|
return fmt.Errorf("cannot load document: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if document.CurrentPublishedVersion == nil {
|
if document.CurrentPublishedMajor == nil {
|
||||||
initialVersionChangelog := "Initial version"
|
initialVersionChangelog := "Initial version"
|
||||||
changelog = &initialVersionChangelog
|
changelog = &initialVersionChangelog
|
||||||
} else {
|
} else {
|
||||||
if err := publishedVersion.LoadByDocumentIDAndVersionNumber(ctx, conn, s.svc.scope, documentID, *document.CurrentPublishedVersion); err != nil {
|
if err := publishedVersion.LoadByDocumentIDAndVersion(ctx, conn, s.svc.scope, documentID, *document.CurrentPublishedMajor, *document.CurrentPublishedMinor); err != nil {
|
||||||
return fmt.Errorf("cannot load published version: %w", err)
|
return fmt.Errorf("cannot load published version: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +368,7 @@ func (s DocumentService) GenerateChangelog(
|
|||||||
return changelog, nil
|
return changelog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DocumentService) BulkPublishVersions(
|
func (s *DocumentService) BulkPublishMajorVersions(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
req BulkPublishVersionsRequest,
|
req BulkPublishVersionsRequest,
|
||||||
) ([]*coredata.DocumentVersion, []*coredata.Document, error) {
|
) ([]*coredata.DocumentVersion, []*coredata.Document, error) {
|
||||||
@@ -380,7 +379,7 @@ func (s *DocumentService) BulkPublishVersions(
|
|||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
for _, documentID := range req.DocumentIDs {
|
for _, documentID := range req.DocumentIDs {
|
||||||
document, version, err := s.publishVersionInTx(ctx, tx, documentID, req.PublishedBy, &req.Changelog, true)
|
document, version, err := s.publishMajorVersionInTx(ctx, tx, documentID, &req.Changelog, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot publish document %q: %w", documentID, err)
|
return fmt.Errorf("cannot publish document %q: %w", documentID, err)
|
||||||
}
|
}
|
||||||
@@ -400,7 +399,38 @@ func (s *DocumentService) BulkPublishVersions(
|
|||||||
return publishedVersions, updatedDocuments, nil
|
return publishedVersions, updatedDocuments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DocumentService) PublishVersion(
|
func (s *DocumentService) BulkPublishMinorVersions(
|
||||||
|
ctx context.Context,
|
||||||
|
req BulkPublishVersionsRequest,
|
||||||
|
) ([]*coredata.DocumentVersion, []*coredata.Document, error) {
|
||||||
|
var publishedVersions []*coredata.DocumentVersion
|
||||||
|
var updatedDocuments []*coredata.Document
|
||||||
|
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
for _, documentID := range req.DocumentIDs {
|
||||||
|
document, version, err := s.publishMinorVersionInTx(ctx, tx, documentID, &req.Changelog, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot publish document %q: %w", documentID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishedVersions = append(publishedVersions, version)
|
||||||
|
updatedDocuments = append(updatedDocuments, document)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return publishedVersions, updatedDocuments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) PublishMajorVersion(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
documentID gid.GID,
|
documentID gid.GID,
|
||||||
publishedBy gid.GID,
|
publishedBy gid.GID,
|
||||||
@@ -414,9 +444,9 @@ func (s *DocumentService) PublishVersion(
|
|||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
document, documentVersion, err = s.publishVersionInTx(ctx, tx, documentID, publishedBy, changelog, false)
|
document, documentVersion, err = s.publishMajorVersionInTx(ctx, tx, documentID, changelog, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot publish version: %w", err)
|
return fmt.Errorf("cannot publish major version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -430,66 +460,31 @@ func (s *DocumentService) PublishVersion(
|
|||||||
return document, documentVersion, nil
|
return document, documentVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DocumentService) publishVersionInTx(
|
func (s *DocumentService) PublishMinorVersion(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx pg.Conn,
|
|
||||||
documentID gid.GID,
|
documentID gid.GID,
|
||||||
publishedBy gid.GID,
|
publishedBy gid.GID,
|
||||||
changelog *string,
|
changelog *string,
|
||||||
ignoreExisting bool,
|
|
||||||
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||||
document := &coredata.Document{}
|
var document *coredata.Document
|
||||||
documentVersion := &coredata.DocumentVersion{}
|
var documentVersion *coredata.DocumentVersion
|
||||||
publishedVersion := &coredata.DocumentVersion{}
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if err := document.LoadByID(ctx, tx, s.svc.scope, documentID); err != nil {
|
err := s.svc.pg.WithTx(
|
||||||
return nil, nil, fmt.Errorf("cannot load document %q: %w", documentID, err)
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
document, documentVersion, err = s.publishMinorVersionInTx(ctx, tx, documentID, changelog, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot publish minor version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if document.ArchivedAt != nil {
|
return nil
|
||||||
return nil, nil, &ErrDocumentArchived{}
|
},
|
||||||
}
|
)
|
||||||
|
|
||||||
if err := documentVersion.LoadLatestVersion(ctx, tx, s.svc.scope, documentID); err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("cannot load current draft: %w", err)
|
return nil, nil, err
|
||||||
}
|
|
||||||
|
|
||||||
if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished {
|
|
||||||
return document, documentVersion, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if documentVersion.Status != coredata.DocumentVersionStatusDraft {
|
|
||||||
return nil, nil, fmt.Errorf("cannot publish version")
|
|
||||||
}
|
|
||||||
|
|
||||||
if document.CurrentPublishedVersion != nil {
|
|
||||||
if err := publishedVersion.LoadByDocumentIDAndVersionNumber(ctx, tx, s.svc.scope, documentID, *document.CurrentPublishedVersion); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot load published version: %w", err)
|
|
||||||
}
|
|
||||||
if publishedVersion.Content == documentVersion.Content &&
|
|
||||||
publishedVersion.Title == documentVersion.Title {
|
|
||||||
return nil, nil, &ErrDocumentVersionNoChanges{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if changelog != nil {
|
|
||||||
documentVersion.Changelog = *changelog
|
|
||||||
}
|
|
||||||
|
|
||||||
document.CurrentPublishedVersion = &documentVersion.VersionNumber
|
|
||||||
document.UpdatedAt = now
|
|
||||||
|
|
||||||
documentVersion.Status = coredata.DocumentVersionStatusPublished
|
|
||||||
documentVersion.PublishedAt = &now
|
|
||||||
documentVersion.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot update document: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := documentVersion.Update(ctx, tx, s.svc.scope); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot update document version: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return document, documentVersion, nil
|
return document, documentVersion, nil
|
||||||
@@ -528,7 +523,8 @@ func (s *DocumentService) Create(
|
|||||||
ID: documentVersionID,
|
ID: documentVersionID,
|
||||||
DocumentID: documentID,
|
DocumentID: documentID,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
VersionNumber: 1,
|
Major: 0,
|
||||||
|
Minor: 1,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
Status: coredata.DocumentVersionStatusDraft,
|
Status: coredata.DocumentVersionStatusDraft,
|
||||||
Classification: req.Classification,
|
Classification: req.Classification,
|
||||||
@@ -1004,7 +1000,8 @@ func (s *DocumentService) CreateDraft(
|
|||||||
draftVersion.OrganizationID = document.OrganizationID
|
draftVersion.OrganizationID = document.OrganizationID
|
||||||
draftVersion.DocumentID = documentID
|
draftVersion.DocumentID = documentID
|
||||||
draftVersion.Title = document.Title
|
draftVersion.Title = document.Title
|
||||||
draftVersion.VersionNumber = latestVersion.VersionNumber + 1
|
draftVersion.Major = latestVersion.Major
|
||||||
|
draftVersion.Minor = latestVersion.Minor + 1
|
||||||
draftVersion.Classification = document.Classification
|
draftVersion.Classification = document.Classification
|
||||||
draftVersion.Content = latestVersion.Content
|
draftVersion.Content = latestVersion.Content
|
||||||
draftVersion.Status = coredata.DocumentVersionStatusDraft
|
draftVersion.Status = coredata.DocumentVersionStatusDraft
|
||||||
@@ -1043,7 +1040,7 @@ func (s *DocumentService) DeleteDraft(
|
|||||||
return fmt.Errorf("cannot delete published document version")
|
return fmt.Errorf("cannot delete published document version")
|
||||||
}
|
}
|
||||||
|
|
||||||
if documentVersion.VersionNumber == 1 {
|
if documentVersion.Major == 0 && documentVersion.Minor == 1 {
|
||||||
return fmt.Errorf("cannot delete the first version of a document")
|
return fmt.Errorf("cannot delete the first version of a document")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1961,7 +1958,8 @@ func exportDocumentPDF(
|
|||||||
docData := docgen.DocumentData{
|
docData := docgen.DocumentData{
|
||||||
Title: version.Title,
|
Title: version.Title,
|
||||||
Content: version.Content,
|
Content: version.Content,
|
||||||
Version: version.VersionNumber,
|
Major: version.Major,
|
||||||
|
Minor: version.Minor,
|
||||||
Classification: classification,
|
Classification: classification,
|
||||||
Approvers: approverNames,
|
Approvers: approverNames,
|
||||||
PublishedAt: version.PublishedAt,
|
PublishedAt: version.PublishedAt,
|
||||||
@@ -2168,3 +2166,133 @@ func sanitizeFilename(title string) string {
|
|||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) loadDraftForPublish(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Conn,
|
||||||
|
documentID gid.GID,
|
||||||
|
ignoreExisting bool,
|
||||||
|
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||||
|
document := &coredata.Document{}
|
||||||
|
documentVersion := &coredata.DocumentVersion{}
|
||||||
|
|
||||||
|
if err := document.LoadByID(ctx, tx, s.svc.scope, documentID); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot load document %q: %w", documentID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if document.ArchivedAt != nil {
|
||||||
|
return nil, nil, &ErrDocumentArchived{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := documentVersion.LoadLatestVersion(ctx, tx, s.svc.scope, documentID); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot load current draft: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished {
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if documentVersion.Status != coredata.DocumentVersionStatusDraft {
|
||||||
|
return nil, nil, &ErrDocumentVersionNotDraft{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) finalizePublish(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Conn,
|
||||||
|
document *coredata.Document,
|
||||||
|
documentVersion *coredata.DocumentVersion,
|
||||||
|
changelog *string,
|
||||||
|
) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if changelog != nil {
|
||||||
|
documentVersion.Changelog = *changelog
|
||||||
|
}
|
||||||
|
|
||||||
|
document.UpdatedAt = now
|
||||||
|
documentVersion.Status = coredata.DocumentVersionStatusPublished
|
||||||
|
documentVersion.PublishedAt = &now
|
||||||
|
documentVersion.UpdatedAt = now
|
||||||
|
|
||||||
|
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update document: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := documentVersion.Update(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update document version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) publishMajorVersionInTx(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Conn,
|
||||||
|
documentID gid.GID,
|
||||||
|
changelog *string,
|
||||||
|
ignoreExisting bool,
|
||||||
|
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||||
|
document, documentVersion, err := s.loadDraftForPublish(ctx, tx, documentID, ignoreExisting)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished {
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if document.CurrentPublishedMajor != nil {
|
||||||
|
publishedVersion := &coredata.DocumentVersion{}
|
||||||
|
if err := publishedVersion.LoadByDocumentIDAndVersion(ctx, tx, s.svc.scope, documentID, *document.CurrentPublishedMajor, *document.CurrentPublishedMinor); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot load published version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if publishedVersion.Content == documentVersion.Content &&
|
||||||
|
publishedVersion.Title == documentVersion.Title {
|
||||||
|
return nil, nil, &ErrDocumentVersionNoChanges{}
|
||||||
|
}
|
||||||
|
|
||||||
|
documentVersion.Major = *document.CurrentPublishedMajor + 1
|
||||||
|
} else {
|
||||||
|
documentVersion.Major = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
documentVersion.Minor = 0
|
||||||
|
document.CurrentPublishedMajor = &documentVersion.Major
|
||||||
|
document.CurrentPublishedMinor = &documentVersion.Minor
|
||||||
|
|
||||||
|
if err := s.finalizePublish(ctx, tx, document, documentVersion, changelog); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DocumentService) publishMinorVersionInTx(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Conn,
|
||||||
|
documentID gid.GID,
|
||||||
|
changelog *string,
|
||||||
|
ignoreExisting bool,
|
||||||
|
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||||
|
document, documentVersion, err := s.loadDraftForPublish(ctx, tx, documentID, ignoreExisting)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ignoreExisting && documentVersion.Status == coredata.DocumentVersionStatusPublished {
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
document.CurrentPublishedMajor = &documentVersion.Major
|
||||||
|
document.CurrentPublishedMinor = &documentVersion.Minor
|
||||||
|
|
||||||
|
if err := s.finalizePublish(ctx, tx, document, documentVersion, changelog); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return document, documentVersion, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2441,7 +2441,8 @@ type Document implements Node {
|
|||||||
description: String
|
description: String
|
||||||
documentType: DocumentType!
|
documentType: DocumentType!
|
||||||
classification: DocumentClassification!
|
classification: DocumentClassification!
|
||||||
currentPublishedVersion: Int
|
currentPublishedMajor: Int
|
||||||
|
currentPublishedMinor: Int
|
||||||
trustCenterVisibility: TrustCenterVisibility!
|
trustCenterVisibility: TrustCenterVisibility!
|
||||||
organization: Organization! @goField(forceResolver: true)
|
organization: Organization! @goField(forceResolver: true)
|
||||||
|
|
||||||
@@ -2501,7 +2502,8 @@ type EmployeeDocumentVersion
|
|||||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.EmployeeDocumentVersion"
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.EmployeeDocumentVersion"
|
||||||
) {
|
) {
|
||||||
id: ID!
|
id: ID!
|
||||||
version: Int!
|
major: Int!
|
||||||
|
minor: Int!
|
||||||
status: DocumentVersionStatus!
|
status: DocumentVersionStatus!
|
||||||
signed: Boolean! @goField(forceResolver: true)
|
signed: Boolean! @goField(forceResolver: true)
|
||||||
approvalDecision: DocumentVersionApprovalDecision @goField(forceResolver: true)
|
approvalDecision: DocumentVersionApprovalDecision @goField(forceResolver: true)
|
||||||
@@ -3792,10 +3794,16 @@ type Mutation {
|
|||||||
exportStateOfApplicabilityPDF(
|
exportStateOfApplicabilityPDF(
|
||||||
input: ExportStateOfApplicabilityPDFInput!
|
input: ExportStateOfApplicabilityPDFInput!
|
||||||
): ExportStateOfApplicabilityPDFPayload!
|
): ExportStateOfApplicabilityPDFPayload!
|
||||||
publishDocumentVersion(
|
publishMajorDocumentVersion(
|
||||||
input: PublishDocumentVersionInput!
|
input: PublishMajorDocumentVersionInput!
|
||||||
): PublishDocumentVersionPayload!
|
): PublishDocumentVersionPayload!
|
||||||
bulkPublishDocumentVersions(
|
publishMinorDocumentVersion(
|
||||||
|
input: PublishMinorDocumentVersionInput!
|
||||||
|
): PublishDocumentVersionPayload!
|
||||||
|
bulkPublishMajorDocumentVersions(
|
||||||
|
input: BulkPublishDocumentVersionsInput!
|
||||||
|
): BulkPublishDocumentVersionsPayload!
|
||||||
|
bulkPublishMinorDocumentVersions(
|
||||||
input: BulkPublishDocumentVersionsInput!
|
input: BulkPublishDocumentVersionsInput!
|
||||||
): BulkPublishDocumentVersionsPayload!
|
): BulkPublishDocumentVersionsPayload!
|
||||||
requestDocumentVersionApproval(
|
requestDocumentVersionApproval(
|
||||||
@@ -5355,7 +5363,8 @@ type DocumentVersion implements Node {
|
|||||||
id: ID!
|
id: ID!
|
||||||
document: Document! @goField(forceResolver: true)
|
document: Document! @goField(forceResolver: true)
|
||||||
status: DocumentVersionStatus!
|
status: DocumentVersionStatus!
|
||||||
version: Int!
|
major: Int!
|
||||||
|
minor: Int!
|
||||||
content: String!
|
content: String!
|
||||||
changelog: String!
|
changelog: String!
|
||||||
title: String!
|
title: String!
|
||||||
@@ -5686,7 +5695,12 @@ type RequestDocumentVersionApprovalPayload {
|
|||||||
approvalQuorum: DocumentVersionApprovalQuorum!
|
approvalQuorum: DocumentVersionApprovalQuorum!
|
||||||
}
|
}
|
||||||
|
|
||||||
input PublishDocumentVersionInput {
|
input PublishMajorDocumentVersionInput {
|
||||||
|
documentId: ID!
|
||||||
|
changelog: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input PublishMinorDocumentVersionInput {
|
||||||
documentId: ID!
|
documentId: ID!
|
||||||
changelog: String
|
changelog: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ func NewDocument(document *coredata.Document) *Document {
|
|||||||
},
|
},
|
||||||
DocumentType: document.DocumentType,
|
DocumentType: document.DocumentType,
|
||||||
Classification: document.Classification,
|
Classification: document.Classification,
|
||||||
CurrentPublishedVersion: document.CurrentPublishedVersion,
|
CurrentPublishedMajor: document.CurrentPublishedMajor,
|
||||||
|
CurrentPublishedMinor: document.CurrentPublishedMinor,
|
||||||
TrustCenterVisibility: document.TrustCenterVisibility,
|
TrustCenterVisibility: document.TrustCenterVisibility,
|
||||||
Status: document.Status,
|
Status: document.Status,
|
||||||
ArchivedAt: document.ArchivedAt,
|
ArchivedAt: document.ArchivedAt,
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ func NewDocumentVersion(documentVersion *coredata.DocumentVersion) *DocumentVers
|
|||||||
Document: &Document{
|
Document: &Document{
|
||||||
ID: documentVersion.DocumentID,
|
ID: documentVersion.DocumentID,
|
||||||
},
|
},
|
||||||
Version: documentVersion.VersionNumber,
|
Major: documentVersion.Major,
|
||||||
|
Minor: documentVersion.Minor,
|
||||||
Title: documentVersion.Title,
|
Title: documentVersion.Title,
|
||||||
Content: documentVersion.Content,
|
Content: documentVersion.Content,
|
||||||
Status: documentVersion.Status,
|
Status: documentVersion.Status,
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ type (
|
|||||||
EmployeeDocumentVersion struct {
|
EmployeeDocumentVersion struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
OrganizationID gid.GID
|
OrganizationID gid.GID
|
||||||
Version int
|
Major int
|
||||||
|
Minor int
|
||||||
Status coredata.DocumentVersionStatus
|
Status coredata.DocumentVersionStatus
|
||||||
PublishedAt *time.Time
|
PublishedAt *time.Time
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
|||||||
@@ -1647,7 +1647,8 @@ func (r *employeeDocumentResolver) Versions(ctx context.Context, obj *types.Empl
|
|||||||
employeeVersions[i] = &types.EmployeeDocumentVersion{
|
employeeVersions[i] = &types.EmployeeDocumentVersion{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
OrganizationID: v.OrganizationID,
|
OrganizationID: v.OrganizationID,
|
||||||
Version: v.VersionNumber,
|
Major: v.Major,
|
||||||
|
Minor: v.Minor,
|
||||||
Status: v.Status,
|
Status: v.Status,
|
||||||
PublishedAt: v.PublishedAt,
|
PublishedAt: v.PublishedAt,
|
||||||
CreatedAt: v.CreatedAt,
|
CreatedAt: v.CreatedAt,
|
||||||
@@ -5012,16 +5013,20 @@ func (r *mutationResolver) ExportStateOfApplicabilityPDF(ctx context.Context, in
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishDocumentVersion is the resolver for the publishDocumentVersion field.
|
// PublishMajorDocumentVersion is the resolver for the publishMajorDocumentVersion field.
|
||||||
func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input types.PublishDocumentVersionInput) (*types.PublishDocumentVersionPayload, error) {
|
func (r *mutationResolver) PublishMajorDocumentVersion(ctx context.Context, input types.PublishMajorDocumentVersionInput) (*types.PublishDocumentVersionPayload, error) {
|
||||||
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish); err != nil {
|
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
identity := authn.IdentityFromContext(ctx)
|
|
||||||
prb := r.ProboService(ctx, input.DocumentID.TenantID())
|
prb := r.ProboService(ctx, input.DocumentID.TenantID())
|
||||||
|
|
||||||
document, documentVersion, err := prb.Documents.PublishVersion(ctx, input.DocumentID, identity.ID, input.Changelog)
|
document, documentVersion, err := prb.Documents.PublishMajorVersion(
|
||||||
|
ctx,
|
||||||
|
input.DocumentID,
|
||||||
|
authn.IdentityFromContext(ctx).ID,
|
||||||
|
input.Changelog,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
|
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
|
||||||
return nil, gqlutils.Conflict(ctx, errArchived)
|
return nil, gqlutils.Conflict(ctx, errArchived)
|
||||||
@@ -5031,7 +5036,11 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
|
|||||||
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot publish document version", log.Error(err))
|
if errNoChanges, ok := errors.AsType[*probo.ErrDocumentVersionNoChanges](err); ok {
|
||||||
|
return nil, gqlutils.Invalid(ctx, errNoChanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot publish major document version", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5041,8 +5050,41 @@ func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input typ
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BulkPublishDocumentVersions is the resolver for the bulkPublishDocumentVersions field.
|
// PublishMinorDocumentVersion is the resolver for the publishMinorDocumentVersion field.
|
||||||
func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error) {
|
func (r *mutationResolver) PublishMinorDocumentVersion(ctx context.Context, input types.PublishMinorDocumentVersionInput) (*types.PublishDocumentVersionPayload, error) {
|
||||||
|
if err := r.authorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.DocumentID.TenantID())
|
||||||
|
|
||||||
|
document, documentVersion, err := prb.Documents.PublishMinorVersion(
|
||||||
|
ctx,
|
||||||
|
input.DocumentID,
|
||||||
|
authn.IdentityFromContext(ctx).ID,
|
||||||
|
input.Changelog,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
|
||||||
|
return nil, gqlutils.Conflict(ctx, errArchived)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errNotDraft, ok := errors.AsType[*probo.ErrDocumentVersionNotDraft](err); ok {
|
||||||
|
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot publish minor document version", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.PublishDocumentVersionPayload{
|
||||||
|
Document: types.NewDocument(document),
|
||||||
|
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkPublishMajorDocumentVersions is the resolver for the bulkPublishMajorDocumentVersions field.
|
||||||
|
func (r *mutationResolver) BulkPublishMajorDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error) {
|
||||||
if len(input.DocumentIds) == 0 {
|
if len(input.DocumentIds) == 0 {
|
||||||
return &types.BulkPublishDocumentVersionsPayload{
|
return &types.BulkPublishDocumentVersionsPayload{
|
||||||
DocumentVersions: []*types.DocumentVersion{},
|
DocumentVersions: []*types.DocumentVersion{},
|
||||||
@@ -5056,12 +5098,10 @@ func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, inpu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identity := authn.IdentityFromContext(ctx)
|
|
||||||
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
|
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
|
||||||
|
|
||||||
versions, documents, err := prb.Documents.BulkPublishVersions(ctx, probo.BulkPublishVersionsRequest{
|
versions, documents, err := prb.Documents.BulkPublishMajorVersions(ctx, probo.BulkPublishVersionsRequest{
|
||||||
DocumentIDs: input.DocumentIds,
|
DocumentIDs: input.DocumentIds,
|
||||||
PublishedBy: identity.ID,
|
|
||||||
Changelog: input.Changelog,
|
Changelog: input.Changelog,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -5073,7 +5113,57 @@ func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, inpu
|
|||||||
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.logger.ErrorCtx(ctx, "cannot bulk publish document versions", log.Error(err))
|
r.logger.ErrorCtx(ctx, "cannot bulk publish major document versions", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
typesVersions := make([]*types.DocumentVersion, len(versions))
|
||||||
|
for i, v := range versions {
|
||||||
|
typesVersions[i] = types.NewDocumentVersion(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
typesDocuments := make([]*types.Document, len(documents))
|
||||||
|
for i, d := range documents {
|
||||||
|
typesDocuments[i] = types.NewDocument(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.BulkPublishDocumentVersionsPayload{
|
||||||
|
DocumentVersions: typesVersions,
|
||||||
|
Documents: typesDocuments,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkPublishMinorDocumentVersions is the resolver for the bulkPublishMinorDocumentVersions field.
|
||||||
|
func (r *mutationResolver) BulkPublishMinorDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error) {
|
||||||
|
if len(input.DocumentIds) == 0 {
|
||||||
|
return &types.BulkPublishDocumentVersionsPayload{
|
||||||
|
DocumentVersions: []*types.DocumentVersion{},
|
||||||
|
Documents: []*types.Document{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, documentID := range input.DocumentIds {
|
||||||
|
if err := r.authorize(ctx, documentID, probo.ActionDocumentVersionPublish); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
|
||||||
|
|
||||||
|
versions, documents, err := prb.Documents.BulkPublishMinorVersions(ctx, probo.BulkPublishVersionsRequest{
|
||||||
|
DocumentIDs: input.DocumentIds,
|
||||||
|
Changelog: input.Changelog,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errArchived, ok := errors.AsType[*probo.ErrDocumentArchived](err); ok {
|
||||||
|
return nil, gqlutils.Conflict(ctx, errArchived)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errNotDraft, ok := errors.AsType[*probo.ErrDocumentVersionNotDraft](err); ok {
|
||||||
|
return nil, gqlutils.Invalid(ctx, errNotDraft)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot bulk publish minor document versions", log.Error(err))
|
||||||
return nil, gqlutils.Internal(ctx)
|
return nil, gqlutils.Internal(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2196,24 +2196,6 @@ func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallT
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) PublishDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishDocumentVersionInput) (*mcp.CallToolResult, types.PublishDocumentVersionOutput, error) {
|
|
||||||
r.MustAuthorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish)
|
|
||||||
|
|
||||||
svc := r.ProboService(ctx, input.DocumentID)
|
|
||||||
|
|
||||||
user := authn.IdentityFromContext(ctx)
|
|
||||||
|
|
||||||
document, documentVersion, err := svc.Documents.PublishVersion(ctx, input.DocumentID, user.ID, input.Changelog)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("cannot publish document version: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, types.PublishDocumentVersionOutput{
|
|
||||||
Document: types.NewDocument(document),
|
|
||||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentVersionSignaturesInput) (*mcp.CallToolResult, types.ListDocumentVersionSignaturesOutput, error) {
|
func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentVersionSignaturesInput) (*mcp.CallToolResult, types.ListDocumentVersionSignaturesOutput, error) {
|
||||||
r.MustAuthorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionSignatureList)
|
r.MustAuthorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionSignatureList)
|
||||||
|
|
||||||
@@ -3324,3 +3306,47 @@ func (r *Resolver) RequestDocumentVersionApprovalTool(ctx context.Context, req *
|
|||||||
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) PublishMajorDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishMajorDocumentVersionInput) (*mcp.CallToolResult, types.PublishMajorDocumentVersionOutput, error) {
|
||||||
|
r.MustAuthorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish)
|
||||||
|
|
||||||
|
svc := r.ProboService(ctx, input.DocumentID)
|
||||||
|
user := authn.IdentityFromContext(ctx)
|
||||||
|
|
||||||
|
document, documentVersion, err := svc.Documents.PublishMajorVersion(
|
||||||
|
ctx,
|
||||||
|
input.DocumentID,
|
||||||
|
user.ID,
|
||||||
|
input.Changelog,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("cannot publish major document version: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.PublishMajorDocumentVersionOutput{
|
||||||
|
Document: types.NewDocument(document),
|
||||||
|
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) PublishMinorDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishMinorDocumentVersionInput) (*mcp.CallToolResult, types.PublishMinorDocumentVersionOutput, error) {
|
||||||
|
r.MustAuthorize(ctx, input.DocumentID, probo.ActionDocumentVersionPublish)
|
||||||
|
|
||||||
|
svc := r.ProboService(ctx, input.DocumentID)
|
||||||
|
user := authn.IdentityFromContext(ctx)
|
||||||
|
|
||||||
|
document, documentVersion, err := svc.Documents.PublishMinorVersion(
|
||||||
|
ctx,
|
||||||
|
input.DocumentID,
|
||||||
|
user.ID,
|
||||||
|
input.Changelog,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("cannot publish minor document version: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.PublishMinorDocumentVersionOutput{
|
||||||
|
Document: types.NewDocument(document),
|
||||||
|
DocumentVersion: types.NewDocumentVersion(documentVersion),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5207,11 +5207,16 @@ components:
|
|||||||
classification:
|
classification:
|
||||||
$ref: "#/components/schemas/DocumentClassification"
|
$ref: "#/components/schemas/DocumentClassification"
|
||||||
description: Document classification
|
description: Document classification
|
||||||
current_published_version:
|
current_published_major:
|
||||||
type:
|
type:
|
||||||
- integer
|
- integer
|
||||||
- "null"
|
- "null"
|
||||||
description: Current published version number
|
description: Current published major version number
|
||||||
|
current_published_minor:
|
||||||
|
type:
|
||||||
|
- integer
|
||||||
|
- "null"
|
||||||
|
description: Current published minor version number
|
||||||
trust_center_visibility:
|
trust_center_visibility:
|
||||||
$ref: "#/components/schemas/TrustCenterVisibility"
|
$ref: "#/components/schemas/TrustCenterVisibility"
|
||||||
description: Trust center visibility
|
description: Trust center visibility
|
||||||
@@ -5240,7 +5245,8 @@ components:
|
|||||||
- organization_id
|
- organization_id
|
||||||
- document_id
|
- document_id
|
||||||
- title
|
- title
|
||||||
- version_number
|
- major
|
||||||
|
- minor
|
||||||
- classification
|
- classification
|
||||||
- content
|
- content
|
||||||
- changelog
|
- changelog
|
||||||
@@ -5260,9 +5266,12 @@ components:
|
|||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
description: Document version title
|
description: Document version title
|
||||||
version_number:
|
major:
|
||||||
type: integer
|
type: integer
|
||||||
description: Version number
|
description: Major version number
|
||||||
|
minor:
|
||||||
|
type: integer
|
||||||
|
description: Minor version number
|
||||||
classification:
|
classification:
|
||||||
$ref: "#/components/schemas/DocumentClassification"
|
$ref: "#/components/schemas/DocumentClassification"
|
||||||
description: Document classification
|
description: Document classification
|
||||||
@@ -5606,7 +5615,19 @@ components:
|
|||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Deleted document version ID
|
description: Deleted document version ID
|
||||||
|
|
||||||
PublishDocumentVersionInput:
|
PublishMajorDocumentVersionInput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- document_id
|
||||||
|
properties:
|
||||||
|
document_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Document ID
|
||||||
|
changelog:
|
||||||
|
type: string
|
||||||
|
description: Changelog for this version
|
||||||
|
|
||||||
|
PublishMinorDocumentVersionInput:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- document_id
|
- document_id
|
||||||
@@ -7471,12 +7492,20 @@ tools:
|
|||||||
$ref: "#/components/schemas/DeleteDraftDocumentVersionInput"
|
$ref: "#/components/schemas/DeleteDraftDocumentVersionInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/DeleteDraftDocumentVersionOutput"
|
$ref: "#/components/schemas/DeleteDraftDocumentVersionOutput"
|
||||||
- name: publishDocumentVersion
|
- name: publishMajorDocumentVersion
|
||||||
description: Publish a draft document version
|
description: Publish a draft document version as a new major version
|
||||||
hints:
|
hints:
|
||||||
readonly: false
|
readonly: false
|
||||||
inputSchema:
|
inputSchema:
|
||||||
$ref: "#/components/schemas/PublishDocumentVersionInput"
|
$ref: "#/components/schemas/PublishMajorDocumentVersionInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/PublishDocumentVersionOutput"
|
||||||
|
- name: publishMinorDocumentVersion
|
||||||
|
description: Publish a draft document version as a minor version
|
||||||
|
hints:
|
||||||
|
readonly: false
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/PublishMinorDocumentVersionInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/PublishDocumentVersionOutput"
|
$ref: "#/components/schemas/PublishDocumentVersionOutput"
|
||||||
- name: requestDocumentVersionApproval
|
- name: requestDocumentVersionApproval
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ func NewDocument(d *coredata.Document) *Document {
|
|||||||
Title: d.Title,
|
Title: d.Title,
|
||||||
DocumentType: d.DocumentType,
|
DocumentType: d.DocumentType,
|
||||||
Classification: d.Classification,
|
Classification: d.Classification,
|
||||||
CurrentPublishedVersion: d.CurrentPublishedVersion,
|
CurrentPublishedMajor: d.CurrentPublishedMajor,
|
||||||
|
CurrentPublishedMinor: d.CurrentPublishedMinor,
|
||||||
TrustCenterVisibility: d.TrustCenterVisibility,
|
TrustCenterVisibility: d.TrustCenterVisibility,
|
||||||
Status: d.Status,
|
Status: d.Status,
|
||||||
ArchivedAt: d.ArchivedAt,
|
ArchivedAt: d.ArchivedAt,
|
||||||
@@ -84,7 +85,8 @@ func NewDocumentVersion(dv *coredata.DocumentVersion) *DocumentVersion {
|
|||||||
OrganizationID: dv.OrganizationID,
|
OrganizationID: dv.OrganizationID,
|
||||||
DocumentID: dv.DocumentID,
|
DocumentID: dv.DocumentID,
|
||||||
Title: dv.Title,
|
Title: dv.Title,
|
||||||
VersionNumber: dv.VersionNumber,
|
Major: dv.Major,
|
||||||
|
Minor: dv.Minor,
|
||||||
Classification: dv.Classification,
|
Classification: dv.Classification,
|
||||||
Content: dv.Content,
|
Content: dv.Content,
|
||||||
Changelog: dv.Changelog,
|
Changelog: dv.Changelog,
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ func (s *DocumentService) exportPDFData(
|
|||||||
docData := docgen.DocumentData{
|
docData := docgen.DocumentData{
|
||||||
Title: version.Title,
|
Title: version.Title,
|
||||||
Content: version.Content,
|
Content: version.Content,
|
||||||
Version: version.VersionNumber,
|
Major: version.Major,
|
||||||
|
Minor: version.Minor,
|
||||||
Classification: classification,
|
Classification: classification,
|
||||||
Approvers: approverNames,
|
Approvers: approverNames,
|
||||||
PublishedAt: version.PublishedAt,
|
PublishedAt: version.PublishedAt,
|
||||||
|
|||||||
Reference in New Issue
Block a user