1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 21:29:42 +02:00

Provide copy document option

Duplicates entire document tree into a new document (same space).
This commit is contained in:
sauls8t 2019-06-06 11:45:41 +01:00
parent b75969ae90
commit ec8d5c78e2
10 changed files with 304 additions and 12 deletions

View file

@ -34,8 +34,10 @@ func (s Store) Add(ctx domain.RequestContext, a attachment.Attachment) (err erro
a.OrgID = ctx.OrgID a.OrgID = ctx.OrgID
a.Created = time.Now().UTC() a.Created = time.Now().UTC()
a.Revised = time.Now().UTC() a.Revised = time.Now().UTC()
if len(a.Extension) == 0 {
bits := strings.Split(a.Filename, ".") bits := strings.Split(a.Filename, ".")
a.Extension = bits[len(bits)-1] 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), _, 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) a.RefID, a.OrgID, a.DocumentID, a.SectionID, a.Job, a.FileID, a.Filename, a.Data, a.Extension, a.Created, a.Revised)

View file

@ -24,6 +24,7 @@ import (
"github.com/documize/community/core/response" "github.com/documize/community/core/response"
"github.com/documize/community/core/streamutil" "github.com/documize/community/core/streamutil"
"github.com/documize/community/core/stringutil" "github.com/documize/community/core/stringutil"
"github.com/documize/community/core/uniqueid"
"github.com/documize/community/domain" "github.com/documize/community/domain"
"github.com/documize/community/domain/organization" "github.com/documize/community/domain/organization"
"github.com/documize/community/domain/permission" "github.com/documize/community/domain/permission"
@ -34,6 +35,7 @@ import (
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
"github.com/documize/community/model/doc" "github.com/documize/community/model/doc"
"github.com/documize/community/model/link" "github.com/documize/community/model/link"
"github.com/documize/community/model/page"
pm "github.com/documize/community/model/permission" pm "github.com/documize/community/model/permission"
"github.com/documize/community/model/search" "github.com/documize/community/model/search"
"github.com/documize/community/model/space" "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.WriteHeader(http.StatusOK)
w.Write([]byte(export)) 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)
}

View file

@ -76,13 +76,13 @@ func (s Store) GetDocumentOutboundLinks(ctx domain.RequestContext, documentID st
WHERE c_orgid=? AND c_sourcedocid=?`), WHERE c_orgid=? AND c_sourcedocid=?`),
ctx.OrgID, documentID) ctx.OrgID, documentID)
if err != nil && err != sql.ErrNoRows { if err == sql.ErrNoRows || len(links) == 0 {
err = errors.Wrap(err, "select document oubound links") err = nil
return
}
if len(links) == 0 {
links = []link.Link{} links = []link.Link{}
} }
if err != nil {
err = errors.Wrap(err, "select document oubound links")
}
return return
} }

View file

