mirror of
https://github.com/documize/community.git
synced 2025-07-24 15:49:44 +02:00
Merge pull request #80 from documize/content-presets
Content refactoring: copy, move, reuse
This commit is contained in:
commit
44844011f6
45 changed files with 2294 additions and 816 deletions
53
app/app/components/document/block-editor.js
Normal file
53
app/app/components/document/block-editor.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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'));
|
||||
},
|
||||
|
||||
|
|
32
app/app/models/block.js
Normal file
32
app/app/models/block.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
|
||||
//
|
||||
// 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()
|
||||
});
|
|
@ -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'),
|
||||
|
|
47
app/app/pods/document/block/controller.js
Normal file
47
app/app/pods/document/block/controller.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
|
||||
//
|
||||
// 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')
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
31
app/app/pods/document/block/route.js
Normal file
31
app/app/pods/document/block/route.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
|
||||
//
|
||||
// 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),
|
||||
});
|
||||
}
|
||||
});
|
1
app/app/pods/document/block/template.hbs
Normal file
1
app/app/pods/document/block/template.hbs
Normal file
|
@ -0,0 +1 @@
|
|||
{{document/block-editor document=model.document folder=model.folder block=model.block onCancel=(action 'onCancel') onAction=(action 'onAction')}}
|
|
@ -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");
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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')}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -50,6 +50,9 @@ export default Router.map(function () {
|
|||
this.route('history', {
|
||||
path: 'history'
|
||||
});
|
||||
this.route('block', {
|
||||
path: 'block/:block_id'
|
||||
});
|
||||
});
|
||||
|
||||
this.route('customize', {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
1
app/app/templates/components/document/block-editor.hbs
Normal file
1
app/app/templates/components/document/block-editor.hbs
Normal file
|
@ -0,0 +1 @@
|
|||
{{component editorType document=document folder=folder page=page meta=meta onCancel=(action 'onCancel') onAction=(action 'onAction')}}
|
|
@ -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}}
|
||||
</div>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
|
||||
{{#if isEditor}}
|
||||
<li class="divider"></li>
|
||||
<li class="item" id="delete-document-button">Delete</li>
|
||||
<li class="item danger" id="delete-document-button">Delete</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{/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" }}
|
||||
<div>
|
||||
<div class="input-control">
|
||||
<label>Pin Document</label>
|
||||
<div class="tip">A 3 or 4 character name</div>
|
||||
{{input type='text' id="pin-document-name" value=pinState.newName}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Pin Document</label>
|
||||
<div class="tip">A 3 or 4 character name</div>
|
||||
{{input type='text' id="pin-document-name" value=pinState.newName}}
|
||||
</div>
|
||||
{{/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" }}
|
||||
<div>
|
||||
<div class="input-control">
|
||||
<label>Name</label>
|
||||
<div class="tip">Short name for this type of document</div>
|
||||
{{input type='text' id="new-template-name" value=saveTemplate.name}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Excerpt</label>
|
||||
<div class="tip">Explain use case for this template</div>
|
||||
{{textarea value=saveTemplate.description rows="3" id="new-template-desc"}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Name</label>
|
||||
<div class="tip">Short name for this type of document</div>
|
||||
{{input type='text' id="new-template-name" value=saveTemplate.name}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Excerpt</label>
|
||||
<div class="tip">Explain use case for this template</div>
|
||||
{{textarea value=saveTemplate.description rows="3" id="new-template-desc"}}
|
||||
</div>
|
||||
{{/dropdown-dialog}}
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
{{#each pages key="id" as |page index|}}
|
||||
<div class="wysiwyg">
|
||||
<div id="page-{{ page.id }}" class="is-a-page" data-id="{{ page.id }}" data-type="{{ page.contentType }}">
|
||||
{{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}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,20 +3,75 @@
|
|||
<div id="page-toolbar-{{ page.id }}" class="pull-right page-toolbar hidden-xs hidden-sm">
|
||||
{{#if isEditor}}
|
||||
{{#link-to 'document.edit' folder.id folder.slug document.id document.slug page.id}}
|
||||
<div class="round-button-mono page-edit-button" data-tooltip="Edit" data-tooltip-position="top center">
|
||||
<i class="material-icons">mode_edit</i>
|
||||
<div class="round-button-mono page-action-button" data-tooltip="Edit" data-tooltip-position="top center">
|
||||
<i class="material-icons color-gray">mode_edit</i>
|
||||
</div>
|
||||
{{/link-to}}
|
||||
<div id="delete-page-button-{{page.id}}" class="round-button-mono page-delete-button" data-tooltip="Delete" data-tooltip-position="top center">
|
||||
<i class="material-icons">delete</i>
|
||||
|
||||
<div id="page-menu-{{page.id}}" class="round-button-mono page-action-button" data-tooltip="More options" data-tooltip-position="top center">
|
||||
<i class="material-icons color-gray">more_vert</i>
|
||||
</div>
|
||||
{{#dropdown-dialog target=dropTarget position="top right" button="Delete" color="flat-red" onAction=(action 'deletePage' page.id)}}
|
||||
<p>Are you sure you want to delete <span class="bold">{{page.title}}?</span></p>
|
||||
<p>
|
||||
{{input type="checkbox" id=checkId class="margin-left-20" checked=deleteChildren}}
|
||||
<label for="{{checkId}}"> Delete child pages</label>
|
||||
</p>
|
||||
{{/dropdown-dialog}}
|
||||
|
||||
{{#dropdown-menu target=menuTarget position="bottom right" open="click" onOpenCallback=(action 'onMenuOpen') onCloseCallback=(action 'onMenuOpen')}}
|
||||
<ul class="menu">
|
||||
<li class="item" id={{copyButtonId}}>Copy</li>
|
||||
<li class="item" id={{moveButtonId}}>Move</li>
|
||||
<li class="item" id={{publishButtonId}}>Publish</li>
|
||||
<li class="divider"></li>
|
||||
<li class="item danger" id={{deleteButtonId}}>Delete</li>
|
||||
</ul>
|
||||
{{/dropdown-menu}}
|
||||
|
||||
{{#if menuOpen}}
|
||||
{{#dropdown-dialog target=deleteButtonId position="bottom right" button="Delete" color="flat-red" onAction=(action 'deletePage' page.id)}}
|
||||
<p>Are you sure you want to delete <span class="bold">{{page.title}}?</span></p>
|
||||
<p>
|
||||
{{input type="checkbox" id=checkId class="margin-left-20" checked=deleteChildren}}
|
||||
<label for="{{checkId}}"> Delete child pages</label>
|
||||
</p>
|
||||
{{/dropdown-dialog}}
|
||||
{{#dropdown-dialog id=publishDialogId target=publishButtonId position="bottom right" button="Publish" color="flat-green" focusOn=blockTitleId onAction=(action 'onAddBlock' page)}}
|
||||
<div class="form-header">
|
||||
<div class="tip">
|
||||
<span class="bold">{{folder.name}}:</span> Content Block
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Name</label>
|
||||
<div class="tip">Short title for reusable content block</div>
|
||||
{{input type="text" value=blockTitle id=blockTitleId}}
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Name</label>
|
||||
<div class="tip">Short description to help others understand<br/>the reusable content block</div>
|
||||
{{textarea rows="3" value=blockExcerpt id=blockExcerptId}}
|
||||
</div>
|
||||
{{/dropdown-dialog}}
|
||||
{{#dropdown-dialog id=copyDialogId target=copyButtonId position="bottom right" button="Copy" color="flat-green" onOpenCallback=(action 'onCopyDialogOpen') onAction=(action 'onCopyPage' page)}}
|
||||
<div class="form-header">
|
||||
<div class="tip">
|
||||
<span class="bold">Copy:</span> {{page.title}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Target</label>
|
||||
<div class="tip">Select where the content should be copied to</div>
|
||||
{{ui-select content=documentList action=(action 'onTargetChange') optionValuePath="id" optionLabelPath="name" selection=document}}
|
||||
</div>
|
||||
{{/dropdown-dialog}}
|
||||
{{#dropdown-dialog id=moveDialogId target=moveButtonId position="bottom right" button="Move" color="flat-green" onOpenCallback=(action 'onCopyDialogOpen') onAction=(action 'onMovePage' page)}}
|
||||
<div class="form-header">
|
||||
<div class="tip">
|
||||
<span class="bold">move:</span> {{page.title}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-control">
|
||||
<label>Target</label>
|
||||
<div class="tip">Select where the content should be moved to</div>
|
||||
{{ui-select content=documentListOthers action=(action 'onTargetChange') optionValuePath="id" optionLabelPath="name"}}
|
||||
</div>
|
||||
{{/dropdown-dialog}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,5 +19,42 @@
|
|||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if hasBlocks}}
|
||||
<div class="divider"></div>
|
||||
<div class="template-caption">Reusable content</div>
|
||||
<ul class="list">
|
||||
{{#each blocks as |block|}}
|
||||
<li class="item">
|
||||
<div class="icon" {{action 'onInsertBlock' block}}>
|
||||
<img class="img" src="/assets/img/section-saved.png" srcset="/assets/img/section-saved@2x.png" />
|
||||
</div>
|
||||
<div class="details" {{action 'onInsertBlock' block}}>
|
||||
<div class='title'>
|
||||
{{block.title}}
|
||||
</div>
|
||||
<div class='desc'>{{block.excerpt}}</div>
|
||||
<div class='desc'>By {{block.firstname}} {{block.lastname}}, {{time-ago block.created}} (used: {{ block.used }})</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{{#link-to 'document.block' folder.id folder.slug document.id document.slug block.id}}
|
||||
<i class="material-icons">mode_edit</i>
|
||||
{{/link-to}}
|
||||
<div class="divider"></div>
|
||||
<i class="material-icons" id={{block.deleteId}}>delete</i>
|
||||
</div>
|
||||
<div class="clearfix" />
|
||||
{{#dropdown-dialog target=block.deleteId position="top right" button="Delete" color="flat-red" onAction=(action 'onDeleteBlock' block.id)}}
|
||||
<p>
|
||||
Are you sure you want to delete block<br/>
|
||||
<span class="bold">{{block.title}}?</span>
|
||||
</p>
|
||||
{{/dropdown-dialog}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<div class="divider"></div>
|
||||
<div class="template-caption">Reusable content appears below</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,15 @@
|
|||
<div class="tip">{{tip}}</div>
|
||||
{{focus-input type='text' id="page-title" value=page.title class="mousetrap"}}
|
||||
</div>
|
||||
{{#if hasExcerpt}}
|
||||
<div class="margin-top-30">
|
||||
<div class="input-control">
|
||||
<label>Excerpt</label>
|
||||
<div class="tip">Short description</div>
|
||||
{{textarea rows="3" id="page-excerpt" value=page.excerpt class="mousetrap"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="buttons pull-right">
|
||||
{{#if busy}}
|
||||
|
|
BIN
app/public/assets/img/section-saved.png
Normal file
BIN
app/public/assets/img/section-saved.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 329 B |
BIN
app/public/assets/img/section-saved@2x.png
Normal file
BIN
app/public/assets/img/section-saved@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 774 B |
139
app/vendor/tether.js
vendored
139
app/vendor/tether.js
vendored
|
@ -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 <frame> offsets
|
||||
// if the element lies within a nested document (<frame> or <iframe>-like).
|
||||
function getActualBoundingClientRect(node) {
|
||||
var boundingRect = node.getBoundingClientRect();
|
||||
|
||||
// The original object returned by getBoundingClientRect is immutable, so we clone it
|
||||
// We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9
|
||||
var rect = {};
|
||||
for (var k in boundingRect) {
|
||||
rect[k] = boundingRect[k];
|
||||
}
|
||||
|
||||
if (node.ownerDocument !== document) {
|
||||
var _frameElement = node.ownerDocument.defaultView.frameElement;
|
||||
if (_frameElement) {
|
||||
var frameRect = getActualBoundingClientRect(_frameElement);
|
||||
rect.top += frameRect.top;
|
||||
rect.bottom += frameRect.top;
|
||||
rect.left += frameRect.left;
|
||||
rect.right += frameRect.left;
|
||||
}
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
function getScrollParents(el) {
|
||||
// In firefox if the el is inside an iframe with display: none; window.getComputedStyle() will return null;
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
||||
|
@ -51,14 +77,20 @@ function getScrollParents(el) {
|
|||
var overflowX = _style.overflowX;
|
||||
var overflowY = _style.overflowY;
|
||||
|
||||
if (/(auto|scroll)/.test(overflow + overflowY + overflowX)) {
|
||||
if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {
|
||||
if (position !== 'absolute' || ['relative', 'absolute', 'fixed'].indexOf(style.position) >= 0) {
|
||||
parents.push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parents.push(document.body);
|
||||
parents.push(el.ownerDocument.body);
|
||||
|
||||
// If the node is within a frame, account for the parent window scroll
|
||||
if (el.ownerDocument !== document) {
|
||||
parents.push(el.ownerDocument.defaultView);
|
||||
}
|
||||
|
||||
return parents;
|
||||
}
|
||||
|
||||
|
@ -76,7 +108,7 @@ var getOrigin = function getOrigin() {
|
|||
// are equivilant or not. We place an element at the top left of the page that will
|
||||
// get the same jitter, so we can cancel the two out.
|
||||
var node = zeroElement;
|
||||
if (!node) {
|
||||
if (!node || !document.body.contains(node)) {
|
||||
node = document.createElement('div');
|
||||
node.setAttribute('data-tether-id', uniqueId());
|
||||
extend(node.style, {
|
||||
|
@ -92,13 +124,7 @@ var getOrigin = function getOrigin() {
|
|||
|
||||
var id = node.getAttribute('data-tether-id');
|
||||
if (typeof zeroPosCache[id] === 'undefined') {
|
||||
zeroPosCache[id] = {};
|
||||
|
||||
var rect = node.getBoundingClientRect();
|
||||
for (var k in rect) {
|
||||
// Can't use extend, as on IE9, elements don't resolve to be hasOwnProperty
|
||||
zeroPosCache[id][k] = rect[k];
|
||||
}
|
||||
zeroPosCache[id] = getActualBoundingClientRect(node);
|
||||
|
||||
// Clear the cache when this position call is done
|
||||
defer(function () {
|
||||
|
@ -127,13 +153,7 @@ function getBounds(el) {
|
|||
|
||||
var docEl = doc.documentElement;
|
||||
|
||||
var box = {};
|
||||
// The original object returned by getBoundingClientRect is immutable, so we clone it
|
||||
// We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9
|
||||
var rect = el.getBoundingClientRect();
|
||||
for (var k in rect) {
|
||||
box[k] = rect[k];
|
||||
}
|
||||
var box = getActualBoundingClientRect(el);
|
||||
|
||||
var origin = getOrigin();
|
||||
|
||||
|
@ -159,7 +179,11 @@ function getOffsetParent(el) {
|
|||
return el.offsetParent || document.documentElement;
|
||||
}
|
||||
|
||||
var _scrollBarSize = null;
|
||||
function getScrollBarSize() {
|
||||
if (_scrollBarSize) {
|
||||
return _scrollBarSize;
|
||||
}
|
||||
var inner = document.createElement('div');
|
||||
inner.style.width = '100%';
|
||||
inner.style.height = '200px';
|
||||
|
@ -192,7 +216,8 @@ function getScrollBarSize() {
|
|||
|
||||
var width = widthContained - widthScroll;
|
||||
|
||||
return { width: width, height: width };
|
||||
_scrollBarSize = { width: width, height: width };
|
||||
return _scrollBarSize;
|
||||
}
|
||||
|
||||
function extend() {
|
||||
|
@ -252,7 +277,9 @@ function hasClass(el, name) {
|
|||
}
|
||||
|
||||
function getClassName(el) {
|
||||
if (el.className instanceof SVGAnimatedString) {
|
||||
// Can't use just SVGAnimatedString here since nodes within a Frame in IE have
|
||||
// completely separately SVGAnimatedString base classes
|
||||
if (el.className instanceof el.ownerDocument.defaultView.SVGAnimatedString) {
|
||||
return el.className.baseVal;
|
||||
}
|
||||
return el.className;
|
||||
|
@ -371,6 +398,7 @@ var Evented = (function () {
|
|||
})();
|
||||
|
||||
TetherBase.Utils = {
|
||||
getActualBoundingClientRect: getActualBoundingClientRect,
|
||||
getScrollParents: getScrollParents,
|
||||
getBounds: getBounds,
|
||||
getOffsetParent: getOffsetParent,
|
||||
|
@ -429,7 +457,7 @@ var transformKey = (function () {
|
|||
}
|
||||
var el = document.createElement('div');
|
||||
|
||||
var transforms = ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform'];
|
||||
var transforms = ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform'];
|
||||
for (var i = 0; i < transforms.length; ++i) {
|
||||
var key = transforms[i];
|
||||
if (el.style[key] !== undefined) {
|
||||
|
@ -828,7 +856,7 @@ var TetherClass = (function (_Evented) {
|
|||
this.enabled = true;
|
||||
|
||||
this.scrollParents.forEach(function (parent) {
|
||||
if (parent !== document) {
|
||||
if (parent !== _this3.target.ownerDocument) {
|
||||
parent.addEventListener('scroll', _this3.position);
|
||||
}
|
||||
});
|
||||
|
@ -1028,21 +1056,24 @@ var TetherClass = (function (_Evented) {
|
|||
}
|
||||
};
|
||||
|
||||
var doc = this.target.ownerDocument;
|
||||
var win = doc.defaultView;
|
||||
|
||||
var scrollbarSize = undefined;
|
||||
if (document.body.scrollWidth > window.innerWidth) {
|
||||
if (win.innerHeight > doc.documentElement.clientHeight) {
|
||||
scrollbarSize = this.cache('scrollbar-size', getScrollBarSize);
|
||||
next.viewport.bottom -= scrollbarSize.height;
|
||||
}
|
||||
|
||||
if (document.body.scrollHeight > window.innerHeight) {
|
||||
if (win.innerWidth > doc.documentElement.clientWidth) {
|
||||
scrollbarSize = this.cache('scrollbar-size', getScrollBarSize);
|
||||
next.viewport.right -= scrollbarSize.width;
|
||||
}
|
||||
|
||||
if (['', 'static'].indexOf(document.body.style.position) === -1 || ['', 'static'].indexOf(document.body.parentElement.style.position) === -1) {
|
||||
if (['', 'static'].indexOf(doc.body.style.position) === -1 || ['', 'static'].indexOf(doc.body.parentElement.style.position) === -1) {
|
||||
// Absolute positioning in the body will be relative to the page, not the 'initial containing block'
|
||||
next.page.bottom = document.body.scrollHeight - top - height;
|
||||
next.page.right = document.body.scrollWidth - left - width;
|
||||
next.page.bottom = doc.body.scrollHeight - top - height;
|
||||
next.page.right = doc.body.scrollWidth - left - width;
|
||||
}
|
||||
|
||||
if (typeof this.options.optimizations !== 'undefined' && this.options.optimizations.moveElement !== false && !(typeof this.targetModifier !== 'undefined')) {
|
||||
|
@ -1061,8 +1092,8 @@ var TetherClass = (function (_Evented) {
|
|||
offsetBorder[side.toLowerCase()] = parseFloat(offsetParentStyle['border' + side + 'Width']);
|
||||
});
|
||||
|
||||
offsetPosition.right = document.body.scrollWidth - offsetPosition.left - offsetParentSize.width + offsetBorder.right;
|
||||
offsetPosition.bottom = document.body.scrollHeight - offsetPosition.top - offsetParentSize.height + offsetBorder.bottom;
|
||||
offsetPosition.right = doc.body.scrollWidth - offsetPosition.left - offsetParentSize.width + offsetBorder.right;
|
||||
offsetPosition.bottom = doc.body.scrollHeight - offsetPosition.top - offsetParentSize.height + offsetBorder.bottom;
|
||||
|
||||
if (next.page.top >= offsetPosition.top + offsetBorder.top && next.page.bottom >= offsetPosition.bottom) {
|
||||
if (next.page.left >= offsetPosition.left + offsetBorder.left && next.page.right >= offsetPosition.right) {
|
||||
|
@ -1155,7 +1186,16 @@ var TetherClass = (function (_Evented) {
|
|||
xPos = -_pos.right;
|
||||
}
|
||||
|
||||
css[transformKey] = 'translateX(' + Math.round(xPos) + 'px) translateY(' + Math.round(yPos) + 'px)';
|
||||
if (window.matchMedia) {
|
||||
// HubSpot/tether#207
|
||||
var retina = window.matchMedia('only screen and (min-resolution: 1.3dppx)').matches || window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 1.3)').matches;
|
||||
if (!retina) {
|
||||
xPos = Math.round(xPos);
|
||||
yPos = Math.round(yPos);
|
||||
}
|
||||
}
|
||||
|
||||
css[transformKey] = 'translateX(' + xPos + 'px) translateY(' + yPos + 'px)';
|
||||
|
||||
if (transformKey !== 'msTransform') {
|
||||
// The Z transform will keep this in the GPU (faster, and prevents artifacts),
|
||||
|
@ -1207,20 +1247,24 @@ var TetherClass = (function (_Evented) {
|
|||
}
|
||||
|
||||
if (!moved) {
|
||||
var offsetParentIsBody = true;
|
||||
var currentNode = this.element.parentNode;
|
||||
while (currentNode && currentNode.nodeType === 1 && currentNode.tagName !== 'BODY') {
|
||||
if (getComputedStyle(currentNode).position !== 'static') {
|
||||
offsetParentIsBody = false;
|
||||
break;
|
||||
if (this.options.bodyElement) {
|
||||
this.options.bodyElement.appendChild(this.element);
|
||||
} else {
|
||||
var offsetParentIsBody = true;
|
||||
var currentNode = this.element.parentNode;
|
||||
while (currentNode && currentNode.nodeType === 1 && currentNode.tagName !== 'BODY') {
|
||||
if (getComputedStyle(currentNode).position !== 'static') {
|
||||
offsetParentIsBody = false;
|
||||
break;
|
||||
}
|
||||
|
||||
currentNode = currentNode.parentNode;
|
||||
}
|
||||
|
||||
currentNode = currentNode.parentNode;
|
||||
}
|
||||
|
||||
if (!offsetParentIsBody) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
document.body.appendChild(this.element);
|
||||
if (!offsetParentIsBody) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
this.element.ownerDocument.body.appendChild(this.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1240,6 +1284,7 @@ var TetherClass = (function (_Evented) {
|
|||
if (write) {
|
||||
defer(function () {
|
||||
extend(_this8.element.style, writeCSS);
|
||||
_this8.trigger('repositioned');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1280,12 +1325,22 @@ function getBoundingRect(tether, to) {
|
|||
|
||||
if (typeof to.nodeType !== 'undefined') {
|
||||
(function () {
|
||||
var node = to;
|
||||
var size = getBounds(to);
|
||||
var pos = size;
|
||||
var style = getComputedStyle(to);
|
||||
|
||||
to = [pos.left, pos.top, size.width + pos.left, size.height + pos.top];
|
||||
|
||||
// Account any parent Frames scroll offset
|
||||
if (node.ownerDocument !== document) {
|
||||
var win = node.ownerDocument.defaultView;
|
||||
to[0] += win.pageXOffset;
|
||||
to[1] += win.pageYOffset;
|
||||
to[2] += win.pageXOffset;
|
||||
to[3] += win.pageYOffset;
|
||||
}
|
||||
|
||||
BOUNDS_FORMAT.forEach(function (side, i) {
|
||||
side = side[0].toUpperCase() + side.substr(1);
|
||||
if (side === 'Top' || side === 'Left') {
|
||||
|
|
|
@ -78,14 +78,13 @@ func AddDocumentPage(w http.ResponseWriter, r *http.Request) {
|
|||
pageID := util.UniqueID()
|
||||
model.Page.RefID = pageID
|
||||
model.Meta.PageID = pageID
|
||||
model.Meta.OrgID = p.Context.OrgID // required for Render call below
|
||||
model.Meta.UserID = p.Context.UserID // required for Render call below
|
||||
model.Page.SetDefaults()
|
||||
model.Meta.SetDefaults()
|
||||
model.Meta.OrgID = p.Context.OrgID
|
||||
model.Meta.UserID = p.Context.UserID
|
||||
// page.Title = template.HTMLEscapeString(page.Title)
|
||||
|
||||
tx, err := request.Db.Beginx()
|
||||
|
||||
if err != nil {
|
||||
writeTransactionError(w, method, err)
|
||||
return
|
||||
|
@ -93,8 +92,7 @@ func AddDocumentPage(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
p.Context.Transaction = tx
|
||||
|
||||
output, ok := provider.Render(model.Page.ContentType,
|
||||
provider.NewContext(model.Meta.OrgID, model.Meta.UserID), model.Meta.Config, model.Meta.RawBody)
|
||||
output, ok := provider.Render(model.Page.ContentType, provider.NewContext(model.Meta.OrgID, model.Meta.UserID), model.Meta.Config, model.Meta.RawBody)
|
||||
if !ok {
|
||||
log.ErrorString("provider.Render could not find: " + model.Page.ContentType)
|
||||
}
|
||||
|
@ -102,16 +100,19 @@ func AddDocumentPage(w http.ResponseWriter, r *http.Request) {
|
|||
model.Page.Body = output
|
||||
|
||||
err = p.AddPage(*model)
|
||||
|
||||
if err != nil {
|
||||
log.IfErr(tx.Rollback())
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(model.Page.BlockID) > 0 {
|
||||
p.IncrementBlockUsage(model.Page.BlockID)
|
||||
}
|
||||
|
||||
log.IfErr(tx.Commit())
|
||||
|
||||
newPage, err := p.GetPage(pageID)
|
||||
newPage, _ := p.GetPage(pageID)
|
||||
|
||||
json, err := json.Marshal(newPage)
|
||||
|
||||
|
@ -307,6 +308,17 @@ func DeleteDocumentPage(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
p.Context.Transaction = tx
|
||||
|
||||
page, err := p.GetPage(pageID)
|
||||
if err != nil {
|
||||
log.IfErr(tx.Rollback())
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(page.BlockID) > 0 {
|
||||
p.DecrementBlockUsage(page.BlockID)
|
||||
}
|
||||
|
||||
_, err = p.DeletePage(documentID, pageID)
|
||||
|
||||
if err != nil {
|
||||
|
@ -364,8 +376,18 @@ func DeleteDocumentPages(w http.ResponseWriter, r *http.Request) {
|
|||
p.Context.Transaction = tx
|
||||
|
||||
for _, page := range *model {
|
||||
_, err = p.DeletePage(documentID, page.PageID)
|
||||
pageData, err := p.GetPage(page.PageID)
|
||||
if err != nil {
|
||||
log.IfErr(tx.Rollback())
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -647,9 +669,38 @@ func GetDocumentPageMeta(w http.ResponseWriter, r *http.Request) {
|
|||
writeSuccessBytes(w, json)
|
||||
}
|
||||
|
||||
/********************
|
||||
* Page Revisions
|
||||
********************/
|
||||
// GetPageMoveCopyTargets returns available documents for page copy/move axction.
|
||||
func GetPageMoveCopyTargets(w http.ResponseWriter, r *http.Request) {
|
||||
method := "GetPageMoveCopyTargets"
|
||||
p := request.GetPersister(r)
|
||||
|
||||
var d []entity.Document
|
||||
var err error
|
||||
|
||||
d, err = p.GetDocumentList()
|
||||
|
||||
if len(d) == 0 {
|
||||
d = []entity.Document{}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
json, err := json.Marshal(d)
|
||||
|
||||
if err != nil {
|
||||
writeJSONMarshalError(w, method, "document", err)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessBytes(w, json)
|
||||
}
|
||||
|
||||
//**************************************************
|
||||
// Page Revisions
|
||||
//**************************************************
|
||||
|
||||
// GetDocumentRevisions returns all changes for a document.
|
||||
func GetDocumentRevisions(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -669,7 +720,7 @@ func GetDocumentRevisions(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
revisions, err := p.GetDocumentRevisions(documentID)
|
||||
revisions, _ := p.GetDocumentRevisions(documentID)
|
||||
|
||||
payload, err := json.Marshal(revisions)
|
||||
|
||||
|
@ -706,7 +757,7 @@ func GetDocumentPageRevisions(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
revisions, err := p.GetPageRevisions(pageID)
|
||||
revisions, _ := p.GetPageRevisions(pageID)
|
||||
|
||||
payload, err := json.Marshal(revisions)
|
||||
|
||||
|
@ -757,7 +808,7 @@ func GetDocumentPageDiff(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
revision, err := p.GetPageRevision(revisionID)
|
||||
revision, _ := p.GetPageRevision(revisionID)
|
||||
|
||||
latestHTML := page.Body
|
||||
previousHTML := revision.Body
|
||||
|
@ -875,3 +926,102 @@ func RollbackDocumentPage(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
writeSuccessBytes(w, payload)
|
||||
}
|
||||
|
||||
//**************************************************
|
||||
// Copy Move Page
|
||||
//**************************************************
|
||||
|
||||
// CopyPage copies page to either same or different document.
|
||||
func CopyPage(w http.ResponseWriter, r *http.Request) {
|
||||
method := "CopyPage"
|
||||
p := request.GetPersister(r)
|
||||
|
||||
params := mux.Vars(r)
|
||||
documentID := params["documentID"]
|
||||
pageID := params["pageID"]
|
||||
targetID := params["targetID"]
|
||||
|
||||
// data checks
|
||||
if len(documentID) == 0 {
|
||||
writeMissingDataError(w, method, "documentID")
|
||||
return
|
||||
}
|
||||
if len(pageID) == 0 {
|
||||
writeMissingDataError(w, method, "pageID")
|
||||
return
|
||||
}
|
||||
if len(targetID) == 0 {
|
||||
writeMissingDataError(w, method, "targetID")
|
||||
return
|
||||
}
|
||||
|
||||
// permission
|
||||
if !p.CanViewDocument(documentID) {
|
||||
writeForbiddenError(w)
|
||||
return
|
||||
}
|
||||
|
||||
// fetch data
|
||||
page, err := p.GetPage(pageID)
|
||||
if err == sql.ErrNoRows {
|
||||
writeNotFoundError(w, method, documentID)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
pageMeta, err := p.GetPageMeta(pageID)
|
||||
if err == sql.ErrNoRows {
|
||||
writeNotFoundError(w, method, documentID)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
newPageID := util.UniqueID()
|
||||
page.RefID = newPageID
|
||||
page.Level = 1
|
||||
page.DocumentID = targetID
|
||||
page.UserID = p.Context.UserID
|
||||
pageMeta.DocumentID = targetID
|
||||
pageMeta.PageID = newPageID
|
||||
pageMeta.UserID = p.Context.UserID
|
||||
|
||||
model := new(models.PageModel)
|
||||
model.Meta = pageMeta
|
||||
model.Page = page
|
||||
|
||||
tx, err := request.Db.Beginx()
|
||||
if err != nil {
|
||||
writeTransactionError(w, method, err)
|
||||
return
|
||||
}
|
||||
p.Context.Transaction = tx
|
||||
|
||||
err = p.AddPage(*model)
|
||||
if err != nil {
|
||||
log.IfErr(tx.Rollback())
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(model.Page.BlockID) > 0 {
|
||||
p.IncrementBlockUsage(model.Page.BlockID)
|
||||
}
|
||||
|
||||
log.IfErr(tx.Commit())
|
||||
|
||||
newPage, _ := p.GetPage(pageID)
|
||||
json, err := json.Marshal(newPage)
|
||||
|
||||
if err != nil {
|
||||
writeJSONMarshalError(w, method, "page", err)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessBytes(w, json)
|
||||
}
|
||||
|
|
|
@ -127,8 +127,9 @@ func buildRoutes(prefix string) *mux.Router {
|
|||
}
|
||||
|
||||
func init() {
|
||||
|
||||
// **** add Unsecure Routes
|
||||
//**************************************************
|
||||
// Non-secure routes
|
||||
//**************************************************
|
||||
|
||||
log.IfErr(Add(RoutePrefixPublic, "meta", []string{"GET", "OPTIONS"}, nil, GetMeta))
|
||||
log.IfErr(Add(RoutePrefixPublic, "authenticate", []string{"POST", "OPTIONS"}, nil, Authenticate))
|
||||
|
@ -139,7 +140,9 @@ func init() {
|
|||
log.IfErr(Add(RoutePrefixPublic, "attachments/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, AttachmentDownload))
|
||||
log.IfErr(Add(RoutePrefixPublic, "version", []string{"GET", "OPTIONS"}, nil, version))
|
||||
|
||||
// **** add secure routes
|
||||
//**************************************************
|
||||
// Secure routes
|
||||
//**************************************************
|
||||
|
||||
// Import & Convert Document
|
||||
log.IfErr(Add(RoutePrefixPrivate, "import/folder/{folderID}", []string{"POST", "OPTIONS"}, nil, UploadConvertDocument))
|
||||
|
@ -151,8 +154,6 @@ func init() {
|
|||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}", []string{"GET", "OPTIONS"}, nil, GetDocument))
|
||||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}", []string{"PUT", "OPTIONS"}, nil, UpdateDocument))
|
||||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}", []string{"DELETE", "OPTIONS"}, nil, DeleteDocument))
|
||||
|
||||
// Document Meta
|
||||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/meta", []string{"GET", "OPTIONS"}, nil, GetDocumentMeta))
|
||||
|
||||
// Document Page
|
||||
|
@ -173,9 +174,8 @@ func init() {
|
|||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"GET", "OPTIONS"}, nil, GetAttachments))
|
||||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/attachments/{attachmentID}", []string{"DELETE", "OPTIONS"}, nil, DeleteAttachment))
|
||||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"POST", "OPTIONS"}, nil, AddAttachments))
|
||||
|
||||
// Document Page Meta
|
||||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/meta", []string{"GET", "OPTIONS"}, nil, GetDocumentPageMeta))
|
||||
log.IfErr(Add(RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/copy/{targetID}", []string{"POST", "OPTIONS"}, nil, CopyPage))
|
||||
|
||||
// Organization
|
||||
log.IfErr(Add(RoutePrefixPrivate, "organizations/{orgID}", []string{"GET", "OPTIONS"}, nil, GetOrganization))
|
||||
|
@ -216,6 +216,12 @@ 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/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/{blockID}", []string{"DELETE", "OPTIONS"}, nil, DeleteBlock))
|
||||
log.IfErr(Add(RoutePrefixPrivate, "sections/blocks", []string{"POST", "OPTIONS"}, nil, AddBlock))
|
||||
log.IfErr(Add(RoutePrefixPrivate, "sections/targets", []string{"GET", "OPTIONS"}, nil, GetPageMoveCopyTargets))
|
||||
|
||||
// Links
|
||||
log.IfErr(Add(RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates))
|
||||
|
|
|
@ -13,6 +13,7 @@ package endpoint
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/documize/community/core/api/entity"
|
||||
|
@ -20,6 +21,8 @@ import (
|
|||
"github.com/documize/community/core/api/util"
|
||||
"github.com/documize/community/core/log"
|
||||
"github.com/documize/community/core/section/provider"
|
||||
"github.com/documize/community/core/utility"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// GetSections returns available smart sections.
|
||||
|
@ -76,8 +79,7 @@ func RunSectionCommand(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// RefreshSections updates document sections where the data
|
||||
// is externally sourced.
|
||||
// RefreshSections updates document sections where the data is externally sourced.
|
||||
func RefreshSections(w http.ResponseWriter, r *http.Request) {
|
||||
method := "RefreshSections"
|
||||
p := request.GetPersister(r)
|
||||
|
@ -176,3 +178,214 @@ func RefreshSections(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
writeSuccessBytes(w, json)
|
||||
}
|
||||
|
||||
/**************************************************
|
||||
* Reusable Content Blocks
|
||||
**************************************************/
|
||||
|
||||
// AddBlock inserts new reusable content block into database.
|
||||
func AddBlock(w http.ResponseWriter, r *http.Request) {
|
||||
method := "AddBlock"
|
||||
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
|
||||
}
|
||||
|
||||
b.RefID = util.UniqueID()
|
||||
|
||||
tx, err := request.Db.Beginx()
|
||||
if err != nil {
|
||||
writeTransactionError(w, method, err)
|
||||
return
|
||||
}
|
||||
p.Context.Transaction = tx
|
||||
|
||||
err = p.AddBlock(b)
|
||||
if err != nil {
|
||||
log.IfErr(tx.Rollback())
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.IfErr(tx.Commit())
|
||||
|
||||
b, err = p.GetBlock(b.RefID)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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"
|
||||
p := request.GetPersister(r)
|
||||
|
||||
params := mux.Vars(r)
|
||||
folderID := params["folderID"]
|
||||
|
||||
if len(folderID) == 0 {
|
||||
writeMissingDataError(w, method, "folderID")
|
||||
return
|
||||
}
|
||||
|
||||
var b []entity.Block
|
||||
var err error
|
||||
|
||||
b, err = p.GetBlocksForSpace(folderID)
|
||||
|
||||
if len(b) == 0 {
|
||||
b = []entity.Block{}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// DeleteBlock removes requested reusable content block.
|
||||
func DeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
method := "DeleteBlock"
|
||||
p := request.GetPersister(r)
|
||||
|
||||
params := mux.Vars(r)
|
||||
blockID := params["blockID"]
|
||||
|
||||
if len(blockID) == 0 {
|
||||
writeMissingDataError(w, method, "blockID")
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := request.Db.Beginx()
|
||||
if err != nil {
|
||||
writeTransactionError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
p.Context.Transaction = tx
|
||||
|
||||
_, err = p.DeleteBlock(blockID)
|
||||
if err != nil {
|
||||
log.IfErr(tx.Rollback())
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = p.RemoveBlockReference(blockID)
|
||||
if err != nil {
|
||||
log.IfErr(tx.Rollback())
|
||||
writeGeneralSQLError(w, method, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.IfErr(tx.Commit())
|
||||
|
||||
writeSuccessEmptyJSON(w)
|
||||
}
|
||||
|
|
|
@ -186,6 +186,7 @@ type Page struct {
|
|||
UserID string `json:"userId"`
|
||||
ContentType string `json:"contentType"`
|
||||
PageType string `json:"pageType"`
|
||||
BlockID string `json:"blockId"`
|
||||
Level uint64 `json:"level"`
|
||||
Sequence float64 `json:"sequence"`
|
||||
Title string `json:"title"`
|
||||
|
@ -259,6 +260,25 @@ type Revision struct {
|
|||
Revisions int `json:"revisions"`
|
||||
}
|
||||
|
||||
// Block represents a section that has been published as a reusable content block.
|
||||
type Block struct {
|
||||
BaseEntity
|
||||
OrgID string `json:"orgId"`
|
||||
LabelID string `json:"folderId"`
|
||||
UserID string `json:"userId"`
|
||||
ContentType string `json:"contentType"`
|
||||
PageType string `json:"pageType"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
RawBody string `json:"rawBody"` // a blob of data
|
||||
Config string `json:"config"` // JSON based custom config for this type
|
||||
ExternalSource bool `json:"externalSource"` // true indicates data sourced externally
|
||||
Used uint64 `json:"used"`
|
||||
Firstname string `json:"firstname"`
|
||||
Lastname string `json:"lastname"`
|
||||
}
|
||||
|
||||
// DocumentMeta details who viewed the document.
|
||||
type DocumentMeta struct {
|
||||
Viewers []DocumentMetaViewer `json:"viewers"`
|
||||
|
|
166
core/api/request/block.go
Normal file
166
core/api/request/block.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
|
||||
//
|
||||
// https://documize.com
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/documize/community/core/api/entity"
|
||||
"github.com/documize/community/core/log"
|
||||
"github.com/documize/community/core/utility"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// AddBlock saves reusable content block.
|
||||
func (p *Persister) AddBlock(b entity.Block) (err error) {
|
||||
b.OrgID = p.Context.OrgID
|
||||
b.UserID = p.Context.UserID
|
||||
b.Created = time.Now().UTC()
|
||||
b.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := p.Context.Transaction.Preparex("INSERT INTO block (refid, orgid, labelid, userid, contenttype, pagetype, title, body, excerpt, rawbody, config, externalsource, used, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Unable to prepare insert AddBlock", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(b.RefID, b.OrgID, b.LabelID, b.UserID, b.ContentType, b.PageType, b.Title, b.Body, b.Excerpt, b.RawBody, b.Config, b.ExternalSource, b.Used, b.Created, b.Revised)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Unable to execute insert AddBlock", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetBlock returns requested reusable content block.
|
||||
func (p *Persister) GetBlock(id string) (b entity.Block, err error) {
|
||||
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 {
|
||||
log.Error(fmt.Sprintf("Unable to prepare select GetBlock %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = stmt.Get(&b, p.Context.OrgID, id)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute select GetBlock %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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 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)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// IncrementBlockUsage increments usage counter for content block.
|
||||
func (p *Persister) IncrementBlockUsage(id string) (err error) {
|
||||
stmt, err := p.Context.Transaction.Preparex("UPDATE block SET used=used+1, revised=? WHERE orgid=? AND refid=?")
|
||||
defer utility.Close(stmt)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to prepare update IncrementBlockUsage id %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(time.Now().UTC(), p.Context.OrgID, id)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute IncrementBlockUsage id %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DecrementBlockUsage decrements usage counter for content block.
|
||||
func (p *Persister) DecrementBlockUsage(id string) (err error) {
|
||||
stmt, err := p.Context.Transaction.Preparex("UPDATE block SET used=used-1, revised=? WHERE orgid=? AND refid=?")
|
||||
defer utility.Close(stmt)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to prepare update DecrementBlockUsage id %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(time.Now().UTC(), p.Context.OrgID, id)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute DecrementBlockUsage id %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveBlockReference clears page.blockid for given blockID.
|
||||
func (p *Persister) RemoveBlockReference(id string) (err error) {
|
||||
stmt, err := p.Context.Transaction.Preparex("UPDATE page SET blockid='', revised=? WHERE orgid=? AND blockid=?")
|
||||
defer utility.Close(stmt)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to prepare update RemoveBlockReference id %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(time.Now().UTC(), p.Context.OrgID, id)
|
||||
|
||||
// skip no rows affected
|
||||
if err == sql.ErrNoRows {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute RemoveBlockReference id %s", id), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateBlock updates existing reusable content block item.
|
||||
func (p *Persister) UpdateBlock(b entity.Block) (err error) {
|
||||
b.Revised = time.Now().UTC()
|
||||
|
||||
var stmt *sqlx.NamedStmt
|
||||
stmt, err = p.Context.Transaction.PrepareNamed("UPDATE block SET title=:title, body=:body, excerpt=:excerpt, rawbody=:rawbody, config=:config, revised=:revised WHERE orgid=:orgid AND refid=:refid")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to prepare update UpdateBlock %s", b.RefID), err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&b)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute update UpdateBlock %s", b.RefID), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteBlock removes reusable content block from database.
|
||||
func (p *Persister) DeleteBlock(id string) (rows int64, err error) {
|
||||
return p.Base.DeleteConstrained(p.Context.Transaction, "block", p.Context.OrgID, id)
|
||||
}
|
|
@ -212,6 +212,31 @@ AND d.template=0`, orgID)
|
|||
return
|
||||
}
|
||||
|
||||
// GetDocumentList returns a slice containing the documents available as templates to the client's organisation, in title order.
|
||||
func (p *Persister) GetDocumentList() (documents []entity.Document, err error) {
|
||||
err = Db.Select(&documents,
|
||||
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, layout, created, revised FROM document WHERE orgid=? AND template=0 AND labelid IN
|
||||
(SELECT refid from label WHERE orgid=? AND type=2 AND userid=?
|
||||
UNION ALL SELECT refid FROM label a where orgid=? AND type=1 AND refid IN (SELECT labelid from labelrole WHERE orgid=? AND userid='' AND (canedit=1 OR canview=1))
|
||||
UNION ALL SELECT refid FROM label a where orgid=? AND type=3 AND refid IN (SELECT labelid from labelrole WHERE orgid=? AND userid=? AND (canedit=1 OR canview=1)))
|
||||
ORDER BY title`,
|
||||
p.Context.OrgID,
|
||||
p.Context.OrgID,
|
||||
p.Context.UserID,
|
||||
p.Context.OrgID,
|
||||
p.Context.OrgID,
|
||||
p.Context.OrgID,
|
||||
p.Context.OrgID,
|
||||
p.Context.UserID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute GetDocumentList org %s", p.Context.OrgID), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SearchDocument searches the documents that the client is allowed to see, using the keywords search string, then audits that search.
|
||||
// Visible documents include both those in the client's own organisation and those that are public, or whose visibility includes the client.
|
||||
func (p *Persister) SearchDocument(keywords string) (results []entity.DocumentSearch, err error) {
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
|
||||
// AddPage inserts the given page into the page table, adds that page to the queue of pages to index and audits that the page has been added.
|
||||
func (p *Persister) AddPage(model models.PageModel) (err error) {
|
||||
err = nil
|
||||
model.Page.OrgID = p.Context.OrgID
|
||||
model.Page.UserID = p.Context.UserID
|
||||
model.Page.Created = time.Now().UTC()
|
||||
|
@ -51,7 +50,7 @@ func (p *Persister) AddPage(model models.PageModel) (err error) {
|
|||
model.Page.Sequence = maxSeq * 2
|
||||
}
|
||||
|
||||
stmt, err := p.Context.Transaction.Preparex("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
stmt, err := p.Context.Transaction.Preparex("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
|
@ -59,14 +58,14 @@ func (p *Persister) AddPage(model models.PageModel) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.Created, model.Page.Revised)
|
||||
_, err = stmt.Exec(model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.BlockID, model.Page.Created, model.Page.Revised)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Unable to execute insert for page", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = searches.Add(&databaseRequest{OrgID: p.Context.OrgID}, model.Page, model.Page.RefID)
|
||||
_ = searches.Add(&databaseRequest{OrgID: p.Context.OrgID}, model.Page, model.Page.RefID)
|
||||
|
||||
stmt2, err := p.Context.Transaction.Preparex("INSERT INTO pagemeta (pageid, orgid, userid, documentid, rawbody, config, externalsource, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
defer utility.Close(stmt2)
|
||||
|
@ -90,9 +89,7 @@ func (p *Persister) AddPage(model models.PageModel) (err error) {
|
|||
|
||||
// GetPage returns the pageID page record from the page table.
|
||||
func (p *Persister) GetPage(pageID string) (page entity.Page, err error) {
|
||||
err = nil
|
||||
|
||||
stmt, err := Db.Preparex("SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?")
|
||||
stmt, err := Db.Preparex("SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
|
@ -112,9 +109,7 @@ func (p *Persister) GetPage(pageID string) (page entity.Page, err error) {
|
|||
|
||||
// GetPages returns a slice containing all the page records for a given documentID, in presentation sequence.
|
||||
func (p *Persister) GetPages(documentID string) (pages []entity.Page, err error) {
|
||||
err = nil
|
||||
|
||||
err = Db.Select(&pages, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", p.Context.OrgID, documentID)
|
||||
err = Db.Select(&pages, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", p.Context.OrgID, documentID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute select pages for org %s and document %s", p.Context.OrgID, documentID), err)
|
||||
|
@ -127,11 +122,9 @@ func (p *Persister) GetPages(documentID string) (pages []entity.Page, err error)
|
|||
// GetPagesWhereIn returns a slice, in presentation sequence, containing those page records for a given documentID
|
||||
// where their refid is in the comma-separated list passed as inPages.
|
||||
func (p *Persister) GetPagesWhereIn(documentID, inPages string) (pages []entity.Page, err error) {
|
||||
err = nil
|
||||
|
||||
args := []interface{}{p.Context.OrgID, documentID}
|
||||
tempValues := strings.Split(inPages, ",")
|
||||
sql := "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND a.refid IN (?" + strings.Repeat(",?", len(tempValues)-1) + ") ORDER BY sequence"
|
||||
sql := "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.blockid, a.revisions, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND a.refid IN (?" + strings.Repeat(",?", len(tempValues)-1) + ") ORDER BY sequence"
|
||||
|
||||
inValues := make([]interface{}, len(tempValues))
|
||||
|
||||
|
@ -181,7 +174,7 @@ func (p *Persister) GetPagesWhereIn(documentID, inPages string) (pages []entity.
|
|||
// GetPagesWithoutContent returns a slice containing all the page records for a given documentID, in presentation sequence,
|
||||
// but without the body field (which holds the HTML content).
|
||||
func (p *Persister) GetPagesWithoutContent(documentID string) (pages []entity.Page, err error) {
|
||||
err = Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", p.Context.OrgID, documentID)
|
||||
err = Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, blockid, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", p.Context.OrgID, documentID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute select pages for org %s and document %s", p.Context.OrgID, documentID), err)
|
||||
|
@ -194,7 +187,6 @@ func (p *Persister) GetPagesWithoutContent(documentID string) (pages []entity.Pa
|
|||
// UpdatePage saves changes to the database and handles recording of revisions.
|
||||
// Not all updates result in a revision being recorded hence the parameter.
|
||||
func (p *Persister) UpdatePage(page entity.Page, refID, userID string, skipRevision bool) (err error) {
|
||||
err = nil
|
||||
page.Revised = time.Now().UTC()
|
||||
|
||||
// Store revision history
|
||||
|
@ -304,7 +296,6 @@ func (p *Persister) UpdatePage(page entity.Page, refID, userID string, skipRevis
|
|||
|
||||
// UpdatePageMeta persists meta information associated with a document page.
|
||||
func (p *Persister) UpdatePageMeta(meta entity.PageMeta, updateUserID bool) (err error) {
|
||||
err = nil
|
||||
meta.Revised = time.Now().UTC()
|
||||
|
||||
if updateUserID {
|
||||
|
@ -386,17 +377,17 @@ func (p *Persister) DeletePage(documentID, pageID string) (rows int64, err error
|
|||
rows, err = p.Base.DeleteConstrained(p.Context.Transaction, "page", p.Context.OrgID, pageID)
|
||||
|
||||
if err == nil {
|
||||
_, err = p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", p.Context.OrgID, pageID))
|
||||
_, err = searches.Delete(&databaseRequest{OrgID: p.Context.OrgID}, documentID, pageID)
|
||||
_, _ = p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", p.Context.OrgID, pageID))
|
||||
_, _ = searches.Delete(&databaseRequest{OrgID: p.Context.OrgID}, documentID, pageID)
|
||||
|
||||
// delete content links from this page
|
||||
_, err = p.DeleteSourcePageLinks(pageID)
|
||||
_, _ = p.DeleteSourcePageLinks(pageID)
|
||||
|
||||
// mark as orphan links to this page
|
||||
err = p.MarkOrphanPageLink(pageID)
|
||||
_ = p.MarkOrphanPageLink(pageID)
|
||||
|
||||
// nuke revisions
|
||||
_, err = p.DeletePageRevisions(pageID)
|
||||
_, _ = p.DeletePageRevisions(pageID)
|
||||
|
||||
p.Base.Audit(p.Context, "remove-page", documentID, pageID)
|
||||
}
|
||||
|
@ -406,8 +397,6 @@ func (p *Persister) DeletePage(documentID, pageID string) (rows int64, err error
|
|||
|
||||
// GetPageMeta returns the meta information associated with the page.
|
||||
func (p *Persister) GetPageMeta(pageID string) (meta entity.PageMeta, err error) {
|
||||
err = nil
|
||||
|
||||
stmt, err := Db.Preparex("SELECT id, pageid, orgid, userid, documentid, rawbody, coalesce(config,JSON_UNQUOTE('{}')) as config, externalsource, created, revised FROM pagemeta WHERE orgid=? AND pageid=?")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
|
@ -428,7 +417,6 @@ func (p *Persister) GetPageMeta(pageID string) (meta entity.PageMeta, err error)
|
|||
|
||||
// GetDocumentPageMeta returns the meta information associated with a document.
|
||||
func (p *Persister) GetDocumentPageMeta(documentID string, externalSourceOnly bool) (meta []entity.PageMeta, err error) {
|
||||
err = nil
|
||||
filter := ""
|
||||
if externalSourceOnly {
|
||||
filter = " AND externalsource=1"
|
||||
|
@ -508,3 +496,17 @@ func (p *Persister) DeletePageRevisions(pageID string) (rows int64, err error) {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// GetNextPageSequence returns the next sequence numbner to use for a page in given document.
|
||||
func (p *Persister) GetNextPageSequence(documentID string) (maxSeq float64, err error) {
|
||||
row := Db.QueryRow("SELECT max(sequence) FROM page WHERE orgid=? AND documentid=? AND pagetype='section'", p.Context.OrgID, documentID)
|
||||
|
||||
err = row.Scan(&maxSeq)
|
||||
if err != nil {
|
||||
maxSeq = 2048
|
||||
}
|
||||
|
||||
maxSeq = maxSeq * 2
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -56,8 +56,6 @@ func (p *Persister) AddPin(pin entity.Pin) (err error) {
|
|||
|
||||
// GetPin returns requested pinned item.
|
||||
func (p *Persister) GetPin(id string) (pin entity.Pin, err error) {
|
||||
err = nil
|
||||
|
||||
stmt, err := Db.Preparex("SELECT id, refid, orgid, userid, labelid as folderid, documentid, pin, sequence, created, revised FROM pin WHERE orgid=? AND refid=?")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
|
@ -90,7 +88,6 @@ func (p *Persister) GetUserPins(userID string) (pins []entity.Pin, err error) {
|
|||
|
||||
// UpdatePin updates existing pinned item.
|
||||
func (p *Persister) UpdatePin(pin entity.Pin) (err error) {
|
||||
err = nil
|
||||
pin.Revised = time.Now().UTC()
|
||||
|
||||
var stmt *sqlx.NamedStmt
|
||||
|
@ -114,8 +111,6 @@ func (p *Persister) UpdatePin(pin entity.Pin) (err error) {
|
|||
|
||||
// UpdatePinSequence updates existing pinned item sequence number
|
||||
func (p *Persister) UpdatePinSequence(pinID string, sequence int) (err error) {
|
||||
err = nil
|
||||
|
||||
stmt, err := p.Context.Transaction.Preparex("UPDATE pin SET sequence=?, revised=? WHERE orgid=? AND userid=? AND refid=?")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
|
|
31
core/database/scripts/autobuild/db_00009.sql
Normal file
31
core/database/scripts/autobuild/db_00009.sql
Normal file
|
@ -0,0 +1,31 @@
|
|||
/* community edition */
|
||||
DROP TABLE IF EXISTS `block`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `block` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`refid` CHAR(16) NOT NULL COLLATE utf8_bin,
|
||||
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
|
||||
`labelid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
|
||||
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
|
||||
`contenttype` CHAR(20) NOT NULL DEFAULT 'wysiwyg',
|
||||
`pagetype` CHAR(10) NOT NULL DEFAULT 'section',
|
||||
`title` NVARCHAR(2000) NOT NULL,
|
||||
`body` LONGTEXT,
|
||||
`excerpt` NVARCHAR(2000) NOT NULL,
|
||||
`used` INT UNSIGNED NOT NULL,
|
||||
`rawbody` LONGBLOB,
|
||||
`config` JSON,
|
||||
`externalsource` BOOL DEFAULT 0,
|
||||
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT pk_id PRIMARY KEY (id),
|
||||
INDEX `idx_block_refid` (`refid` ASC),
|
||||
INDEX `idx_block_labelid` (`labelid` ASC))
|
||||
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
|
||||
ENGINE = InnoDB;
|
||||
|
||||
ALTER TABLE page ADD COLUMN `blockid` CHAR(16) NOT NULL DEFAULT '' COLLATE utf8_bin AFTER `pagetype`;
|
||||
/* Note: version history table does not need blockid field as they are populated once during page creation:
|
||||
- you cannot mark an existing section as a preset
|
||||
- a page is only marked as preset during it's creation (e.g. created from an existing preset)
|
||||
*/
|
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue