diff --git a/app/app/components/document/document-sidebar.js b/app/app/components/document/document-sidebar.js index f34f2d8f..ff9c6ca2 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').getSpaceSectionTemplates(this.get('folder.id')).then((t) => { + this.set('templates', t); + }); + }, + didRender() { if (this.session.authenticated) { this.addTooltip(document.getElementById("section-tool")); @@ -93,6 +101,11 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, { this.attrs.onAddSection(section); }, + onInsertTemplate(template) { + this.send('showToc'); + this.attrs.onInsertTemplate(template); + }, + 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..72675fbc 100644 --- a/app/app/components/document/document-view.js +++ b/app/app/components/document/document-view.js @@ -70,6 +70,16 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, { }, actions: { + onSaveAsPage(id, title) { + let params = { + documentId: this.get('document.id'), + pageId: id, + title: title, + }; + + this.attrs.onSaveAsPage(params); + }, + 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..252430b9 100644 --- a/app/app/components/document/page-heading.js +++ b/app/app/components/document/page-heading.js @@ -18,24 +18,44 @@ const { export default Ember.Component.extend(TooltipMixin, { deleteChildren: false, + menuOpen: false, + saveAsTitle: "", + 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}`; }), + saveAsTarget: computed('page', function () { + let id = this.get('page.id'); + return `saveas-page-button-${id}`; + }), + saveAsDialogId: computed('page', function () { + let id = this.get('page.id'); + return `save-as-dialog-${id}`; + }), + saveAsTitleId: computed('page', function () { + let id = this.get('page.id'); + return `save-as-title-${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('saveAsTitleId')).removeClass('error'); }, willDestroyElement() { @@ -43,6 +63,14 @@ export default Ember.Component.extend(TooltipMixin, { }, actions: { + onMenuOpen() { + if ($('#' + this.get('saveAsDialogId')).is( ":visible" )) { + return; + } + + this.set('menuOpen', !this.get('menuOpen')); + }, + editPage(id) { this.attrs.onEditPage(id); }, @@ -50,5 +78,21 @@ export default Ember.Component.extend(TooltipMixin, { deletePage(id) { this.attrs.onDeletePage(id, this.get('deleteChildren')); }, + + saveAsPage(id) { + let titleElem = '#' + this.get('saveAsTitleId'); + let saveAsTitle = this.get('saveAsTitle'); + if (is.empty(saveAsTitle)) { + $(titleElem).addClass('error'); + return; + } + + this.attrs.onSaveAsPage(id, saveAsTitle); + this.set('menuOpen', false); + this.set('saveAsTitle', ''); + $(titleElem).removeClass('error'); + + return true; + }, } }); diff --git a/app/app/components/document/page-wizard.js b/app/app/components/document/page-wizard.js index 9e1728dc..5d7f2136 100644 --- a/app/app/components/document/page-wizard.js +++ b/app/app/components/document/page-wizard.js @@ -14,6 +14,12 @@ import NotifierMixin from '../../mixins/notifier'; export default Ember.Component.extend(NotifierMixin, { display: 'section', // which CSS to use + hasTemplates: false, + + didReceiveAttrs() { + console.log(this.get('templates.length')); + this.set('hasTemplates', this.get('templates.length') > 0); + }, didRender() { let self = this; @@ -34,7 +40,11 @@ export default Ember.Component.extend(NotifierMixin, { }, addSection(section) { - this.attrs.onAction(section); + this.attrs.onAddSection(section); + }, + + insertTemplate(template) { + this.attrs.onInsertTemplate(template); } } }); 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/models/page-template.js b/app/app/models/page-template.js new file mode 100644 index 00000000..fb7bea4c --- /dev/null +++ b/app/app/models/page-template.js @@ -0,0 +1,28 @@ +// 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({ + documentId: attr('string'), + orgId: attr('string'), + contentType: attr('string'), + pageType: attr('string'), + preset: attr('boolean', { defaultValue: false }), + presetId: attr('string'), + title: attr('string'), + body: attr('string'), + 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..5a568a71 100644 --- a/app/app/models/page.js +++ b/app/app/models/page.js @@ -22,6 +22,8 @@ export default Model.extend({ level: attr('number', { defaultValue: 1 }), sequence: attr('number', { defaultValue: 0 }), revisions: attr('number', { defaultValue: 0 }), + preset: attr('boolean', { defaultValue: false }), + presetId: attr('string'), title: attr('string'), body: attr('string'), rawBody: attr('string'), diff --git a/app/app/pods/document/controller.js b/app/app/pods/document/controller.js index 0a5b840b..fd87e367 100644 --- a/app/app/pods/document/controller.js +++ b/app/app/pods/document/controller.js @@ -142,6 +142,51 @@ export default Ember.Controller.extend(NotifierMixin, { }); }, + onInsertTemplate(template) { + this.audit.record("added-section-template-" + template.get('contentType')); + + let page = { + documentId: this.get('model.document.id'), + title: `${template.get('title')}`, + level: 1, + sequence: 0, + body: template.get('body'), + contentType: template.get('contentType'), + pageType: template.get('pageType'), + presetId: template.get('id') + }; + + let meta = { + documentId: this.get('model.document.id'), + rawBody: "", + config: "" + }; + + 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); + }); + }); + }); + }, + 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..8a061996 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,12 @@ export default Ember.Controller.extend(NotifierMixin, { }); }, + onSaveAsPage(params) { + this.get('sectionService').saveSectionTemplate(params).then(() => { + this.showNotification("Published"); + }); + }, + 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..5788a63f 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') onSaveAsPage=(action 'onSaveAsPage') onDeletePage=(action 'onPageDeleted')}} diff --git a/app/app/pods/document/template.hbs b/app/app/pods/document/template.hbs index 233afefe..57900141 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') onInsertTemplate=(action 'onInsertTemplate') changePageSequence=(action 'onPageSequenceChange') changePageLevel=(action 'onPageLevelChange') gotoPage=(action 'gotoPage')}} {{/layout/zone-sidebar}} {{#layout/zone-content}} diff --git a/app/app/services/document.js b/app/app/services/document.js index 1554494e..53499202 100644 --- a/app/app/services/document.js +++ b/app/app/services/document.js @@ -296,7 +296,7 @@ export default Ember.Service.extend({ return this.get('ajax').request(`documents/${documentId}/attachments/${attachmentId}`, { method: 'DELETE' }); - }, + } }); function isObject(a) { diff --git a/app/app/services/section.js b/app/app/services/section.js index fdac0d35..1a4bea32 100644 --- a/app/app/services/section.js +++ b/app/app/services/section.js @@ -68,5 +68,35 @@ export default BaseService.extend({ return pages; }); + }, + + /****************************** + * Reusable section blocks + ******************************/ + + // Saves section as template + saveSectionTemplate(payload) { + let url = `sections/templates`; + + return this.get('ajax').post(url, { + data: JSON.stringify(payload), + contentType: 'json' + }); + }, + + // Returns all available sections. + getSpaceSectionTemplates(folderId) { + return this.get('ajax').request(`sections/templates/${folderId}`, { + method: 'GET' + }).then((response) => { + let data = []; + + data = response.map((obj) => { + let data = this.get('store').normalize('pageTemplate', obj); + return this.get('store').push(data); + }); + + return data; + }); } }); diff --git a/app/app/styles/view/document/content.scss b/app/app/styles/view/document/content.scss index 4d0cbbdb..d2c23b9e 100644 --- a/app/app/styles/view/document/content.scss +++ b/app/app/styles/view/document/content.scss @@ -41,7 +41,7 @@ > .is-a-page { .page-title { > .page-toolbar { - opacity: 0.3; + opacity: 0.5; @extend .transition-all; &:hover { diff --git a/app/app/styles/view/document/wizard.scss b/app/app/styles/view/document/wizard.scss index d49aae6a..128bb7fa 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; 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/document-sidebar.hbs b/app/app/templates/components/document/document-sidebar.hbs index ab750ae1..8ddf2ea4 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 templates=templates + onCancel=(action 'onCancel') onAddSection=(action 'onAddSection') onInsertTemplate=(action 'onInsertTemplate')}} {{/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..e622f790 100644 --- a/app/app/templates/components/document/document-view.hbs +++ b/app/app/templates/components/document/document-view.hbs @@ -17,7 +17,7 @@ {{#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 onSaveAsPage=(action 'onSaveAsPage') 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..cda77c65 100644 --- a/app/app/templates/components/document/page-heading.hbs +++ b/app/app/templates/components/document/page-heading.hbs @@ -3,20 +3,46 @@
    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