@ -48,6 +48,7 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, {
this.get('permissions.documentEdit')) return true; this.get('permissions.documentEdit')) return true;
}), }),
duplicateName: '',
init() { init() {
this._super(...arguments); this._super(...arguments);
@ -88,6 +89,10 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, {
this.modalOpen("#document-template-modal", {show:true}, "#new-template-name"); this.modalOpen("#document-template-modal", {show:true}, "#new-template-name");
}, },
onShowDuplicateModal() {
this.modalOpen("#document-duplicate-modal", {show:true}, "#duplicate-name");
},
onShowDeleteModal() { onShowDeleteModal() {
this.modalOpen("#document-delete-modal", {show:true}); this.modalOpen("#document-delete-modal", {show:true});
}, },
@ -183,6 +188,25 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, {
return true; 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() { onExport() {
let spec = { let spec = {
spaceId: this.get('document.spaceId'), spaceId: this.get('document.spaceId'),

View file

@ -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) { onPageSequenceChange(currentPageId, changes) {
this.set('currentPageId', currentPageId); this.set('currentPageId', currentPageId);

View file

@ -17,6 +17,7 @@
refresh=(action "refresh") refresh=(action "refresh")
onSaveTemplate=(action "onSaveTemplate") onSaveTemplate=(action "onSaveTemplate")
onSaveDocument=(action "onSaveDocument") onSaveDocument=(action "onSaveDocument")
onDuplicate=(action "onDuplicate")
onDocumentDelete=(action "onDocumentDelete")}} onDocumentDelete=(action "onDocumentDelete")}}
</div> </div>
</Layout::MasterToolbar> </Layout::MasterToolbar>
@ -63,7 +64,7 @@
<Ui::UiSpacer @size="300" /> <Ui::UiSpacer @size="300" />
<div class="document-meta {{if permissions.documentEdit "cursor-pointer"}}" {{action "onEditMeta"}}> <div class="document-meta">
<div class="document-heading"> <div class="document-heading">
<h1 class="name">{{document.name}}</h1> <h1 class="name">{{document.name}}</h1>
<h2 class="desc">{{document.excerpt}}</h2> <h2 class="desc">{{document.excerpt}}</h2>

View file

@ -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 // Page
//************************************************** //**************************************************

View file

@ -33,18 +33,17 @@
<li class="item" {{action "onExport"}}>Download</li> <li class="item" {{action "onExport"}}>Download</li>
{{#if permissions.documentAdd}} {{#if permissions.documentAdd}}
<li class="divider"/> <li class="divider"/>
<li class="item" {{action "onShowTemplateModal"}}>Publish template</li> <li class="item" {{action "onShowTemplateModal"}}>Template</li>
<li class="item" {{action "onShowDuplicateModal"}}>Duplicate</li>
{{/if}} {{/if}}
{{#if permissions.documentDelete}} {{#if permissions.documentDelete}}
<li class="divider"/> <li class="divider"/>
<li class="item red" {{action "onShowDeleteModal"}}>Delete</li> <li class="item red" {{action "onShowDeleteModal"}}>Delete</li>
{{/if}} {{/if}}
</ul> </ul>
{{/attach-popover}} {{/attach-popover}}
{{/ui/ui-toolbar-dropdown}} {{/ui/ui-toolbar-dropdown}}
{{#if (or showActivity showRevisions)}} {{#if (or showActivity showRevisions)}}
{{#ui/ui-toolbar-dropdown label="History" arrow=true}} {{#ui/ui-toolbar-dropdown label="History" arrow=true}}
{{#attach-popover class="ember-attacher-popper" hideOn="click clickout" showOn="click" isShown=false}} {{#attach-popover class="ember-attacher-popper" hideOn="click clickout" showOn="click" isShown=false}}
@ -129,3 +128,25 @@
</div> </div>
</div> </div>
</div> </div>
<div id="document-duplicate-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">Duplicate</div>
<div class="modal-body">
<form onsubmit={{action "onDuplicate"}}>
<div class="form-group">
<label for="duplicate-name">Name</label>
{{input id="duplicate-name" value=duplicateName type="email" class="form-control mousetrap" placeholder="Name"}}
<small class="form-text text-muted">Content will be duplicated within this space</small>
</div>
</form>
</div>
<div class="modal-footer">
{{ui/ui-button color=constants.Color.Gray light=true label=constants.Label.Cancel dismiss=true}}
{{ui/ui-button-gap}}
{{ui/ui-button color=constants.Color.Green light=true label=constants.Label.Duplicate onClick=(action "onDuplicate")}}
</div>
</div>
</div>
</div>

View file

@ -111,3 +111,10 @@ type Version struct {
DocumentID string `json:"documentId"` DocumentID string `json:"documentId"`
Lifecycle workflow.Lifecycle `json:"lifecycle"` 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"`
}

View file

@ -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}/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}/meta", []string{"GET", "OPTIONS"}, nil, page.GetMeta)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}/copy/{targetID}", []string{"POST", "OPTIONS"}, nil, page.Copy) 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{"GET", "OPTIONS"}, nil, setting.GetGlobalSetting)
AddPrivate(rt, "organization/setting", []string{"POST", "OPTIONS"}, nil, setting.SaveGlobalSetting) AddPrivate(rt, "organization/setting", []string{"POST", "OPTIONS"}, nil, setting.SaveGlobalSetting)