From ec8d5c78e2e1dab304b512fafd127a4100e2861b Mon Sep 17 00:00:00 2001 From: sauls8t Date: Thu, 6 Jun 2019 11:45:41 +0100 Subject: [PATCH] Provide copy document option Duplicates entire document tree into a new document (same space). --- domain/attachment/store.go | 6 +- domain/document/endpoint.go | 215 ++++++++++++++++++ domain/link/store.go | 10 +- .../components/document/document-toolbar.js | 26 ++- gui/app/pods/document/index/controller.js | 6 + gui/app/pods/document/index/template.hbs | 3 +- gui/app/services/document.js | 15 ++ .../components/document/document-toolbar.hbs | 27 ++- model/doc/doc.go | 7 + server/routing/routes.go | 1 + 10 files changed, 304 insertions(+), 12 deletions(-) diff --git a/domain/attachment/store.go b/domain/attachment/store.go index b6ce964a..78a1f5cd 100644 --- a/domain/attachment/store.go +++ b/domain/attachment/store.go @@ -34,8 +34,10 @@ func (s Store) Add(ctx domain.RequestContext, a attachment.Attachment) (err erro a.OrgID = ctx.OrgID a.Created = time.Now().UTC() a.Revised = time.Now().UTC() - bits := strings.Split(a.Filename, ".") - a.Extension = bits[len(bits)-1] + if len(a.Extension) == 0 { + bits := strings.Split(a.Filename, ".") + a.Extension = bits[len(bits)-1] + } _, err = ctx.Transaction.Exec(s.Bind("INSERT INTO dmz_doc_attachment (c_refid, c_orgid, c_docid, c_sectionid, c_job, c_fileid, c_filename, c_data, c_extension, c_created, c_revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), a.RefID, a.OrgID, a.DocumentID, a.SectionID, a.Job, a.FileID, a.Filename, a.Data, a.Extension, a.Created, a.Revised) diff --git a/domain/document/endpoint.go b/domain/document/endpoint.go index 2ceedb54..a072ed06 100644 --- a/domain/document/endpoint.go +++ b/domain/document/endpoint.go @@ -24,6 +24,7 @@ import ( "github.com/documize/community/core/response" "github.com/documize/community/core/streamutil" "github.com/documize/community/core/stringutil" + "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" "github.com/documize/community/domain/organization" "github.com/documize/community/domain/permission" @@ -34,6 +35,7 @@ import ( "github.com/documize/community/model/audit" "github.com/documize/community/model/doc" "github.com/documize/community/model/link" + "github.com/documize/community/model/page" pm "github.com/documize/community/model/permission" "github.com/documize/community/model/search" "github.com/documize/community/model/space" @@ -766,3 +768,216 @@ func (h *Handler) Export(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(export)) } + +// Duplicate makes a copy of a document. +// Name of new document is required. +func (h *Handler) Duplicate(w http.ResponseWriter, r *http.Request) { + method := "document.Duplicate" + ctx := domain.GetRequestContext(r) + + // Parse payload + defer streamutil.Close(r.Body) + body, err := ioutil.ReadAll(r.Body) + if err != nil { + response.WriteBadRequestError(w, method, err.Error()) + h.Runtime.Log.Error(method, err) + return + } + + m := doc.DuplicateModel{} + err = json.Unmarshal(body, &m) + if err != nil { + response.WriteBadRequestError(w, method, err.Error()) + h.Runtime.Log.Error(method, err) + return + } + + // Check permissions + if !permission.CanViewDocument(ctx, *h.Store, m.DocumentID) { + response.WriteForbiddenError(w) + return + } + if !permission.CanUploadDocument(ctx, *h.Store, m.SpaceID) { + response.WriteForbiddenError(w) + return + } + + ctx.Transaction, err = h.Runtime.Db.Beginx() + if err != nil { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Get document to be duplicated. + d, err := h.Store.Document.Get(ctx, m.DocumentID) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Assign new ID and remove versioning info. + d.RefID = uniqueid.Generate() + d.GroupID = "" + d.Name = m.Name + + // Fetch doc attachments, links. + da, err := h.Store.Attachment.GetAttachmentsWithData(ctx, m.DocumentID) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + dl, err := h.Store.Link.GetDocumentOutboundLinks(ctx, m.DocumentID) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Fetch published and unpublished sections. + pages, err := h.Store.Page.GetPages(ctx, m.DocumentID) + if err != nil && err != sql.ErrNoRows { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + if len(pages) == 0 { + pages = []page.Page{} + } + unpublished, err := h.Store.Page.GetUnpublishedPages(ctx, m.DocumentID) + if err != nil && err != sql.ErrNoRows { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + if len(unpublished) == 0 { + unpublished = []page.Page{} + } + pages = append(pages, unpublished...) + meta, err := h.Store.Page.GetDocumentPageMeta(ctx, m.DocumentID, false) + if err != nil && err != sql.ErrNoRows { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + if len(meta) == 0 { + meta = []page.Meta{} + } + + // Duplicate the complete document starting with the document. + err = h.Store.Document.Add(ctx, d) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + // Attachments + for i := range da { + da[i].RefID = uniqueid.Generate() + da[i].DocumentID = d.RefID + + err = h.Store.Attachment.Add(ctx, da[i]) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + } + // Links + for l := range dl { + dl[l].SourceDocumentID = d.RefID + dl[l].RefID = uniqueid.Generate() + + err = h.Store.Link.Add(ctx, dl[l]) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + } + // Sections + for j := range pages { + // Get meta for section + sm := page.Meta{} + for k := range meta { + if meta[k].SectionID == pages[j].RefID { + sm = meta[k] + break + } + } + + // Get attachments for section. + sa, err := h.Store.Attachment.GetSectionAttachments(ctx, pages[j].RefID) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + pages[j].RefID = uniqueid.Generate() + pages[j].DocumentID = d.RefID + sm.DocumentID = d.RefID + sm.SectionID = pages[j].RefID + + err = h.Store.Page.Add(ctx, page.NewPage{Page: pages[j], Meta: sm}) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Now add any section attachments. + for n := range sa { + sa[n].RefID = uniqueid.Generate() + sa[n].DocumentID = d.RefID + sa[n].SectionID = pages[j].RefID + + err = h.Store.Attachment.Add(ctx, sa[n]) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + } + } + + // Record activity and finish. + h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{ + SpaceID: d.SpaceID, + DocumentID: d.RefID, + SourceType: activity.SourceTypeDocument, + ActivityType: activity.TypeCreated}) + + ctx.Transaction.Commit() + + h.Store.Audit.Record(ctx, audit.EventTypeDocumentAdd) + + // Update search index if published. + if d.Lifecycle == workflow.LifecycleLive { + a, _ := h.Store.Attachment.GetAttachments(ctx, d.RefID) + go h.Indexer.IndexDocument(ctx, d, a) + + pages, _ := h.Store.Page.GetPages(ctx, d.RefID) + for i := range pages { + go h.Indexer.IndexContent(ctx, pages[i]) + } + } else { + go h.Indexer.DeleteDocument(ctx, d.RefID) + } + + response.WriteEmpty(w) +} diff --git a/domain/link/store.go b/domain/link/store.go index baf7aeb5..cdbf71de 100644 --- a/domain/link/store.go +++ b/domain/link/store.go @@ -76,13 +76,13 @@ func (s Store) GetDocumentOutboundLinks(ctx domain.RequestContext, documentID st WHERE c_orgid=? AND c_sourcedocid=?`), ctx.OrgID, documentID) - if err != nil && err != sql.ErrNoRows { - err = errors.Wrap(err, "select document oubound links") - return - } - if len(links) == 0 { + if err == sql.ErrNoRows || len(links) == 0 { + err = nil links = []link.Link{} } + if err != nil { + err = errors.Wrap(err, "select document oubound links") + } return } diff --git a/gui/app/components/document/document-toolbar.js b/gui/app/components/document/document-toolbar.js index 5c8ace31..8280ddca 100644 --- a/gui/app/components/document/document-toolbar.js +++ b/gui/app/components/document/document-toolbar.js @@ -48,6 +48,7 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, { this.get('permissions.documentEdit')) return true; }), + duplicateName: '', init() { this._super(...arguments); @@ -57,7 +58,7 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, { pinId: '', newName: '' }; - + this.saveTemplate = { name: '', description: '' @@ -88,6 +89,10 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, { this.modalOpen("#document-template-modal", {show:true}, "#new-template-name"); }, + onShowDuplicateModal() { + this.modalOpen("#document-duplicate-modal", {show:true}, "#duplicate-name"); + }, + onShowDeleteModal() { this.modalOpen("#document-delete-modal", {show:true}); }, @@ -183,6 +188,25 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, { return true; }, + onDuplicate() { + let name = this.get('duplicateName'); + + if (_.isEmpty(name)) { + $("#duplicate-name").addClass("is-invalid").focus(); + return; + } + + $("#duplicate-name").removeClass("is-invalid"); + + this.set('duplicateName', ''); + + this.get('onDuplicate')(name); + + this.modalClose('#document-duplicate-modal'); + + return true; + }, + onExport() { let spec = { spaceId: this.get('document.spaceId'), diff --git a/gui/app/pods/document/index/controller.js b/gui/app/pods/document/index/controller.js index 4777c3f9..3fc60131 100644 --- a/gui/app/pods/document/index/controller.js +++ b/gui/app/pods/document/index/controller.js @@ -189,6 +189,12 @@ export default Controller.extend(Notifier, { }); }, + onDuplicate(name) { + this.get('documentService').duplicate(this.get('folder.id'), this.get('document.id'), name).then(() => { + this.notifySuccess('Duplicated'); + }); + }, + onPageSequenceChange(currentPageId, changes) { this.set('currentPageId', currentPageId); diff --git a/gui/app/pods/document/index/template.hbs b/gui/app/pods/document/index/template.hbs index f21d2e0e..79887efd 100644 --- a/gui/app/pods/document/index/template.hbs +++ b/gui/app/pods/document/index/template.hbs @@ -17,6 +17,7 @@ refresh=(action "refresh") onSaveTemplate=(action "onSaveTemplate") onSaveDocument=(action "onSaveDocument") + onDuplicate=(action "onDuplicate") onDocumentDelete=(action "onDocumentDelete")}} @@ -63,7 +64,7 @@ -
+

{{document.name}}

{{document.excerpt}}

diff --git a/gui/app/services/document.js b/gui/app/services/document.js index edfb1012..0ea608a8 100644 --- a/gui/app/services/document.js +++ b/gui/app/services/document.js @@ -78,6 +78,21 @@ export default Service.extend({ }); }, + + // Duplicate creates a copy. + duplicate(spaceId, docId, docName) { + let data = { + spaceId: spaceId, + documentId: docId, + documentName: docName + }; + + return this.get('ajax').request(`document/duplicate`, { + method: 'POST', + data: JSON.stringify(data) + }); + }, + //************************************************** // Page //************************************************** diff --git a/gui/app/templates/components/document/document-toolbar.hbs b/gui/app/templates/components/document/document-toolbar.hbs index 58bd1018..bde8c54d 100644 --- a/gui/app/templates/components/document/document-toolbar.hbs +++ b/gui/app/templates/components/document/document-toolbar.hbs @@ -33,18 +33,17 @@
  • Download
  • {{#if permissions.documentAdd}}
  • -
  • Publish template
  • +
  • Template
  • +
  • Duplicate
  • {{/if}} {{#if permissions.documentDelete}}
  • Delete
  • {{/if}} - {{/attach-popover}} {{/ui/ui-toolbar-dropdown}} - {{#if (or showActivity showRevisions)}} {{#ui/ui-toolbar-dropdown label="History" arrow=true}} {{#attach-popover class="ember-attacher-popper" hideOn="click clickout" showOn="click" isShown=false}} @@ -129,3 +128,25 @@
    + + diff --git a/model/doc/doc.go b/model/doc/doc.go index bc183196..0c6900c7 100644 --- a/model/doc/doc.go +++ b/model/doc/doc.go @@ -111,3 +111,10 @@ type Version struct { DocumentID string `json:"documentId"` Lifecycle workflow.Lifecycle `json:"lifecycle"` } + +// DuplicateModel is used to create a copy of a document. +type DuplicateModel struct { + SpaceID string `json:"spaceId"` + DocumentID string `json:"documentId"` + Name string `json:"documentName"` +} diff --git a/server/routing/routes.go b/server/routing/routes.go index 88b0a434..6ea47a71 100644 --- a/server/routing/routes.go +++ b/server/routing/routes.go @@ -126,6 +126,7 @@ func RegisterEndpoints(rt *env.Runtime, s *store.Store) { AddPrivate(rt, "documents/{documentID}/attachments", []string{"POST", "OPTIONS"}, nil, attachment.Add) AddPrivate(rt, "documents/{documentID}/pages/{pageID}/meta", []string{"GET", "OPTIONS"}, nil, page.GetMeta) AddPrivate(rt, "documents/{documentID}/pages/{pageID}/copy/{targetID}", []string{"POST", "OPTIONS"}, nil, page.Copy) + AddPrivate(rt, "document/duplicate", []string{"POST", "OPTIONS"}, nil, document.Duplicate) AddPrivate(rt, "organization/setting", []string{"GET", "OPTIONS"}, nil, setting.GetGlobalSetting) AddPrivate(rt, "organization/setting", []string{"POST", "OPTIONS"}, nil, setting.SaveGlobalSetting)