mirror of
https://github.com/documize/community.git
synced 2025-07-19 05:09:42 +02:00
Provide copy document option
Duplicates entire document tree into a new document (same space).
This commit is contained in:
parent
b75969ae90
commit
ec8d5c78e2
10 changed files with 304 additions and 12 deletions
|
@ -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()
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ export default Component.extend(ModalMixin, AuthMixin, Notifier, {
|
|||
this.get('permissions.documentEdit')) return true;
|
||||
|
||||
}),
|
||||
duplicateName: '',
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
//**************************************************
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue