Consolidate document draft management into updateDocument
Replace the three separate draft mutations (createDraftDocumentVersion, updateDocumentVersion, deleteDraftDocumentVersion) with automatic draft lifecycle management inside updateDocument. The backend now auto-creates a draft when a published document is edited, updates the existing draft on subsequent edits, and auto-deletes the draft when content reverts to match the published version. A new deleteDocumentDraft mutation provides explicit draft deletion. Backend: - Merge version-level fields (content, title, classification, documentType) into UpdateDocumentRequest - Convert CreateDraft, UpdateVersion, DeleteDraft into private transaction helpers called from Update - Update returns (*Document, *DocumentVersion, error) with the version present only when a draft exists Frontend: - Remove all create/update/delete draft mutations from components - Auto-save via updateDocument with layout refetch on draft status transitions while preserving editor cursor (data-generation key) - Title, type, and classification editable on published versions (backend auto-creates draft) - Forms use react-hook-form values option to stay synced with Relay fragment data across draft/publish transitions API surface (GraphQL, MCP, CLI, n8n) updated consistently: - Removed: createDraftDocumentVersion, updateDocumentVersion, deleteDraftDocumentVersion - Added: deleteDocumentDraft (document-level) - Updated: updateDocument accepts content, classification, documentType Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -1,111 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package createdraft
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const createDraftMutation = `
|
||||
mutation($input: CreateDraftDocumentVersionInput!) {
|
||||
createDraftDocumentVersion(input: $input) {
|
||||
documentVersionEdge {
|
||||
node {
|
||||
id
|
||||
title
|
||||
major
|
||||
minor
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type createDraftResponse struct {
|
||||
CreateDraftDocumentVersion struct {
|
||||
DocumentVersionEdge struct {
|
||||
Node struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Status string `json:"status"`
|
||||
} `json:"node"`
|
||||
} `json:"documentVersionEdge"`
|
||||
} `json:"createDraftDocumentVersion"`
|
||||
}
|
||||
|
||||
func NewCmdCreateDraft(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create-draft <document-id>",
|
||||
Short: "Create a new draft version of a document",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
)
|
||||
|
||||
data, err := client.Do(
|
||||
createDraftMutation,
|
||||
map[string]any{
|
||||
"input": map[string]any{
|
||||
"documentID": args[0],
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp createDraftResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse response: %w", err)
|
||||
}
|
||||
|
||||
v := resp.CreateDraftDocumentVersion.DocumentVersionEdge.Node
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Created draft version %s (%s v%d.%d)\n",
|
||||
v.ID,
|
||||
v.Title,
|
||||
v.Major,
|
||||
v.Minor,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -24,9 +24,11 @@ import (
|
||||
)
|
||||
|
||||
const deleteDraftMutation = `
|
||||
mutation($input: DeleteDraftDocumentVersionInput!) {
|
||||
deleteDraftDocumentVersion(input: $input) {
|
||||
deletedDocumentVersionId
|
||||
mutation($input: DeleteDocumentDraftInput!) {
|
||||
deleteDocumentDraft(input: $input) {
|
||||
document {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -35,8 +37,8 @@ func NewCmdDeleteDraft(f *cmdutil.Factory) *cobra.Command {
|
||||
var flagYes bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete-draft <document-version-id>",
|
||||
Short: "Delete a draft document version",
|
||||
Use: "delete-draft <document-id>",
|
||||
Short: "Delete the draft version of a document",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !flagYes {
|
||||
@@ -46,7 +48,7 @@ func NewCmdDeleteDraft(f *cmdutil.Factory) *cobra.Command {
|
||||
|
||||
var confirmed bool
|
||||
err := huh.NewConfirm().
|
||||
Title(fmt.Sprintf("Delete draft version %s?", args[0])).
|
||||
Title(fmt.Sprintf("Delete draft for document %s?", args[0])).
|
||||
Value(&confirmed).
|
||||
Run()
|
||||
if err != nil {
|
||||
@@ -78,7 +80,7 @@ func NewCmdDeleteDraft(f *cmdutil.Factory) *cobra.Command {
|
||||
deleteDraftMutation,
|
||||
map[string]any{
|
||||
"input": map[string]any{
|
||||
"documentVersionId": args[0],
|
||||
"documentId": args[0],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -88,7 +90,7 @@ func NewCmdDeleteDraft(f *cmdutil.Factory) *cobra.Command {
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Deleted draft version %s\n",
|
||||
"Deleted draft for document %s\n",
|
||||
args[0],
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
"go.probo.inc/probo/pkg/cmd/document/archive"
|
||||
"go.probo.inc/probo/pkg/cmd/document/create"
|
||||
createdraft "go.probo.inc/probo/pkg/cmd/document/create-draft"
|
||||
"go.probo.inc/probo/pkg/cmd/document/delete"
|
||||
deletedraft "go.probo.inc/probo/pkg/cmd/document/delete-draft"
|
||||
"go.probo.inc/probo/pkg/cmd/document/list"
|
||||
@@ -28,7 +27,6 @@ import (
|
||||
publishminor "go.probo.inc/probo/pkg/cmd/document/publish-minor"
|
||||
"go.probo.inc/probo/pkg/cmd/document/unarchive"
|
||||
"go.probo.inc/probo/pkg/cmd/document/update"
|
||||
updateversion "go.probo.inc/probo/pkg/cmd/document/update-version"
|
||||
"go.probo.inc/probo/pkg/cmd/document/view"
|
||||
viewversion "go.probo.inc/probo/pkg/cmd/document/view-version"
|
||||
)
|
||||
@@ -48,9 +46,7 @@ func NewCmdDocument(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.AddCommand(unarchive.NewCmdUnarchive(f))
|
||||
cmd.AddCommand(listversions.NewCmdListVersions(f))
|
||||
cmd.AddCommand(viewversion.NewCmdViewVersion(f))
|
||||
cmd.AddCommand(createdraft.NewCmdCreateDraft(f))
|
||||
cmd.AddCommand(deletedraft.NewCmdDeleteDraft(f))
|
||||
cmd.AddCommand(updateversion.NewCmdUpdateVersion(f))
|
||||
cmd.AddCommand(publishmajor.NewCmdPublishMajor(f))
|
||||
cmd.AddCommand(publishminor.NewCmdPublishMinor(f))
|
||||
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package updateversion
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const updateMutation = `
|
||||
mutation($input: UpdateDocumentVersionInput!) {
|
||||
updateDocumentVersion(input: $input) {
|
||||
documentVersion {
|
||||
id
|
||||
title
|
||||
major
|
||||
minor
|
||||
status
|
||||
documentType
|
||||
classification
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type updateResponse struct {
|
||||
UpdateDocumentVersion struct {
|
||||
DocumentVersion struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Status string `json:"status"`
|
||||
DocumentType string `json:"documentType"`
|
||||
Classification string `json:"classification"`
|
||||
} `json:"documentVersion"`
|
||||
} `json:"updateDocumentVersion"`
|
||||
}
|
||||
|
||||
func NewCmdUpdateVersion(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagTitle string
|
||||
flagContent string
|
||||
flagDocumentType string
|
||||
flagClassification string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update-version <document-version-id>",
|
||||
Short: "Update a document version",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
)
|
||||
|
||||
input := map[string]any{
|
||||
"documentVersionId": args[0],
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("title") {
|
||||
input["title"] = flagTitle
|
||||
}
|
||||
if cmd.Flags().Changed("content") {
|
||||
input["content"] = flagContent
|
||||
}
|
||||
if cmd.Flags().Changed("document-type") {
|
||||
if err := cmdutil.ValidateEnum(
|
||||
"document-type",
|
||||
flagDocumentType,
|
||||
[]string{"OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
input["documentType"] = flagDocumentType
|
||||
}
|
||||
if cmd.Flags().Changed("classification") {
|
||||
if err := cmdutil.ValidateEnum(
|
||||
"classification",
|
||||
flagClassification,
|
||||
[]string{"PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
input["classification"] = flagClassification
|
||||
}
|
||||
|
||||
if len(input) == 1 {
|
||||
return fmt.Errorf("at least one field must be specified for update")
|
||||
}
|
||||
|
||||
data, err := client.Do(
|
||||
updateMutation,
|
||||
map[string]any{"input": input},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp updateResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse response: %w", err)
|
||||
}
|
||||
|
||||
v := resp.UpdateDocumentVersion.DocumentVersion
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Updated version %s (%s v%d.%d)\n",
|
||||
v.ID,
|
||||
v.Title,
|
||||
v.Major,
|
||||
v.Minor,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagTitle, "title", "", "Version title")
|
||||
cmd.Flags().StringVar(&flagContent, "content", "", "Version content")
|
||||
cmd.Flags().StringVar(&flagDocumentType, "document-type", "", "Document type: OTHER, GOVERNANCE, POLICY, PROCEDURE, PLAN, REGISTER, RECORD, REPORT, TEMPLATE")
|
||||
cmd.Flags().StringVar(&flagClassification, "classification", "", "Classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -29,13 +29,15 @@ mutation($input: UpdateDocumentInput!) {
|
||||
document {
|
||||
id
|
||||
trustCenterVisibility
|
||||
versions(first: 1) {
|
||||
edges {
|
||||
node {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
documentVersion {
|
||||
id
|
||||
title
|
||||
major
|
||||
minor
|
||||
status
|
||||
documentType
|
||||
classification
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,19 +48,27 @@ type updateResponse struct {
|
||||
Document struct {
|
||||
ID string `json:"id"`
|
||||
TrustCenterVisibility string `json:"trustCenterVisibility"`
|
||||
Versions struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Title string `json:"title"`
|
||||
} `json:"node"`
|
||||
} `json:"edges"`
|
||||
} `json:"versions"`
|
||||
} `json:"document"`
|
||||
DocumentVersion *struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Status string `json:"status"`
|
||||
DocumentType string `json:"documentType"`
|
||||
Classification string `json:"classification"`
|
||||
} `json:"documentVersion"`
|
||||
} `json:"updateDocument"`
|
||||
}
|
||||
|
||||
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
var flagTrustCenterVisibility string
|
||||
var (
|
||||
flagTitle string
|
||||
flagContent string
|
||||
flagDocumentType string
|
||||
flagClassification string
|
||||
flagTrustCenterVisibility string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <id>",
|
||||
@@ -86,6 +96,32 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
"id": args[0],
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("title") {
|
||||
input["title"] = flagTitle
|
||||
}
|
||||
if cmd.Flags().Changed("content") {
|
||||
input["content"] = flagContent
|
||||
}
|
||||
if cmd.Flags().Changed("document-type") {
|
||||
if err := cmdutil.ValidateEnum(
|
||||
"document-type",
|
||||
flagDocumentType,
|
||||
[]string{"OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE"},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
input["documentType"] = flagDocumentType
|
||||
}
|
||||
if cmd.Flags().Changed("classification") {
|
||||
if err := cmdutil.ValidateEnum(
|
||||
"classification",
|
||||
flagClassification,
|
||||
[]string{"PUBLIC", "INTERNAL", "CONFIDENTIAL", "SECRET"},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
input["classification"] = flagClassification
|
||||
}
|
||||
if cmd.Flags().Changed("trust-center-visibility") {
|
||||
if err := cmdutil.ValidateEnum(
|
||||
"trust-center-visibility",
|
||||
@@ -115,21 +151,31 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
doc := resp.UpdateDocument.Document
|
||||
title := doc.ID
|
||||
if len(doc.Versions.Edges) > 0 {
|
||||
title = doc.Versions.Edges[0].Node.Title
|
||||
if v := resp.UpdateDocument.DocumentVersion; v != nil {
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Updated document %s (%s v%d.%d)\n",
|
||||
doc.ID,
|
||||
v.Title,
|
||||
v.Major,
|
||||
v.Minor,
|
||||
)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Updated document %s\n",
|
||||
doc.ID,
|
||||
)
|
||||
}
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Updated document %s (%s)\n",
|
||||
doc.ID,
|
||||
title,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagTitle, "title", "", "Document title")
|
||||
cmd.Flags().StringVar(&flagContent, "content", "", "Document content")
|
||||
cmd.Flags().StringVar(&flagDocumentType, "document-type", "", "Document type: OTHER, GOVERNANCE, POLICY, PROCEDURE, PLAN, REGISTER, RECORD, REPORT, TEMPLATE")
|
||||
cmd.Flags().StringVar(&flagClassification, "classification", "", "Classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET")
|
||||
cmd.Flags().StringVar(&flagTrustCenterVisibility, "trust-center-visibility", "", "Trust center visibility: NONE, PRIVATE, PUBLIC")
|
||||
|
||||
return cmd
|
||||
|
||||
Reference in New Issue
Block a user