1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-18 20:59:43 +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.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)

View file

@ -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)
}

View file

@ -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
}

View file

@ -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'),

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

View file

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

View file

@ -33,18 +33,17 @@
<li class="item" {{action "onExport"}}>Download</li>
{{#if permissions.documentAdd}}
<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 permissions.documentDelete}}
<li class="divider"/>
<li class="item red" {{action "onShowDeleteModal"}}>Delete</li>
{{/if}}
</ul>
{{/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 @@
</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"`
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}/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)