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/document/document-sidebar.js b/app/app/components/document/document-sidebar.js index f34f2d8f..52f5dd32 100644 --- a/app/app/components/document/document-sidebar.js +++ b/app/app/components/document/document-sidebar.js @@ -15,6 +15,7 @@ import NotifierMixin from '../../mixins/notifier'; export default Ember.Component.extend(TooltipMixin, NotifierMixin, { documentService: Ember.inject.service('document'), + sectionService: Ember.inject.service('section'), document: {}, folder: {}, showToc: true, @@ -22,6 +23,13 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, { showScrollTool: false, showingSections: false, + init() { + this._super(...arguments); + this.get('sectionService').getSpaceBlocks(this.get('folder.id')).then((b) => { + this.set('blocks', b); + }); + }, + didRender() { if (this.session.authenticated) { this.addTooltip(document.getElementById("section-tool")); @@ -93,6 +101,16 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, { this.attrs.onAddSection(section); }, + onInsertBlock(block) { + this.send('showToc'); + this.attrs.onInsertBlock(block); + }, + + onDeleteBlock(id) { + this.set('blocks', this.get('blocks').filter((item) => item.get('id') !== id)); + this.attrs.onDeleteBlock(id); + }, + scrollTop() { this.set('showScrollTool', false); diff --git a/app/app/components/document/document-toolbar.js b/app/app/components/document/document-toolbar.js index 5db06255..d7190d04 100644 --- a/app/app/components/document/document-toolbar.js +++ b/app/app/components/document/document-toolbar.js @@ -17,6 +17,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, { appMeta: Ember.inject.service(), userService: Ember.inject.service('user'), localStorage: Ember.inject.service(), + pinned: Ember.inject.service(), drop: null, users: [], menuOpen: false, @@ -24,7 +25,6 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, { name: "", description: "" }, - pinned: Ember.inject.service(), pinState : { isPinned: false, pinId: '', diff --git a/app/app/components/document/document-view.js b/app/app/components/document/document-view.js index 7ac563ed..15466ae7 100644 --- a/app/app/components/document/document-view.js +++ b/app/app/components/document/document-view.js @@ -70,6 +70,18 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, { }, actions: { + onAddBlock(block) { + this.attrs.onAddBlock(block); + }, + + onCopyPage(pageId, documentId) { + this.attrs.onCopyPage(pageId, documentId); + }, + + onMovePage(pageId, documentId) { + this.attrs.onMovePage(pageId, documentId); + }, + onDeletePage(id, deleteChildren) { let page = this.get('pages').findBy("id", id); diff --git a/app/app/components/document/page-heading.js b/app/app/components/document/page-heading.js index 022674f0..7bd974bb 100644 --- a/app/app/components/document/page-heading.js +++ b/app/app/components/document/page-heading.js @@ -13,29 +13,75 @@ import Ember from 'ember'; import TooltipMixin from '../../mixins/tooltip'; const { - computed + computed, + inject: { service } } = Ember; export default Ember.Component.extend(TooltipMixin, { + documentService: service('document'), deleteChildren: false, + menuOpen: false, + blockTitle: "", + blockExcerpt: "", + documentList: [], //includes the current document + documentListOthers: [], //excludes the current document + selectedDocument: null, checkId: computed('page', function () { let id = this.get('page.id'); return `delete-check-button-${id}`; }), - - dropTarget: computed('page', function () { + menuTarget: computed('page', function () { + let id = this.get('page.id'); + return `page-menu-${id}`; + }), + deleteButtonId: computed('page', function () { let id = this.get('page.id'); return `delete-page-button-${id}`; }), + publishButtonId: computed('page', function () { + let id = this.get('page.id'); + return `publish-button-${id}`; + }), + publishDialogId: computed('page', function () { + let id = this.get('page.id'); + return `publish-dialog-${id}`; + }), + blockTitleId: computed('page', function () { + let id = this.get('page.id'); + return `block-title-${id}`; + }), + blockExcerptId: computed('page', function () { + let id = this.get('page.id'); + return `block-excerpt-${id}`; + }), + copyButtonId: computed('page', function () { + let id = this.get('page.id'); + return `copy-page-button-${id}`; + }), + copyDialogId: computed('page', function () { + let id = this.get('page.id'); + return `copy-dialog-${id}`; + }), + moveButtonId: computed('page', function () { + let id = this.get('page.id'); + return `move-page-button-${id}`; + }), + moveDialogId: computed('page', function () { + let id = this.get('page.id'); + return `move-dialog-${id}`; + }), didRender() { if (this.get('isEditor')) { let self = this; - $(".page-edit-button, .page-delete-button").each(function (i, el) { + $(".page-action-button").each(function (i, el) { self.addTooltip(el); }); } + + $("#" + this.get('blockTitleId')).removeClass('error'); + $("#" + this.get('blockExcerptId')).removeClass('error'); }, willDestroyElement() { @@ -43,6 +89,20 @@ export default Ember.Component.extend(TooltipMixin, { }, actions: { + onMenuOpen() { + if ($('#' + this.get('publishDialogId')).is( ":visible" )) { + return; + } + if ($('#' + this.get('copyDialogId')).is( ":visible" )) { + return; + } + if ($('#' + this.get('moveDialogId')).is( ":visible" )) { + return; + } + + this.set('menuOpen', !this.get('menuOpen')); + }, + editPage(id) { this.attrs.onEditPage(id); }, @@ -50,5 +110,94 @@ export default Ember.Component.extend(TooltipMixin, { deletePage(id) { this.attrs.onDeletePage(id, this.get('deleteChildren')); }, + + onAddBlock(page) { + let titleElem = '#' + this.get('blockTitleId'); + let blockTitle = this.get('blockTitle'); + if (is.empty(blockTitle)) { + $(titleElem).addClass('error'); + return; + } + + let excerptElem = '#' + this.get('blockExcerptId'); + let blockExcerpt = this.get('blockExcerpt'); + blockExcerpt = blockExcerpt.replace(/\n/g, ""); + if (is.empty(blockExcerpt)) { + $(excerptElem).addClass('error'); + return; + } + + this.get('documentService').getPageMeta(this.get('document.id'), page.get('id')).then((pm) => { + let block = { + folderId: this.get('folder.id'), + contentType: page.get('contentType'), + pageType: page.get('pageType'), + title: blockTitle, + body: page.get('body'), + excerpt: blockExcerpt, + rawBody: pm.get('rawBody'), + config: pm.get('config'), + externalSource: pm.get('externalSource') + }; + + this.attrs.onAddBlock(block); + this.set('menuOpen', false); + this.set('blockTitle', ''); + this.set('blockExcerpt', ''); + $(titleElem).removeClass('error'); + $(excerptElem).removeClass('error'); + + return true; + }); + }, + + // Copy/move actions + onCopyDialogOpen() { + // Fetch document targets once. + if (this.get('documentList').length > 0) { + return; + } + + this.get('documentService').getPageMoveCopyTargets().then((d) => { + let me = this.get('document'); + this.set('documentList', d); + this.set('documentListOthers', d.filter((item) => item.get('id') !== me.get('id'))); + }); + }, + + onTargetChange(d) { + this.set('selectedDocument', d); + }, + + onCopyPage(page) { + // can't proceed if no data + if (this.get('documentList.length') === 0) { + return; + } + + let targetDocumentId = this.get('document.id'); + if (is.not.null(this.get('selectedDocument'))) { + targetDocumentId = this.get('selectedDocument.id'); + } + + this.attrs.onCopyPage(page.get('id'), targetDocumentId); + return true; + }, + + onMovePage(page) { + // can't proceed if no data + if (this.get('documentListOthers.length') === 0) { + return; + } + + if (is.null(this.get('selectedDocument'))) { + this.set('selectedDocument', this.get('documentListOthers')[0]); + } + + let targetDocumentId = this.get('selectedDocument.id'); + + this.attrs.onMovePage(page.get('id'), targetDocumentId); + return true; + } } }); diff --git a/app/app/components/document/page-wizard.js b/app/app/components/document/page-wizard.js index 9e1728dc..87536103 100644 --- a/app/app/components/document/page-wizard.js +++ b/app/app/components/document/page-wizard.js @@ -12,8 +12,23 @@ import Ember from 'ember'; import NotifierMixin from '../../mixins/notifier'; +const { + computed, +} = Ember; + export default Ember.Component.extend(NotifierMixin, { display: 'section', // which CSS to use + hasTemplates: false, + + didReceiveAttrs() { + let blocks = this.get('blocks'); + + this.set('hasBlocks', blocks.get('length') > 0); + + blocks.forEach((b) => { + b.set('deleteId', `delete-block-button-${b.id}`); + }); + }, didRender() { let self = this; @@ -34,7 +49,15 @@ export default Ember.Component.extend(NotifierMixin, { }, addSection(section) { - this.attrs.onAction(section); + this.attrs.onAddSection(section); + }, + + onDeleteBlock(id) { + this.attrs.onDeleteBlock(id); + }, + + onInsertBlock(block) { + this.attrs.onInsertBlock(block); } } }); diff --git a/app/app/components/dropdown-dialog.js b/app/app/components/dropdown-dialog.js index d4e0190f..70ff7ea3 100644 --- a/app/app/components/dropdown-dialog.js +++ b/app/app/components/dropdown-dialog.js @@ -57,16 +57,12 @@ export default Ember.Component.extend({ classes: 'drop-theme-basic', position: self.get('position'), openOn: self.get('open'), + constrainToWindow: false, + constrainToScrollParent: false, tetherOptions: { offset: self.offset, targetOffset: self.targetOffset, - // optimizations: { - // moveElement: false - // }, - constraints: [{ - to: 'window', - attachment: 'together' - }] + targetModifier: 'scroll-handle' }, remove: true }); diff --git a/app/app/components/dropdown-menu.js b/app/app/components/dropdown-menu.js index ad05604f..ab3ce88b 100644 --- a/app/app/components/dropdown-menu.js +++ b/app/app/components/dropdown-menu.js @@ -24,10 +24,6 @@ export default Ember.Component.extend({ didReceiveAttrs() { this.set("contentId", 'dropdown-menu-' + stringUtil.makeId(10)); - - // if (this.session.get('isMobile')) { - // this.set('open', "click"); - // } }, didInsertElement() { @@ -40,10 +36,14 @@ export default Ember.Component.extend({ classes: 'drop-theme-menu', position: self.get('position'), openOn: self.get('open'), + constrainToWindow: false, + constrainToScrollParent: false, tetherOptions: { offset: "5px 0", - targetOffset: "10px 0" - } + targetOffset: "10px 0", + targetModifier: 'scroll-handle', + }, + remove: true }); if (drop) { diff --git a/app/app/components/folder/folders-list.js b/app/app/components/folder/folders-list.js index 8937f3d0..4925d300 100644 --- a/app/app/components/folder/folders-list.js +++ b/app/app/components/folder/folders-list.js @@ -53,7 +53,7 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, { }; saved.forEach(function(t) { - t.img = "template-saved"; + Ember.set(t, 'img', 'template-saved'); }); saved.unshiftObject(emptyTemplate); diff --git a/app/app/components/section/base-editor.js b/app/app/components/section/base-editor.js index f140e418..b30be215 100644 --- a/app/app/components/section/base-editor.js +++ b/app/app/components/section/base-editor.js @@ -17,6 +17,9 @@ 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')); + }), didRender() { let self = this; @@ -30,10 +33,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 +87,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/models/block.js b/app/app/models/block.js new file mode 100644 index 00000000..5b882429 --- /dev/null +++ b/app/app/models/block.js @@ -0,0 +1,32 @@ +// 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 Model from 'ember-data/model'; +import attr from 'ember-data/attr'; + +export default Model.extend({ + orgId: attr('string'), + folderId: attr('string'), + userId: attr('string'), + contentType: attr('string'), + pageType: attr('string'), + title: attr('string'), + body: attr('string'), + excerpt: attr('string'), + used: attr('number', { defaultValue: 0 }), + rawBody: attr(), + config: attr(), + externalSource: attr('boolean', { defaultValue: false }), + firstname: attr('string'), + lastname: attr('string'), + created: attr(), + revised: attr() +}); diff --git a/app/app/models/page.js b/app/app/models/page.js index d72ca489..ad563ebe 100644 --- a/app/app/models/page.js +++ b/app/app/models/page.js @@ -22,6 +22,7 @@ export default Model.extend({ level: attr('number', { defaultValue: 1 }), sequence: attr('number', { defaultValue: 0 }), revisions: attr('number', { defaultValue: 0 }), + blockId: attr('string'), title: attr('string'), body: attr('string'), rawBody: attr('string'), 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/pods/document/controller.js b/app/app/pods/document/controller.js index 0a5b840b..7e08039d 100644 --- a/app/app/pods/document/controller.js +++ b/app/app/pods/document/controller.js @@ -15,6 +15,7 @@ import NotifierMixin from '../../mixins/notifier'; export default Ember.Controller.extend(NotifierMixin, { documentService: Ember.inject.service('document'), templateService: Ember.inject.service('template'), + sectionService: Ember.inject.service('section'), page: null, folder: {}, pages: [], @@ -142,6 +143,60 @@ export default Ember.Controller.extend(NotifierMixin, { }); }, + onInsertBlock(block) { + this.audit.record("added-content-block-" + block.get('contentType')); + + let page = { + documentId: this.get('model.document.id'), + title: `${block.get('title')}`, + level: 1, + sequence: 0, + body: block.get('body'), + contentType: block.get('contentType'), + pageType: block.get('pageType'), + blockId: block.get('id') + }; + + let meta = { + documentId: this.get('model.document.id'), + rawBody: block.get('rawBody'), + config: block.get('config'), + externalSource: block.get('externalSource') + }; + + let model = { + page: page, + meta: meta + }; + + this.get('documentService').addPage(this.get('model.document.id'), model).then((newPage) => { + let data = this.get('store').normalize('page', newPage); + this.get('store').push(data); + + this.get('documentService').getPages(this.get('model.document.id')).then((pages) => { + this.set('model.pages', pages.filterBy('pageType', 'section')); + this.set('model.tabs', pages.filterBy('pageType', 'tab')); + + this.get('documentService').getPageMeta(this.get('model.document.id'), newPage.id).then(() => { + this.transitionToRoute('document.edit', + this.get('model.folder.id'), + this.get('model.folder.slug'), + this.get('model.document.id'), + this.get('model.document.slug'), + newPage.id); + }); + }); + }); + }, + + onDeleteBlock(blockId) { + this.get('sectionService').deleteBlock(blockId).then(() => { + this.audit.record("deleted-block"); + this.send("showNotification", "Deleted"); + this.transitionToRoute('document.index'); + }); + }, + onDocumentDelete() { this.get('documentService').deleteDocument(this.get('model.document.id')).then(() => { this.audit.record("deleted-page"); diff --git a/app/app/pods/document/index/controller.js b/app/app/pods/document/index/controller.js index 9046f1b8..b8fbbb14 100644 --- a/app/app/pods/document/index/controller.js +++ b/app/app/pods/document/index/controller.js @@ -14,6 +14,7 @@ import NotifierMixin from '../../../mixins/notifier'; export default Ember.Controller.extend(NotifierMixin, { documentService: Ember.inject.service('document'), + sectionService: Ember.inject.service('section'), queryParams: ['page'], // Jump to the right part of the document. @@ -85,6 +86,34 @@ export default Ember.Controller.extend(NotifierMixin, { }); }, + onAddBlock(block) { + this.get('sectionService').addBlock(block).then(() => { + this.showNotification("Published"); + }); + }, + + onCopyPage(pageId, targetDocumentId) { + let documentId = this.get('model.document.id'); + this.get('documentService').copyPage(documentId, pageId, targetDocumentId).then(() => { + this.showNotification("Copied"); + + // refresh data if copied to same document + if (documentId === targetDocumentId) { + this.get('target.router').refresh(); + } + }); + }, + + onMovePage(pageId, targetDocumentId) { + let documentId = this.get('model.document.id'); + + this.get('documentService').copyPage(documentId, pageId, targetDocumentId).then(() => { + this.showNotification("Moved"); + + this.send('onPageDeleted', { id: pageId, children: false }); + }); + }, + onPageDeleted(deletePage) { let documentId = this.get('model.document.id'); let pages = this.get('model.pages'); diff --git a/app/app/pods/document/index/template.hbs b/app/app/pods/document/index/template.hbs index 36019b5a..70063ee5 100644 --- a/app/app/pods/document/index/template.hbs +++ b/app/app/pods/document/index/template.hbs @@ -1,2 +1,2 @@ {{document/document-view document=model.document links=model.links allPages=model.allPages tabs=model.tabs pages=model.pages folder=model.folder folders=model.folders isEditor=model.isEditor - gotoPage=(action 'gotoPage') onDeletePage=(action 'onPageDeleted')}} + gotoPage=(action 'gotoPage') onAddBlock=(action 'onAddBlock') onCopyPage=(action 'onCopyPage') onMovePage=(action 'onMovePage') onDeletePage=(action 'onPageDeleted')}} diff --git a/app/app/pods/document/template.hbs b/app/app/pods/document/template.hbs index 233afefe..cff52928 100644 --- a/app/app/pods/document/template.hbs +++ b/app/app/pods/document/template.hbs @@ -2,7 +2,7 @@ {{#layout/zone-sidebar}} {{document/document-sidebar document=model.document folder=model.folder pages=model.pages page=model.page isEditor=model.isEditor sections=model.sections - onAddSection=(action 'onAddSection') changePageSequence=(action 'onPageSequenceChange') changePageLevel=(action 'onPageLevelChange') gotoPage=(action 'gotoPage')}} + onAddSection=(action 'onAddSection') onInsertBlock=(action 'onInsertBlock') onDeleteBlock=(action 'onDeleteBlock') changePageSequence=(action 'onPageSequenceChange') changePageLevel=(action 'onPageLevelChange') gotoPage=(action 'gotoPage')}} {{/layout/zone-sidebar}} {{#layout/zone-content}} 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/document.js b/app/app/services/document.js index 1554494e..7d1d1130 100644 --- a/app/app/services/document.js +++ b/app/app/services/document.js @@ -292,11 +292,50 @@ export default Ember.Service.extend({ // nuke an attachment deleteAttachment(documentId, attachmentId) { - return this.get('ajax').request(`documents/${documentId}/attachments/${attachmentId}`, { method: 'DELETE' }); }, + + //************************************************** + // Page Move Copy + //************************************************** + + // Return list of documents that can accept a page. + getPageMoveCopyTargets() { + return this.get('ajax').request(`sections/targets`, { + method: 'GET' + }).then((response) => { + let data = []; + + data = response.map((obj) => { + let data = this.get('store').normalize('document', obj); + return this.get('store').push(data); + }); + + return data; + }); + }, + + // Copy existing page to same or different document. + copyPage(documentId, pageId, targetDocumentId) { + return this.get('ajax').request(`documents/${documentId}/pages/${pageId}/copy/${targetDocumentId}`, { + method: 'POST' + }).then((response) => { + let data = this.get('store').normalize('page', response); + return this.get('store').push(data); + }); + }, + + // Move existing page to different document. + movePage(documentId, pageId, targetDocumentId) { + return this.get('ajax').request(`documents/${documentId}/pages/${pageId}/move/${targetDocumentId}`, { + method: 'POST' + }).then((response) => { + let data = this.get('store').normalize('page', response); + return this.get('store').push(data); + }); + } }); function isObject(a) { diff --git a/app/app/services/section.js b/app/app/services/section.js index fdac0d35..3f7d36d0 100644 --- a/app/app/services/section.js +++ b/app/app/services/section.js @@ -68,5 +68,65 @@ export default BaseService.extend({ return pages; }); + }, + + /************************************************** + * Reusable Content Blocks + **************************************************/ + + // Save new reusable content block. + addBlock(payload) { + let url = `sections/blocks`; + + return this.get('ajax').post(url, { + data: JSON.stringify(payload), + contentType: 'json' + }).then((response) => { + let data = this.get('store').normalize('block', response); + return this.get('store').push(data); + }); + }, + + // 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/space/${folderId}`, { + method: 'GET' + }).then((response) => { + let data = []; + + data = response.map((obj) => { + let data = this.get('store').normalize('block', obj); + return this.get('store').push(data); + }); + + return data; + }); + }, + + // Returns reusable content block. + updateBlock(block) { + return this.get('ajax').request(`sections/blocks/${block.id}`, { + method: 'PUT', + data: JSON.stringify(block) + }); + }, + + // Removes specified reusable content block. + deleteBlock: function (blockId) { + let url = `sections/blocks/${blockId}`; + + return this.get('ajax').request(url, { + method: 'DELETE' + }); } }); diff --git a/app/app/styles/view/document/content.scss b/app/app/styles/view/document/content.scss index 4d0cbbdb..47756abd 100644 --- a/app/app/styles/view/document/content.scss +++ b/app/app/styles/view/document/content.scss @@ -41,12 +41,13 @@ > .is-a-page { .page-title { > .page-toolbar { - opacity: 0.3; + // opacity: 0.5; + opacity: 0; @extend .transition-all; - &:hover { - opacity: 1; - } + // &:hover { + // opacity: 1; + // } } &:hover { diff --git a/app/app/styles/view/document/wizard.scss b/app/app/styles/view/document/wizard.scss index d49aae6a..e9996962 100644 --- a/app/app/styles/view/document/wizard.scss +++ b/app/app/styles/view/document/wizard.scss @@ -4,6 +4,17 @@ > .canvas { padding: 0; + > .divider { + margin: 30px 0 20px 0; + border-top: 1px dotted $color-gray; + } + + > .template-caption { + text-align: center; + color: $color-gray; + padding-bottom: 10px; + } + > .list { margin: 0; padding: 0; @@ -18,6 +29,10 @@ &:hover { @include ease-in(); + > .actions { + display: inline-block; + } + > .details { > .title { color: $color-primary; @@ -61,6 +76,19 @@ margin-top: 5px; } } + + > .actions { + display: none; + vertical-align: top; + text-align: center; + width: 20px; + margin-top: 5px; + opacity: 0.4; + + > .material-icons, a { + color: $color-gray; + } + } } } } diff --git a/app/app/styles/widget/widget-dropdown.scss b/app/app/styles/widget/widget-dropdown.scss index 0de75d68..38dfc147 100644 --- a/app/app/styles/widget/widget-dropdown.scss +++ b/app/app/styles/widget/widget-dropdown.scss @@ -97,6 +97,15 @@ } } + > li.danger { + color: $color-red; + + &:hover { + color: $color-red; + font-weight: bold; + } + } + > li.divider { height: 1px; border-top: 1px solid $color-border; 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/document-sidebar.hbs b/app/app/templates/components/document/document-sidebar.hbs index ab750ae1..9f5529a6 100644 --- a/app/app/templates/components/document/document-sidebar.hbs +++ b/app/app/templates/components/document/document-sidebar.hbs @@ -25,7 +25,7 @@ gotoPage=(action 'gotoPage')}} {{/if}} {{#if showSections}} - {{document/page-wizard display='section' document=document folder=folder sections=sections - onCancel=(action 'onCancel') onAction=(action 'onAddSection')}} + {{document/page-wizard display='section' document=document folder=folder sections=sections blocks=blocks + onCancel=(action 'onCancel') onAddSection=(action 'onAddSection') onInsertBlock=(action 'onInsertBlock') onDeleteBlock=(action 'onDeleteBlock')}} {{/if}} diff --git a/app/app/templates/components/document/document-toolbar.hbs b/app/app/templates/components/document/document-toolbar.hbs index 698c0cb7..02d7191b 100644 --- a/app/app/templates/components/document/document-toolbar.hbs +++ b/app/app/templates/components/document/document-toolbar.hbs @@ -59,7 +59,7 @@ {{#if isEditor}}
  • -
  • Delete
  • +
  • Delete
  • {{/if}} {{/dropdown-menu}} @@ -74,12 +74,10 @@ {{#if session.authenticated}} {{#unless pinState.isPinned}} {{#dropdown-dialog target="pin-document-button" position="bottom right" button="Pin" color="flat-green" onAction=(action 'pin') focusOn="pin-document-name" }} -
    -
    - -
    A 3 or 4 character name
    - {{input type='text' id="pin-document-name" value=pinState.newName}} -
    +
    + +
    A 3 or 4 character name
    + {{input type='text' id="pin-document-name" value=pinState.newName}}
    {{/dropdown-dialog}} {{/unless}} @@ -87,17 +85,15 @@ {{#if isEditor}} {{#dropdown-dialog target="save-template-button" position="bottom right" button="Save as Template" color="flat-green" onAction=(action 'saveTemplate') focusOn="new-template-name" }} -
    -
    - -
    Short name for this type of document
    - {{input type='text' id="new-template-name" value=saveTemplate.name}} -
    -
    - -
    Explain use case for this template
    - {{textarea value=saveTemplate.description rows="3" id="new-template-desc"}} -
    +
    + +
    Short name for this type of document
    + {{input type='text' id="new-template-name" value=saveTemplate.name}} +
    +
    + +
    Explain use case for this template
    + {{textarea value=saveTemplate.description rows="3" id="new-template-desc"}}
    {{/dropdown-dialog}} diff --git a/app/app/templates/components/document/document-view.hbs b/app/app/templates/components/document/document-view.hbs index 8cc7cafa..6b5f7b4f 100644 --- a/app/app/templates/components/document/document-view.hbs +++ b/app/app/templates/components/document/document-view.hbs @@ -17,7 +17,8 @@ {{#each pages key="id" as |page index|}}
    - {{document/page-heading tagName=page.tagName document=document folder=folder page=page isEditor=isEditor onDeletePage=(action 'onDeletePage')}} + {{document/page-heading tagName=page.tagName document=document folder=folder page=page isEditor=isEditor + onAddBlock=(action 'onAddBlock') onCopyPage=(action 'onCopyPage') onMovePage=(action 'onMovePage') onDeletePage=(action 'onDeletePage')}} {{section/base-renderer page=page}}
    diff --git a/app/app/templates/components/document/page-heading.hbs b/app/app/templates/components/document/page-heading.hbs index 5da52b54..95258f8b 100644 --- a/app/app/templates/components/document/page-heading.hbs +++ b/app/app/templates/components/document/page-heading.hbs @@ -3,20 +3,75 @@
    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/app/public/assets/img/section-saved.png b/app/public/assets/img/section-saved.png new file mode 100644 index 00000000..c061a80c Binary files /dev/null and b/app/public/assets/img/section-saved.png differ diff --git a/app/public/assets/img/section-saved@2x.png b/app/public/assets/img/section-saved@2x.png new file mode 100644 index 00000000..a8bf09b1 Binary files /dev/null and b/app/public/assets/img/section-saved@2x.png differ diff --git a/app/vendor/tether.js b/app/vendor/tether.js index 7df866bc..49b2c220 100644 --- a/app/vendor/tether.js +++ b/app/vendor/tether.js @@ -1,4 +1,4 @@ -/*! tether 1.3.2 */ +/*! tether 1.4.0 */ (function(root, factory) { if (typeof define === 'function' && define.amd) { @@ -23,6 +23,32 @@ if (typeof TetherBase === 'undefined') { var zeroElement = null; +// Same as native getBoundingClientRect, except it takes into account parent offsets +// if the element lies within a nested document ( or