diff --git a/app/app/components/document/block-editor.js b/app/app/components/document/block-editor.js new file mode 100644 index 00000000..83a2e4a6 --- /dev/null +++ b/app/app/components/document/block-editor.js @@ -0,0 +1,53 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +export default Ember.Component.extend({ + store: Ember.inject.service(), + + didReceiveAttrs() { + let p = this.get('store').createRecord('page'); + let m = this.get('store').createRecord('pageMeta'); + + p.set('id', this.get('block.id')); + p.set('orgId', this.get('block.orgId')); + p.set('documentId', this.get('document.id')); + p.set('contentType', this.get('block.contentType')); + p.set('pageType', this.get('block.pageType')); + p.set('title', this.get('block.title')); + p.set('body', this.get('block.body')); + p.set('rawBody', this.get('block.rawBody')); + p.set('excerpt', this.get('block.excerpt')); + + m.set('pageId', this.get('block.id')); + m.set('orgId', this.get('block.orgId')); + m.set('documentId', this.get('document.id')); + m.set('rawBody', this.get('block.rawBody')); + m.set('config', this.get('block.config')); + m.set('externalSource', this.get('block.externalSource')); + + this.set('page', p); + this.set('meta', m); + + this.set('editorType', 'section/' + this.get('block.contentType') + '/type-editor'); + }, + + actions: { + onCancel() { + this.attrs.onCancel(); + }, + + onAction(page, meta) { + this.attrs.onAction(page, meta); + } + } +}); diff --git a/app/app/components/section/base-editor.js b/app/app/components/section/base-editor.js index f140e418..b9d0c0bb 100644 --- a/app/app/components/section/base-editor.js +++ b/app/app/components/section/base-editor.js @@ -17,6 +17,13 @@ export default Ember.Component.extend({ actionLabel: "Save", tip: "Short and concise title", busy: false, + hasExcerpt: Ember.computed('page', function () { + return is.not.undefined(this.get('page.excerpt')); + }), + + didReceiveAttrs() { + this._super(...arguments); + }, didRender() { let self = this; @@ -30,10 +37,14 @@ export default Ember.Component.extend({ }); $("#page-title").removeClass("error"); + $("#page-excerpt").removeClass("error"); $("#page-title").focus(function() { $(this).select(); }); + $("#page-excerpt").focus(function() { + $(this).select(); + }); }, willDestroyElement() { @@ -80,6 +91,11 @@ export default Ember.Component.extend({ return; } + if (this.get('hasExcerpt') && is.empty(this.get('page.excerpt'))) { + $("#page-excerpt").addClass("error").focus(); + return; + } + this.attrs.onAction(this.get('page.title')); }, diff --git a/app/app/pods/document/block/controller.js b/app/app/pods/document/block/controller.js new file mode 100644 index 00000000..4699a8d9 --- /dev/null +++ b/app/app/pods/document/block/controller.js @@ -0,0 +1,47 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; + +export default Ember.Controller.extend({ + sectionService: Ember.inject.service('section'), + + actions: { + onCancel( /*page*/ ) { + this.transitionToRoute('document', { + queryParams: { + page: this.get('model.page.id') + } + }); + }, + + onAction(page, meta) { + let self = this; + + let block = this.get('model.block'); + block.set('title', page.get('title')); + block.set('body', page.get('body')); + block.set('excerpt', page.get('excerpt')); + block.set('rawBody', meta.get('rawBody')); + block.set('config', meta.get('config')); + block.set('externalSource', meta.get('externalSource')); + + this.get('sectionService').updateBlock(block).then(function () { + self.audit.record("edited-block"); + self.transitionToRoute('document', { + queryParams: { + page: page.get('id') + } + }); + }); + } + } +}); diff --git a/app/app/pods/document/block/route.js b/app/app/pods/document/block/route.js new file mode 100644 index 00000000..43cdf8a4 --- /dev/null +++ b/app/app/pods/document/block/route.js @@ -0,0 +1,31 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import Ember from 'ember'; +import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; + +export default Ember.Route.extend(AuthenticatedRouteMixin, { + documentService: Ember.inject.service('document'), + folderService: Ember.inject.service('folder'), + sectionService: Ember.inject.service('section'), + + model(params) { + let self = this; + + this.audit.record("edited-block"); + + return Ember.RSVP.hash({ + folder: self.modelFor('document').folder, + document: self.modelFor('document').document, + block: self.get('sectionService').getBlock(params.block_id), + }); + } +}); diff --git a/app/app/pods/document/block/template.hbs b/app/app/pods/document/block/template.hbs new file mode 100644 index 00000000..4bfed5af --- /dev/null +++ b/app/app/pods/document/block/template.hbs @@ -0,0 +1 @@ +{{document/block-editor document=model.document folder=model.folder block=model.block onCancel=(action 'onCancel') onAction=(action 'onAction')}} diff --git a/app/app/router.js b/app/app/router.js index 421b0ec4..2856dbe5 100644 --- a/app/app/router.js +++ b/app/app/router.js @@ -50,6 +50,9 @@ export default Router.map(function () { this.route('history', { path: 'history' }); + this.route('block', { + path: 'block/:block_id' + }); }); this.route('customize', { diff --git a/app/app/services/section.js b/app/app/services/section.js index 734f9c1d..e0018a30 100644 --- a/app/app/services/section.js +++ b/app/app/services/section.js @@ -84,9 +84,19 @@ export default BaseService.extend({ }); }, + // Returns reusable content block. + getBlock(blockId) { + return this.get('ajax').request(`sections/blocks/${blockId}`, { + method: 'GET' + }).then((response) => { + let data = this.get('store').normalize('block', response); + return this.get('store').push(data); + }); + }, + // Returns all available reusable content block for section. getSpaceBlocks(folderId) { - return this.get('ajax').request(`sections/blocks/${folderId}`, { + return this.get('ajax').request(`sections/blocks/space/${folderId}`, { method: 'GET' }).then((response) => { let data = []; @@ -98,5 +108,13 @@ export default BaseService.extend({ return data; }); + }, + + // Returns reusable content block. + updateBlock(block) { + return this.get('ajax').request(`sections/blocks/${block.id}`, { + method: 'PUT', + data: JSON.stringify(block) + }); } }); diff --git a/app/app/styles/view/document/wizard.scss b/app/app/styles/view/document/wizard.scss index 128bb7fa..8eaa0860 100644 --- a/app/app/styles/view/document/wizard.scss +++ b/app/app/styles/view/document/wizard.scss @@ -29,6 +29,10 @@ &:hover { @include ease-in(); + > .actions { + display: inline-block; + } + > .details { > .title { color: $color-primary; @@ -72,6 +76,25 @@ margin-top: 5px; } } + + > .actions { + display: none; + vertical-align: top; + text-align: center; + width: 20px; + margin-top: 5px; + opacity: 0.3; + + > .material-icons, a { + color: $color-gray; + + &:hover { + opacity: 1; + color: $color-primary; + } + } + + } } } } diff --git a/app/app/templates/components/document/block-editor.hbs b/app/app/templates/components/document/block-editor.hbs new file mode 100644 index 00000000..96b3adc1 --- /dev/null +++ b/app/app/templates/components/document/block-editor.hbs @@ -0,0 +1 @@ +{{component editorType document=document folder=folder page=page meta=meta onCancel=(action 'onCancel') onAction=(action 'onAction')}} diff --git a/app/app/templates/components/document/page-wizard.hbs b/app/app/templates/components/document/page-wizard.hbs index 4850b503..4dcb5970 100644 --- a/app/app/templates/components/document/page-wizard.hbs +++ b/app/app/templates/components/document/page-wizard.hbs @@ -24,24 +24,31 @@
Reusable Content
    {{#each blocks as |block|}} -
  • -
    +
  • +
    -
    +
    {{block.title}}
    {{block.excerpt}}
    By {{block.firstname}} {{block.lastname}}, {{time-ago block.created}} (used: {{ block.used }})
    +
    + {{#link-to 'document.block' folder.id folder.slug document.id document.slug block.id}} + mode_edit + {{/link-to}} +
    + delete +
  • {{/each}}
{{else}}
-
No reusable content
+
Published, reusable sections appear below
{{/if}} diff --git a/app/app/templates/components/section/base-editor.hbs b/app/app/templates/components/section/base-editor.hbs index 4bb101fc..7add5190 100644 --- a/app/app/templates/components/section/base-editor.hbs +++ b/app/app/templates/components/section/base-editor.hbs @@ -6,6 +6,15 @@
{{tip}}
{{focus-input type='text' id="page-title" value=page.title class="mousetrap"}} + {{#if hasExcerpt}} +
+
+ +
Short description
+ {{textarea rows="3" id="page-excerpt" value=page.excerpt class="mousetrap"}} +
+
+ {{/if}}
{{#if busy}} diff --git a/core/api/endpoint/page_endpoint.go b/core/api/endpoint/page_endpoint.go index 06e403fb..73d80fd1 100644 --- a/core/api/endpoint/page_endpoint.go +++ b/core/api/endpoint/page_endpoint.go @@ -308,14 +308,6 @@ func DeleteDocumentPage(w http.ResponseWriter, r *http.Request) { p.Context.Transaction = tx - _, err = p.DeletePage(documentID, pageID) - - if err != nil { - log.IfErr(tx.Rollback()) - writeGeneralSQLError(w, method, err) - return - } - page, err := p.GetPage(pageID) if err != nil { log.IfErr(tx.Rollback()) @@ -327,6 +319,14 @@ func DeleteDocumentPage(w http.ResponseWriter, r *http.Request) { p.DecrementBlockUsage(page.BlockID) } + _, err = p.DeletePage(documentID, pageID) + + if err != nil { + log.IfErr(tx.Rollback()) + writeGeneralSQLError(w, method, err) + return + } + log.IfErr(tx.Commit()) writeSuccessEmptyJSON(w) @@ -376,13 +376,6 @@ func DeleteDocumentPages(w http.ResponseWriter, r *http.Request) { p.Context.Transaction = tx for _, page := range *model { - _, err = p.DeletePage(documentID, page.PageID) - if err != nil { - log.IfErr(tx.Rollback()) - writeGeneralSQLError(w, method, err) - return - } - pageData, err := p.GetPage(page.PageID) if err != nil { log.IfErr(tx.Rollback()) @@ -393,6 +386,13 @@ func DeleteDocumentPages(w http.ResponseWriter, r *http.Request) { if len(pageData.BlockID) > 0 { p.DecrementBlockUsage(pageData.BlockID) } + + _, err = p.DeletePage(documentID, page.PageID) + if err != nil { + log.IfErr(tx.Rollback()) + writeGeneralSQLError(w, method, err) + return + } } log.IfErr(tx.Commit()) diff --git a/core/api/endpoint/router.go b/core/api/endpoint/router.go index 79b1bfb9..1f8ace18 100644 --- a/core/api/endpoint/router.go +++ b/core/api/endpoint/router.go @@ -216,7 +216,9 @@ func init() { log.IfErr(Add(RoutePrefixPrivate, "sections", []string{"GET", "OPTIONS"}, nil, GetSections)) log.IfErr(Add(RoutePrefixPrivate, "sections", []string{"POST", "OPTIONS"}, nil, RunSectionCommand)) log.IfErr(Add(RoutePrefixPrivate, "sections/refresh", []string{"GET", "OPTIONS"}, nil, RefreshSections)) - log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/{folderID}", []string{"GET", "OPTIONS"}, nil, GetBlocksForSpace)) + log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/space/{folderID}", []string{"GET", "OPTIONS"}, nil, GetBlocksForSpace)) + log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"GET", "OPTIONS"}, nil, GetBlock)) + log.IfErr(Add(RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"PUT", "OPTIONS"}, nil, UpdateBlock)) log.IfErr(Add(RoutePrefixPrivate, "sections/blocks", []string{"POST", "OPTIONS"}, nil, AddBlock)) // Links diff --git a/core/api/endpoint/sections_endpoint.go b/core/api/endpoint/sections_endpoint.go index 58907c71..3f94f435 100644 --- a/core/api/endpoint/sections_endpoint.go +++ b/core/api/endpoint/sections_endpoint.go @@ -229,6 +229,34 @@ func AddBlock(w http.ResponseWriter, r *http.Request) { writeSuccessEmptyJSON(w) } +// GetBlock returns requested reusable content block. +func GetBlock(w http.ResponseWriter, r *http.Request) { + method := "GetBlock" + p := request.GetPersister(r) + + params := mux.Vars(r) + blockID := params["blockID"] + + if len(blockID) == 0 { + writeMissingDataError(w, method, "blockID") + return + } + + b, err := p.GetBlock(blockID) + if err != nil { + writeGeneralSQLError(w, method, err) + return + } + + json, err := json.Marshal(b) + if err != nil { + writeJSONMarshalError(w, method, "block", err) + return + } + + writeSuccessBytes(w, json) +} + // GetBlocksForSpace returns available reusable content blocks for the space. func GetBlocksForSpace(w http.ResponseWriter, r *http.Request) { method := "GetBlocksForSpace" @@ -264,3 +292,48 @@ func GetBlocksForSpace(w http.ResponseWriter, r *http.Request) { writeSuccessBytes(w, json) } + +// UpdateBlock inserts new reusable content block into database. +func UpdateBlock(w http.ResponseWriter, r *http.Request) { + method := "UpdateBlock" + p := request.GetPersister(r) + + defer utility.Close(r.Body) + body, err := ioutil.ReadAll(r.Body) + + if err != nil { + writeBadRequestError(w, method, "Bad payload") + return + } + + b := entity.Block{} + err = json.Unmarshal(body, &b) + if err != nil { + writePayloadError(w, method, err) + return + } + + if !p.CanUploadDocument(b.LabelID) { + writeForbiddenError(w) + return + } + + tx, err := request.Db.Beginx() + if err != nil { + writeTransactionError(w, method, err) + return + } + + p.Context.Transaction = tx + + err = p.UpdateBlock(b) + if err != nil { + log.IfErr(tx.Rollback()) + writeGeneralSQLError(w, method, err) + return + } + + log.IfErr(tx.Commit()) + + writeSuccessEmptyJSON(w) +} diff --git a/core/api/request/block.go b/core/api/request/block.go index b918eae2..2f42a415 100644 --- a/core/api/request/block.go +++ b/core/api/request/block.go @@ -48,7 +48,7 @@ func (p *Persister) AddBlock(b entity.Block) (err error) { // GetBlock returns requested reusable content block. func (p *Persister) GetBlock(id string) (b entity.Block, err error) { - stmt, err := Db.Preparex("SELECT id, refid, orgid, labelid, userid, contenttype, pagetype, title, body, excerpt, rawbody, config, externalsource, used, created, revised FROM block WHERE orgid=? AND refid=?") + stmt, err := Db.Preparex("SELECT a.id, a.refid, a.orgid, a.labelid, a.userid, a.contenttype, a.pagetype, a.title, a.body, a.excerpt, a.rawbody, a.config, a.externalsource, a.used, a.created, a.revised, b.firstname, b.lastname FROM block a LEFT JOIN user b ON a.userid = b.refid WHERE a.orgid=? AND a.refid=?") defer utility.Close(stmt) if err != nil { @@ -67,7 +67,7 @@ func (p *Persister) GetBlock(id string) (b entity.Block, err error) { // GetBlocksForSpace returns all reusable content scoped to given space. func (p *Persister) GetBlocksForSpace(labelID string) (b []entity.Block, err error) { - err = Db.Select(&b, "SELECT a.id, a.refid, a.orgid, a.labelid, a.userid, a.contenttype, a.pagetype, a.title, a.body, a.excerpt, a.rawbody, a.config, a.externalsource, a.used, a.created, a.revised, b.firstname, b.lastname FROM block a LEFT JOIN user b ON a.userid = b.refid WHERE orgid=? AND labelid=? ORDER BY a.title", p.Context.OrgID, labelID) + err = Db.Select(&b, "SELECT a.id, a.refid, a.orgid, a.labelid, a.userid, a.contenttype, a.pagetype, a.title, a.body, a.excerpt, a.rawbody, a.config, a.externalsource, a.used, a.created, a.revised, b.firstname, b.lastname FROM block a LEFT JOIN user b ON a.userid = b.refid WHERE a.orgid=? AND a.labelid=? ORDER BY a.title", p.Context.OrgID, labelID) if err != nil { log.Error(fmt.Sprintf("Unable to execute select GetBlocksForSpace org %s and label %s", p.Context.OrgID, labelID), err)