1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-24 07:39:43 +02:00

Merge pull request #52 from documize/content-links

Content links
This commit is contained in:
Harvey Kandola 2016-10-29 16:28:09 -07:00 committed by GitHub
commit aad1ffb063
54 changed files with 2496 additions and 984 deletions

View file

@ -1,49 +1,49 @@
{
"predef": [
"server",
"document",
"window",
"-Promise",
"moment",
"$",
"_",
"is",
"Mousetrap",
"CodeMirror",
"Intercom",
"Materialize",
"tinymce",
"Tether",
"Tooltip",
"Drop",
"Dropzone",
"dragula",
"datetimepicker",
"Waypoint"
],
"browser": true,
"boss": true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true,
"unused": true
"predef": [
"server",
"document",
"window",
"-Promise",
"moment",
"$",
"_",
"is",
"Mousetrap",
"CodeMirror",
"Intercom",
"Materialize",
"tinymce",
"Tether",
"Tooltip",
"Drop",
"Dropzone",
"dragula",
"datetimepicker",
"Waypoint"
],
"browser": true,
"boss": true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true,
"unused": true
}

View file

@ -17,6 +17,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
documentService: Ember.inject.service('document'),
sectionService: Ember.inject.service('section'),
appMeta: Ember.inject.service(),
link: Ember.inject.service(),
/* Parameters */
document: null,
// pages: [],
@ -50,6 +51,10 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
});
},
didRender() {
this.contentLinkHandler();
},
willDestroyElement() {
this.destroyTooltips();
@ -60,6 +65,39 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
}
},
contentLinkHandler() {
let links = this.get('link');
let doc = this.get('document');
let self = this;
$("a[data-documize='true']").off('click').on('click', function(e) {
let link = links.getLinkObject(self.get('meta.outboundLinks'), this);
// local link? exists?
if (link.linkType === "section" && link.documentId === doc.get('id')) {
let exists = self.get('pages').findBy('id', link.targetId);
if (_.isUndefined(exists)) {
link.orphan = true;
} else {
self.attrs.gotoPage(link.targetId);
return false;
}
}
if (link.orphan) {
$(this).addClass('broken-link');
self.showNotification('Broken link!');
e.preventDefault();
e.stopPropagation();
return false;
}
links.linkClick(doc, link);
return false;
});
},
actions: {
confirmDeleteAttachment(id, name) {
this.set('deleteAttachment', {

View file

@ -0,0 +1,133 @@
// 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 TooltipMixin from '../../mixins/tooltip';
const {
inject: { service }
} = Ember;
export default Ember.Component.extend(TooltipMixin, {
link: service(),
linkName: '',
keywords: '',
selection: null,
matches: {
documents: [],
pages: [],
attachments: []
},
tabs: [
{ label: 'Section', selected: true },
{ label: 'Attachment', selected: false },
{ label: 'Search', selected: false }
],
showSections: Ember.computed('tabs.@each.selected', function () {
return this.get('tabs').findBy('label', 'Section').selected;
}),
showAttachments: Ember.computed('tabs.@each.selected', function () {
return this.get('tabs').findBy('label', 'Attachment').selected;
}),
showSearch: Ember.computed('tabs.@each.selected', function () {
return this.get('tabs').findBy('label', 'Search').selected;
}),
hasMatches: Ember.computed('matches', function () {
let m = this.get('matches');
return m.documents.length || m.pages.length || m.attachments.length;
}),
init() {
this._super(...arguments);
let self = this;
let folderId = this.get('folder.id');
let documentId = this.get('document.id');
let pageId = this.get('page.id');
this.get('link').getCandidates(folderId, documentId, pageId).then(function (candidates) {
self.set('candidates', candidates);
self.set('hasSections', is.not.null(candidates.pages) && candidates.pages.length);
self.set('hasAttachments', is.not.null(candidates.attachments) && candidates.attachments.length);
});
},
didRender() {
this.addTooltip(document.getElementById("content-linker-button"));
this.addTooltip(document.getElementById("content-counter-button"));
},
willDestroyElement() {
this.destroyTooltips();
},
onKeywordChange: function () {
Ember.run.debounce(this, this.fetch, 750);
}.observes('keywords'),
fetch() {
let keywords = this.get('keywords');
let self = this;
if (_.isEmpty(keywords)) {
this.set('matches', { documents: [], pages: [], attachments: [] });
return;
}
this.get('link').searchCandidates(keywords).then(function (matches) {
self.set('matches', matches);
});
},
actions: {
setSelection(i) {
let candidates = this.get('candidates');
let matches = this.get('matches');
this.set('selection', i);
candidates.pages.forEach(c => {
Ember.set(c, 'selected', c.id === i.id);
});
candidates.attachments.forEach(c => {
Ember.set(c, 'selected', c.id === i.id);
});
matches.documents.forEach(c => {
Ember.set(c, 'selected', c.id === i.id);
});
matches.pages.forEach(c => {
Ember.set(c, 'selected', c.id === i.id);
});
matches.attachments.forEach(c => {
Ember.set(c, 'selected', c.id === i.id);
});
},
onInsertLink() {
let selection = this.get('selection');
if (is.null(selection)) {
return;
}
return this.get('onInsertLink')(selection);
},
onTabSelect(tabs) {
this.set('tabs', tabs);
}
}
});

View file

@ -14,67 +14,63 @@ import NotifierMixin from '../../mixins/notifier';
import TooltipMixin from '../../mixins/tooltip';
const {
computed
computed
} = Ember;
export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
folderService: Ember.inject.service('folder'),
documentService: Ember.inject.service('document'),
session: Ember.inject.service(),
documentService: Ember.inject.service('document'),
session: Ember.inject.service(),
appMeta: Ember.inject.service(),
showToolbar: false,
folder: {},
busy: false,
importedDocuments: [],
isFolderOwner: computed.equal('folder.userId', 'session.user.id'),
moveFolderId: "",
folder: {},
busy: false,
importedDocuments: [],
isFolderOwner: computed.equal('folder.userId', 'session.user.id'),
moveFolderId: "",
drop: null,
didReceiveAttrs() {
this.set('isFolderOwner', this.get('folder.userId') === this.get("session.user.id"));
didReceiveAttrs() {
this.set('isFolderOwner', this.get('folder.userId') === this.get("session.user.id"));
let show = this.get('isFolderOwner') || this.get('hasSelectedDocuments') || this.get('folderService').get('canEditCurrentFolder');
this.set('showToolbar', show);
let targets = _.reject(this.get('folders'), {
id: this.get('folder').get('id')
});
let targets = _.reject(this.get('folders'), {
id: this.get('folder').get('id')
});
this.set('movedFolderOptions', targets);
},
this.set('movedFolderOptions', targets);
},
didRender() {
if (this.get('hasSelectedDocuments')) {
this.addTooltip(document.getElementById("move-documents-button"));
this.addTooltip(document.getElementById("delete-documents-button"));
} else {
if (this.get('isFolderOwner')) {
this.addTooltip(document.getElementById("folder-share-button"));
this.addTooltip(document.getElementById("folder-settings-button"));
}
if (this.get('folderService').get('canEditCurrentFolder')) {
if (this.get('hasSelectedDocuments')) {
this.addTooltip(document.getElementById("move-documents-button"));
this.addTooltip(document.getElementById("delete-documents-button"));
} else {
if (this.get('isFolderOwner')) {
this.addTooltip(document.getElementById("folder-share-button"));
this.addTooltip(document.getElementById("folder-settings-button"));
}
if (this.get('folderService').get('canEditCurrentFolder')) {
this.addTooltip(document.getElementById("import-document-button"));
}
}
}
}
},
didUpdate() {
this.setupImport();
},
didInsertElement() {
// this.setupImport();
},
willDestroyElement() {
willDestroyElement() {
if (is.not.null(this.get('drop'))) {
this.get('drop').destroy();
this.set('drop', null);
}
this.destroyTooltips();
},
this.destroyTooltips();
},
setupImport() {
// guard against unecessary file upload component init
@ -135,52 +131,52 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
this.set('drop', dzone);
},
actions: {
onDocumentImporting(filename) {
this.send("showNotification", `Importing ${filename}`);
actions: {
onDocumentImporting(filename) {
this.send("showNotification", `Importing ${filename}`);
let documents = this.get('importedDocuments');
documents.push(filename);
this.set('importedDocuments', documents);
},
let documents = this.get('importedDocuments');
documents.push(filename);
this.set('importedDocuments', documents);
},
onDocumentImported(filename /*, document*/ ) {
this.send("showNotification", `${filename} ready`);
onDocumentImported(filename /*, document*/ ) {
this.send("showNotification", `${filename} ready`);
let documents = this.get('importedDocuments');
documents.pop(filename);
this.set('importedDocuments', documents);
let documents = this.get('importedDocuments');
documents.pop(filename);
this.set('importedDocuments', documents);
this.attrs.refresh();
this.attrs.refresh();
if (documents.length === 0) {
// this.get('showDocument')(this.get('folder'), document);
}
},
if (documents.length === 0) {
// this.get('showDocument')(this.get('folder'), document);
}
},
deleteDocuments() {
deleteDocuments() {
this.attrs.onDeleteDocument();
},
},
setMoveFolder(folderId) {
this.set('moveFolderId', folderId);
setMoveFolder(folderId) {
this.set('moveFolderId', folderId);
let folders = this.get('folders');
let folders = this.get('folders');
folders.forEach(folder => {
folder.set('selected', folder.id === folderId);
});
},
folders.forEach(folder => {
folder.set('selected', folder.id === folderId);
});
},
moveDocuments() {
if (this.get("moveFolderId") === "") {
return false;
}
moveDocuments() {
if (this.get("moveFolderId") === "") {
return false;
}
this.attrs.onMoveDocument(this.get('moveFolderId'));
this.set("moveFolderId", "");
this.attrs.onMoveDocument(this.get('moveFolderId'));
this.set("moveFolderId", "");
return true;
}
}
return true;
}
}
});

View file

@ -1,17 +1,24 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import miscUtil from '../../../utils/misc';
const {
inject: { service }
} = Ember;
export default Ember.Component.extend({
link: service(),
isDirty: false,
pageBody: "",
@ -45,6 +52,15 @@ export default Ember.Component.extend({
},
actions: {
onInsertLink(link) {
let linkMarkdown = this.get('link').buildLink(link);
miscUtil.insertAtCursor($("#section-markdown-editor")[0], linkMarkdown);
this.set('pageBody', $("#section-markdown-editor").val());
return true;
},
isDirty() {
return this.get('isDirty');
},
@ -62,4 +78,4 @@ export default Ember.Component.extend({
this.attrs.onAction(page, meta);
}
}
});
});

View file

@ -11,30 +11,37 @@
import Ember from 'ember';
const {
inject: { service }
} = Ember;
export default Ember.Component.extend({
appMeta: service(),
link: service(),
pageBody: "",
appMeta: Ember.inject.service(),
didReceiveAttrs() {
this.set('pageBody', this.get('meta.rawBody'));
},
didInsertElement() {
let maxHeight = $(document).height() - $(".document-editor > .toolbar").height() - 200;
let options = {
selector: "#rich-text-editor",
relative_urls: false,
cache_suffix: "?v=430",
cache_suffix: "?v=443",
browser_spellcheck: false,
gecko_spellcheck: false,
theme: "modern",
statusbar: false,
height: $(document).height() - $(".document-editor > .toolbar").height() - 200,
height: maxHeight,
entity_encoding: "raw",
paste_data_images: true,
image_advtab: true,
image_caption: true,
media_live_embeds: true,
fontsize_formats: "8pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 30pt 32pt 34pt 36pt",
fontsize_formats: "8px 10px 12px 14px 18px 24px 36px 40px 50px 60px",
formats: {
bold: {
inline: 'b'
@ -48,27 +55,12 @@ export default Ember.Component.extend({
'advlist autolink lists link image charmap print preview hr anchor pagebreak',
'searchreplace wordcount visualblocks visualchars code codesample fullscreen',
'insertdatetime media nonbreaking save table directionality',
'emoticons template paste textcolor colorpicker textpattern imagetools'
'template paste textcolor colorpicker textpattern imagetools'
],
menu: {
edit: {
title: 'Edit',
items: 'undo redo | cut copy paste pastetext | selectall | searchreplace'
},
insert: {
title: 'Insert',
items: 'anchor link media | hr | charmap emoticons | blockquote'
},
format: {
title: 'Format',
items: 'bold italic underline strikethrough superscript subscript | formats fonts | removeformat'
},
table: {
title: 'Table',
items: 'inserttable tableprops deletetable | cell row column'
}
},
toolbar1: "formatselect fontselect fontsizeselect | bold italic underline | link unlink | image media | codesample | outdent indent | alignleft aligncenter alignright alignjustify | bullist numlist | forecolor backcolor",
menu: {},
menubar: false,
toolbar1: "bold italic underline strikethrough superscript subscript | outdent indent bullist numlist forecolor backcolor | alignleft aligncenter alignright alignjustify | link unlink | table image media | hr codesample",
toolbar2: "formatselect fontselect fontsizeselect",
save_onsavecallback: function () {
Mousetrap.trigger('ctrl+s');
}
@ -91,6 +83,19 @@ export default Ember.Component.extend({
},
actions: {
onInsertLink(link) {
let userSelection = tinymce.activeEditor.selection.getContent();
if (is.not.empty(userSelection)) {
Ember.set(link, 'title', userSelection);
}
let linkHTML = this.get('link').buildLink(link);
tinymce.activeEditor.insertContent(linkHTML);
return true;
},
isDirty() {
return is.not.undefined(tinymce) && is.not.undefined(tinymce.activeEditor) && tinymce.activeEditor.isDirty();
},

View file

@ -0,0 +1,16 @@
// 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({
tagName: 'span'
});

View file

@ -0,0 +1,15 @@
// 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({
});

View 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 Ember from 'ember';
export default Ember.Component.extend({
myWidth: Ember.computed('tabs', function() {
let count = this.get('tabs.length');
let width = 95 / count;
return Ember.String.htmlSafe("width: " + `${width}%;`);
}),
actions: {
onTabSelect(tab) {
this.get('tabs').forEach(t => {
Ember.set(t, 'selected', false);
});
Ember.set(tab, 'selected', true);
this.attrs.onTabSelect(this.get('tabs'));
}
}
});

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
@ -18,6 +18,6 @@ export function initialize(application) {
export default {
name: 'eventBus',
after: 'session',
after: 'application',
initialize: initialize
};
};

View file

@ -0,0 +1,20 @@
// 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
export function initialize(application) {
application.inject('component', 'router', 'router:main');
application.inject('service', 'router', 'router:main');
}
export default {
name: 'route-injector',
initialize: initialize
};

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
@ -16,7 +16,6 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
documentService: Ember.inject.service('document'),
folderService: Ember.inject.service('folder'),
userService: Ember.inject.service('user'),
pages: [],
attachments: [],
users: [],
@ -107,14 +106,5 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
controller.set('meta', meta);
this.browser.setMetaDescription(model.get('excerpt'));
},
// Document view needs all white background!
activate() {
Ember.$('html').addClass('background-color-white');
},
deactivate() {
Ember.$('html').removeClass('background-color-white');
}
});
});

View file

@ -18,8 +18,9 @@
onAttachmentUpload=(action 'onAttachmentUpload')
onDocumentDelete=(action 'onDocumentDelete')}}
{{document/document-view document=model pages=pages attachments=attachments folder=folder folders=folders
{{document/document-view document=model meta=meta pages=pages attachments=attachments folder=folder folders=folders
isEditor=isEditor
gotoPage=(action 'gotoPage')
onAttachmentDeleted=(action 'onAttachmentDeleted')
onDeletePage=(action 'onPageDeleted')}}
{{/layout/zone-content}}

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
@ -19,4 +19,13 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
this.audit.record("viewed-document");
return this.get('documentService').getDocument(params.document_id);
},
});
actions: {
error(error /*, transition*/ ) {
if (error) {
this.transitionTo('/not-found');
return false;
}
}
}
});

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
@ -13,8 +13,7 @@ import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
beforeModel: function () {
beforeModel() {
this.transitionTo('folders');
}
});
});

View file

@ -101,5 +101,5 @@ export default Router.map(function () {
path: '/*wildcard'
});
this.route('pods', function () {});
// this.route('pods', function () {});
});

View file

@ -47,6 +47,7 @@ export default Ember.Service.extend({
allowAnonymousAccess: true,
setupMode: true
});
this.get('localStorage').clearAll();
return resolve(this);

View file

@ -25,6 +25,7 @@ export default Ember.Service.extend({
appId: config.APP.intercomKey,
init() {
this._super(...arguments);
this.start();
},

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
@ -28,5 +28,9 @@ export default Ember.Service.extend({
}(wait, times);
setTimeout(interv, wait);
}
});
},
showNotification(msg) {
this.get('eventBus').publish('notifyUser', msg);
}
});

View file

@ -13,6 +13,7 @@ import Ember from 'ember';
export default Ember.Service.extend(Ember.Evented, {
init() {
this._super(...arguments);
let _this = this;
window.addEventListener("scroll", _.throttle(function() {

131
app/app/services/link.js Normal file
View file

@ -0,0 +1,131 @@
// 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';
const {
inject: { service }
} = Ember;
export default Ember.Service.extend({
sessionService: service('session'),
ajax: service(),
appMeta: service(),
store: service(),
// Returns candidate links using provided parameters
getCandidates(folderId, documentId, pageId) {
return this.get('ajax').request(`links/${folderId}/${documentId}/${pageId}`, {
method: 'GET'
}).then((response) => {
return response;
});
},
// Returns keyword-based candidates
searchCandidates(keywords) {
let url = "links?keywords=" + encodeURIComponent(keywords);
return this.get('ajax').request(url, {
method: 'GET'
}).then((response) => {
return response;
});
},
// getUsers returns all users for organization.
find(keywords) {
let url = "search?keywords=" + encodeURIComponent(keywords);
return this.get('ajax').request(url, {
method: "GET"
});
},
buildLink(link) {
let result = "";
let href = "";
let endpoint = this.get('appMeta').get('endpoint');
let orgId = this.get('appMeta').get('orgId');
if (link.linkType === "section" || link.linkType === "document") {
href = `/link/${link.linkType}/${link.id}`;
result = `<a data-documize='true' data-link-space-id='${link.folderId}' data-link-id='${link.id}' data-link-target-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`;
}
if (link.linkType === "file") {
href = `${endpoint}/public/attachments/${orgId}/${link.targetId}`;
result = `<a data-documize='true' data-link-space-id='${link.folderId}' data-link-id='${link.id}' data-link-target-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`;
}
return result;
},
getLinkObject(outboundLinks, a) {
let link = {
linkId: a.attributes["data-link-id"].value,
linkType: a.attributes["data-link-type"].value,
documentId: a.attributes["data-link-target-document-id"].value,
folderId: a.attributes["data-link-space-id"].value,
targetId: a.attributes["data-link-target-id"].value,
url: a.attributes["href"].value,
orphan: false
};
link.orphan = _.isEmpty(link.linkId) || _.isEmpty(link.documentId) || _.isEmpty(link.folderId) || _.isEmpty(link.targetId);
// we check latest state of link using database data
let existing = outboundLinks.findBy('id', link.linkId);
if (_.isUndefined(existing)) {
link.orphan = true;
} else {
link.orphan = existing.orphan;
}
return link;
},
linkClick(doc, link) {
if (link.orphan) {
return;
}
let router = this.get('router');
let targetFolder = this.get('store').peekRecord('folder', link.folderId);
let targetDocument = this.get('store').peekRecord('document', link.documentId);
let folderSlug = is.null(targetFolder) ? "s" : targetFolder.get('slug');
let documentSlug = is.null(targetDocument) ? "d" : targetDocument.get('slug');
// handle section link
if (link.linkType === "section") {
let options = {};
options['page'] = link.targetId;
router.transitionTo('document', link.folderId, folderSlug, link.documentId, documentSlug, { queryParams: options });
return;
}
// handle document link
if (link.linkType === "document") {
router.transitionTo('document', link.folderId, folderSlug, link.documentId, documentSlug);
return;
}
// handle attachment links
if (link.linkType === "file") {
window.location.href = link.url;
return;
}
}
});
/*
doc meta to show inbound and outbound links.
*/

View file

@ -41,6 +41,8 @@ export default SimpleAuthSession.extend({
}),
init: function () {
this._super(...arguments);
this.set('isMac', is.mac());
this.set('isMobile', is.mobile());
},

View file

@ -8,7 +8,6 @@
// by contacting <sales@documize.com>.
//
// https://documize.com
@import "color.scss";
@import "font.scss";
@import "functions.scss";
@ -26,11 +25,11 @@
@import "view/document/wysiwyg.scss";
@import "view/document/editor.scss";
@import "view/document/wizard.scss";
@import "view/document/edit-tools.scss";
@import "view/common.scss";
@import "vendor.scss";
@import "responsive.scss";
@import "print.scss";
@import "section/trello.scss";
@import "section/gemini.scss";
@import "section/github.scss";

View file

@ -9,26 +9,100 @@
//
// https://documize.com
.cursor-pointer { cursor: pointer; }
.cursor-not-allowed { cursor: not-allowed !important; }
.cursor-auto { cursor: auto; }
.vertical-top { vertical-align: top; }
.inline-block { display: inline-block; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.center { margin: 0 auto; }
.bold { font-weight: bold; }
.italic { font-style: italic; }
.text-uppercase { text-transform: uppercase; }
.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } // requires element to specify width
.absolute-center { margin: auto; position: absolute; top: 0; left: 0; bottom: 0; right: 0; }
.no-width { white-space: nowrap; width: 1%; }
.no-float { float: none !important; }
.no-overflow-x { overflow-x: visible !important; }
input:-webkit-autofill { -webkit-box-shadow: 0 0 0px 1000px white inset; }
img.responsive-img, video.responsive-video { max-width: 100%; height: auto; }
.bordered { border: 1px solid $color-border; }
.cursor-pointer {
cursor: pointer;
}
.cursor-not-allowed {
cursor: not-allowed !important;
}
.cursor-auto {
cursor: auto;
}
.vertical-top {
vertical-align: top;
}
.inline-block {
display: inline-block;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.center {
margin: 0 auto;
}
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.text-uppercase {
text-transform: uppercase;
}
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// requires element to specify width
.absolute-center {
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.no-width {
white-space: nowrap;
width: 1%;
}
.no-float {
float: none !important;
}
.no-overflow-x {
overflow-x: visible !important;
}
.no-display {
display: none;
}
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0 1000px white inset;
}
img.responsive-img,
video.responsive-video {
max-width: 100%;
height: auto;
}
.bordered {
border: 1px solid $color-border;
}
html {
overflow-y: scroll;
@ -52,62 +126,97 @@ a {
-webkit-font-smoothing: antialiased;
text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
a:hover, a:focus {
a:focus,
a:hover {
text-decoration: none;
}
}
a.broken-link {
color: $color-red;
text-decoration: line-through;
}
a.alt {
color: $color-blue;
text-decoration: none;
cursor: pointer;
a:hover, a:focus {
a:focus,
a:hover {
text-decoration: underline;
}
}
$i: 150;
@while $i > 0 {
.margin-#{$i} { margin: #{$i}px; }
.margin-top-#{$i} { margin-top: #{$i}px; }
.margin-bottom-#{$i} { margin-bottom: #{$i}px; }
.margin-right-#{$i} { margin-right: #{$i}px; }
.margin-left-#{$i} { margin-left: #{$i}px; }
.margin-#{$i} {
margin: #{$i}px;
}
.margin-top-#{$i} {
margin-top: #{$i}px;
}
.margin-bottom-#{$i} {
margin-bottom: #{$i}px;
}
.margin-right-#{$i} {
margin-right: #{$i}px;
}
.margin-left-#{$i} {
margin-left: #{$i}px;
}
$i: $i - 5;
}
$i: 150;
@while $i > 0 {
.padding-#{$i} { padding: #{$i}px; }
.padding-top-#{$i} { padding-top: #{$i}px; }
.padding-bottom-#{$i} { padding-bottom: #{$i}px; }
.padding-right-#{$i} { padding-right: #{$i}px; }
.padding-left-#{$i} { padding-left: #{$i}px; }
.padding-#{$i} {
padding: #{$i}px;
}
.padding-top-#{$i} {
padding-top: #{$i}px;
}
.padding-bottom-#{$i} {
padding-bottom: #{$i}px;
}
.padding-right-#{$i} {
padding-right: #{$i}px;
}
.padding-left-#{$i} {
padding-left: #{$i}px;
}
$i: $i - 5;
}
$i: 100;
@while $i > 0 {
.width-#{$i} { width: #{$i}#{"%"}; }
$i: $i - 5;
.width-#{$i} {
width: #{$i}#{"%"};
}
$i: $i - 1;
}
.no-outline
{
outline:none !important;
border:none !important;
box-shadow:none !important;
.no-outline {
outline: none !important;
border: none !important;
box-shadow: none !important;
&:focus, &:active
{
border:none !important;
box-shadow:none !important;
&:active,
&:focus {
border: none !important;
box-shadow: none !important;
}
}
.no-select
{
.no-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
@ -127,11 +236,12 @@ ul {
}
}
.clearfix:before,
.clearfix:after {
.clearfix:after,
.clearfix:before {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}

View file

@ -0,0 +1,44 @@
.edit-tools {
margin: 0 0 0 20px;
min-height: 600px;
> .toolbar {
margin: 0;
padding: 0;
> .item {
list-style-type: none;
margin: 0 0 20px;
}
}
}
.content-counter-dialog {
width: 200px;
height: 200px;
}
.content-linker-dialog {
width: 350px;
height: 500px;
overflow-y: auto;
.link-list {
margin: 0;
padding: 0;
.link-item {
margin: 0;
padding: 0;
font-size: 0.9rem;
color: $color-gray;
cursor: pointer;
.icon {
margin-right: 5px;
height: 15px;
width: 15px;
}
}
}
}

View file

@ -0,0 +1,21 @@
.checkbox-option {
vertical-align: bottom;
cursor: pointer;
font-size: 0.9rem;
overflow: hidden;
white-space: nowrap;
> .material-icons {
font-size: 0.9rem;
vertical-align: top;
margin-right: 5px;
}
&:hover {
color: $color-link;
}
}
.checkbox-option-selected {
color: $color-link;
}

View file

@ -0,0 +1,34 @@
.widget-selection {
> .option {
width: 100%;
margin: 0;
padding: 5px 10px;
text-align: left;
@extend .no-select;
cursor: pointer;
// border: 1px solid $color-border;
color: $color-off-black;
position: relative;
> i.material-icons {
display: none;
}
}
&:hover {
@include ease-in();
background-color: $color-off-white;
}
> .selected {
background-color: $color-card-active !important;
color: $color-primary !important;
> i.material-icons {
display: inline-block;
position: absolute;
right: 10px;
top: 5px;
}
}
}

View file

@ -0,0 +1,29 @@
.widget-tab {
width: 100%;
margin: 0;
padding: 0 10px;
text-align: center;
@extend .no-select;
> .tab {
display: inline-block;
margin: 0;
padding: 5px 10px;
background-color: $color-off-white;
color: $color-gray;
text-align: center;
cursor: pointer;
margin-right: -3px;
&:hover {
@include ease-in();
background-color: $color-gray;
color: $color-off-white;
}
}
> .selected {
background-color: $color-gray;
color: $color-off-white;
}
}

View file

@ -2,62 +2,63 @@
// Material Design icons from https://design.google.com/icons/
.material-icons {
font-family : "Material Icons";
font-weight : normal;
font-style : normal;
font-size : 1.2rem;
display : inline-block;
text-transform : none;
letter-spacing : normal;
word-wrap : normal;
-webkit-font-smoothing : antialiased;
text-rendering : optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings : "liga";
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
font-size: 1.2rem;
display: inline-block;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "liga";
}
.transition-shadow {
transition: box-shadow .25s;
transition: box-shadow 0.25s;
}
.transition-all {
transition: all .25s;
transition: all 0.25s;
}
.z-depth-0 {
box-shadow: none !important;
box-shadow: none !important;
}
.z-depth-tiny {
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.05), 0 1px 1px 0 rgba(0, 0, 0, 0.05);
}
.z-depth-half {
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.16), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.16), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
}
.z-depth-1 {
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
.z-depth-1-half { /* used on hover states */
box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
.z-depth-1-half {
/* used on hover states */
box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
}
.z-depth-2 {
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.z-depth-3 {
box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19);
box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19);
}
.z-depth-4 {
box-shadow: 0 16px 28px 0 rgba(0, 0, 0, 0.22), 0 25px 55px 0 rgba(0, 0, 0, 0.21);
box-shadow: 0 16px 28px 0 rgba(0, 0, 0, 0.22), 0 25px 55px 0 rgba(0, 0, 0, 0.21);
}
.z-depth-5 {
box-shadow: 0 27px 24px 0 rgba(0, 0, 0, 0.2), 0 40px 77px 0 rgba(0, 0, 0, 0.22);
box-shadow: 0 27px 24px 0 rgba(0, 0, 0, 0.2), 0 40px 77px 0 rgba(0, 0, 0, 0.22);
}
@import "widget-avatar";
@ -70,3 +71,6 @@
@import "widget-sidebar-menu";
@import "widget-table";
@import "widget-tooltip";
@import "widget-checkbox";
@import "widget-tab";
@import "widget-selection";

View file

@ -20,7 +20,7 @@
{{#each attachments key="id" as |a index|}}
<li class="item">
<img class="icon" src="/assets/img/attachments/{{document/file-icon a.extension}}" />
<a href="{{ appMeta.endpoint }}/public/attachments/{{ appMeta.orgId }}/{{ a.job }}/{{ a.fileId }}">
<a href="{{ appMeta.endpoint }}/public/attachments/{{ appMeta.orgId }}/{{ a.id }}">
<span class="file">{{ a.filename }}</span>
</a>
{{#if isEditor}}
@ -34,42 +34,41 @@
</div>
{{/if}}
<div class="pages">
{{#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')}}
{{section/base-renderer page=page}}
</div>
</div>
{{/each}}
</div>
<div class="pages">
{{#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')}} {{section/base-renderer page=page}}
</div>
</div>
{{/each}}
</div>
<div class="dropdown-dialog delete-attachment-dialog">
<div class="content">
<p>Are you sure you want to delete <span class="bold">{{deleteAttachment.name}}?</span></p>
</div>
<div class="actions">
<div class="flat-button" {{action 'cancel'}}>
cancel
</div>
<div class="flat-button flat-red" {{action 'deleteAttachment'}}>
delete
</div>
</div>
<div class="clearfix"></div>
</div>
<div class="dropdown-dialog delete-attachment-dialog">
<div class="content">
<p>Are you sure you want to delete <span class="bold">{{deleteAttachment.name}}?</span></p>
</div>
<div class="actions">
<div class="flat-button" {{action 'cancel'}}>
cancel
</div>
<div class="flat-button flat-red" {{action 'deleteAttachment'}}>
delete
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
{{#if noSections}}
<div class="no-sections">
<div class="box">
<div class="message">Click the
<div class="no-sections">
<div class="box">
<div class="message">Click the
<div class="round-button-mono">
<i class="material-icons color-gray">add</i>
<i class="material-icons color-gray">add</i>
<div class="name">section</div>
</div>
to add a new section to this document</div>
</div>
</div>
</div>
to add a new section to this document</div>
</div>
</div>
{{/if}}

View file

@ -0,0 +1,84 @@
<div class="edit-tools">
<ul class="toolbar">
<li class="item">
<div class="square-button-mono button-gray" id="content-linker-button" data-tooltip="Reference link" data-tooltip-position="left middle">
<i class="material-icons color-white">link</i>
</div>
</li>
</ul>
{{#dropdown-dialog target="content-linker-button" position="bottom right" button="Insert" color="flat-blue" onAction=(action 'onInsertLink')}}
<div class="content-linker-dialog">
<form>
{{ui/ui-tab tabs=tabs onTabSelect=(action 'onTabSelect')}}
<div class="margin-top-40" />
{{#if showSections}}
<ul class="link-list">
{{#each candidates.pages as |p|}}
<li class="link-item" {{ action 'setSelection' p }}>
{{#ui/ui-selection selected=p.selected}}
{{p.title}}
{{/ui/ui-selection}}
</li>
{{/each}}
</ul>
{{/if}}
{{#if showAttachments}}
<ul class="link-list">
{{#each candidates.attachments as |a|}}
<li class="link-item" {{ action 'setSelection' a }}>
{{#ui/ui-selection selected=a.selected}}
<img class="icon" src="/assets/img/attachments/{{document/file-icon a.context}}" />
{{ a.title }}
{{/ui/ui-selection}}
</li>
{{/each}}
</ul>
{{/if}}
{{#if showSearch}}
<div class="input-control">
<label>Search</label>
<div class="tip">For content or attachments</div>
{{focus-input id="content-linker-search" type="input" value=keywords placeholder="keyword search" autocomplete="off"}}
</div>
{{#unless hasMatches}}
Nothing found.
{{/unless}}
<ul class="link-list">
{{#each matches.documents as |m|}}
<li class="link-item" {{ action 'setSelection' m }}>
{{#ui/ui-selection selected=m.selected}}
{{m.title}}
{{/ui/ui-selection}}
</li>
{{/each}}
{{#each matches.pages as |m|}}
<li class="link-item" {{ action 'setSelection' m }}>
{{#ui/ui-selection selected=m.selected}}
{{m.title}}<br/><span class="color-gray">{{m.context}}</span>
{{/ui/ui-selection}}
</li>
{{/each}}
{{#each matches.attachments as |a|}}
<li class="link-item" {{ action 'setSelection' a }}>
{{#ui/ui-selection selected=a.selected}}
<img class="icon" src="/assets/img/attachments/{{document/file-icon a.context}}" />
{{ a.title }}
{{/ui/ui-selection}}
</li>
{{/each}}
</ul>
{{/if}}
<div class="hide regular-button button-blue pull-right" {{ action 'onInsertLink' }}>Insert</div>
<div class="hide clearfix" />
</form>
</div>
{{/dropdown-dialog}}
</div>

View file

@ -4,12 +4,7 @@
<i class="material-icons color-gray">close</i>
</div>
{{folder/start-document
savedTemplates=savedTemplates
folder=folder
editor=folderService.canEditCurrentFolder
onEditTemplate=(action 'onEditTemplate')
onDocumentTemplate=(action 'onDocumentTemplate')}}
{{folder/start-document savedTemplates=savedTemplates folder=folder editor=folderService.canEditCurrentFolder onEditTemplate=(action 'onEditTemplate') onDocumentTemplate=(action 'onDocumentTemplate')}}
{{/if}}
{{#if showScrollTool}}
@ -35,7 +30,7 @@
<i class="material-icons">add</i>
<div class="name">Space</div>
</div>
{{#dropdown-dialog target="add-space-button" position="bottom left" button="Add" color="flat-green" onAction=(action 'addFolder') focusOn="new-folder-name"}}
{{#dropdown-dialog target="add-space-button" position="bottom left" button="Add" color="flat-green" onAction=(action 'addFolder') focusOn="new-folder-name" }}
<div>
<div class="input-control">
<label>New space</label>
@ -92,8 +87,6 @@
{{/if}}
</div>
<div class="copyright hidden-xs hidden-sm">
<a href="https://documize.com?ref=app-footer" target="_blank">Copyright&nbsp;&copy;&nbsp;2016 Documize Inc.</a>
</div>

View file

@ -1,13 +1,16 @@
{{#section/base-editor document=document folder=folder page=page isDirty=(action 'isDirty') onCancel=(action 'onCancel') onAction=(action 'onAction')}}
<div class="container-fluid">
<div class="row">
<div class="col-xs-6">
<div class="col-xs-6 col-sm-6 col-md-6 col-lg-6">
{{focus-textarea id="section-markdown-editor" class="mousetrap bordered" value=pageBody}}
</div>
<div class="col-xs-6">
<div class="col-xs-5 col-sm-5 col-md-5 col-lg-5">
<div id="section-markdown-preview" class="mousetrap bordered wysiwyg">
</div>
</div>
<div class="col-xs-1 col-sm-1 col-md-1 col-lg-1">
{{document/edit-tools document=document folder=folder page=page onInsertLink=(action 'onInsertLink')}}
</div>
</div>
</div>
{{/section/base-editor}}

View file

@ -1,3 +1,13 @@
{{#section/base-editor document=document folder=folder page=page isDirty=(action 'isDirty') onCancel=(action 'onCancel') onAction=(action 'onAction')}}
{{focus-textarea value=pageBody id="rich-text-editor" class="mousetrap"}}
<div class="container-fluid">
<div class="row">
<div class="col-xs-11 col-sm-11 col-md-11 col-lg-11">
{{focus-textarea value=pageBody id="rich-text-editor" class="mousetrap"}}
</div>
<div class="col-xs-1 col-sm-1 col-md-1 col-lg-1">
{{document/edit-tools document=document folder=folder page=page onInsertLink=(action 'onInsertLink')}}
</div>
</div>
</div>
{{/section/base-editor}}

View file

@ -0,0 +1,8 @@
<div class="checkbox-option {{if selected 'checkbox-option-selected'}}">
{{#if selected}}
<i class="material-icons checkbox-option-selected">radio_button_checked</i>
{{else}}
<i class="material-icons">radio_button_unchecked</i>
{{/if}}
{{yield}}
</div>

View file

@ -0,0 +1,6 @@
<div class="widget-selection">
<div class="option {{if selected 'selected'}}">
{{yield}}
<i class="material-icons">check</i>
</div>
</div>

View file

@ -0,0 +1,5 @@
<ul class="widget-tab">
{{#each tabs as |tab|}}
<li style={{myWidth}} class="tab {{if tab.selected 'selected'}}" {{action 'onTabSelect' tab}}>{{tab.label}}</li>
{{/each}}
</ul>

View file

@ -1,31 +1,31 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
// from http://thecodeship.com/web-development/alternative-to-javascript-evil-setinterval/
function interval(func, wait, times) {
var interv = function(w, t) {
return function() {
if (typeof t === "undefined" || t-- > 0) {
setTimeout(interv, w);
try {
func.call(null);
} catch (e) {
t = 0;
throw e.toString();
}
}
};
}(wait, times);
var interv = function (w, t) {
return function () {
if (typeof t === "undefined" || t-- > 0) {
setTimeout(interv, w);
try {
func.call(null);
} catch (e) {
t = 0;
throw e.toString();
}
}
};
}(wait, times);
setTimeout(interv, wait);
setTimeout(interv, wait);
}
// Function wrapping code.
@ -35,12 +35,34 @@ function interval(func, wait, times) {
// e.g. var fun1 = wrapFunction(sayStuff, this, ["Hello, world!"]);
// http://stackoverflow.com/questions/899102/how-do-i-store-javascript-functions-in-a-queue-for-them-to-be-executed-eventuall
function wrapFunction(fn, context, params) {
return function() {
fn.apply(context, params);
};
return function () {
fn.apply(context, params);
};
}
function insertAtCursor(myField, myValue) {
//IE support
if (document.selection) {
myField.focus();
let sel = document.selection.createRange();
sel.text = myValue;
}
//MOZILLA and others
else if (myField.selectionStart || myField.selectionStart === '0') {
var startPos = myField.selectionStart;
var endPos = myField.selectionEnd;
myField.value = myField.value.substring(0, startPos) +
myValue +
myField.value.substring(endPos, myField.value.length);
myField.selectionStart = startPos + myValue.length;
myField.selectionEnd = startPos + myValue.length;
} else {
myField.value += myValue;
}
}
export default {
interval,
wrapFunction
};
interval,
wrapFunction,
insertAtCursor
};

View file

@ -13,50 +13,50 @@
var EmberApp = require('ember-cli/lib/broccoli/ember-app');
var isDevelopment = EmberApp.env() === 'development';
module.exports = function(defaults) {
var app = new EmberApp(defaults, {
fingerprint: {
enabled: true,
extensions: ['js', 'css'],
exclude: ['tinymce/**', 'codemirror/**']
},
module.exports = function (defaults) {
var app = new EmberApp(defaults, {
fingerprint: {
enabled: true,
extensions: ['js', 'css'],
exclude: ['tinymce/**', 'codemirror/**']
},
minifyJS: {
enabled: !isDevelopment,
options: {
exclude: ['tinymce/**', 'codemirror/**']
}
},
minifyJS: {
enabled: !isDevelopment,
options: {
exclude: ['tinymce/**', 'codemirror/**']
}
},
minifyCSS: {
enabled: !isDevelopment,
options: {
exclude: ['tinymce/**', 'codemirror/**']
}
},
minifyCSS: {
enabled: !isDevelopment,
options: {
exclude: ['tinymce/**', 'codemirror/**']
}
},
sourcemaps: {
enabled: isDevelopment,
extensions: ['js']
}
});
sourcemaps: {
enabled: isDevelopment,
extensions: ['js']
}
});
app.import('vendor/dropzone.js');
app.import('vendor/is.js');
app.import('vendor/md5.js');
app.import('vendor/moment.js');
app.import('vendor/mousetrap.js');
app.import('vendor/table-editor.min.js');
app.import('vendor/underscore.js');
app.import('vendor/bootstrap.css');
app.import('vendor/tether.js');
app.import('vendor/drop.js');
app.import('vendor/tooltip.js');
app.import('vendor/markdown-it.min.js');
app.import('vendor/dragula.js');
app.import('vendor/dropzone.js');
app.import('vendor/is.js');
app.import('vendor/md5.js');
app.import('vendor/moment.js');
app.import('vendor/mousetrap.js');
app.import('vendor/table-editor.min.js');
app.import('vendor/underscore.js');
app.import('vendor/bootstrap.css');
app.import('vendor/tether.js');
app.import('vendor/drop.js');
app.import('vendor/tooltip.js');
app.import('vendor/markdown-it.min.js');
app.import('vendor/dragula.js');
app.import('vendor/datetimepicker.min.js');
app.import('vendor/hoverIntent.js');
app.import('vendor/waypoints.js');
return app.toTree();
return app.toTree();
};

File diff suppressed because one or more lines are too long

View file

@ -40,7 +40,7 @@ func AttachmentDownload(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
attachment, err := p.GetAttachmentByJobAndFileID(params["orgID"], params["job"], params["fileID"])
attachment, err := p.GetAttachment(params["orgID"], params["attachmentID"])
if err == sql.ErrNoRows {
writeNotFoundError(w, method, params["fileID"])

View file

@ -0,0 +1,167 @@
// 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 endpoint
import (
"database/sql"
"encoding/json"
"net/http"
"net/url"
"github.com/gorilla/mux"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/util"
"github.com/documize/community/core/log"
)
// GetLinkCandidates returns references to documents/sections/attachments.
func GetLinkCandidates(w http.ResponseWriter, r *http.Request) {
method := "GetLinkCandidates"
p := request.GetPersister(r)
params := mux.Vars(r)
folderID := params["folderID"]
documentID := params["documentID"]
pageID := params["pageID"]
// parameter check
if len(documentID) == 0 {
util.WriteMissingDataError(w, method, "documentID")
return
}
if len(pageID) == 0 {
util.WriteMissingDataError(w, method, "pageID")
return
}
// permission check
if !p.CanViewDocument(documentID) {
util.WriteForbiddenError(w)
return
}
// We can link to a section within the same document so
// let's get all pages for the document and remove "us".
pages, err := p.GetPagesWithoutContent(documentID)
if err != nil && err != sql.ErrNoRows {
util.WriteServerError(w, method, err)
return
}
if len(pages) == 0 {
pages = []entity.Page{}
}
pc := []entity.LinkCandidate{}
for _, p := range pages {
if p.RefID != pageID {
c := entity.LinkCandidate{
RefID: util.UniqueID(),
FolderID: folderID,
DocumentID: documentID,
TargetID: p.RefID,
LinkType: "section",
Title: p.Title,
}
pc = append(pc, c)
}
}
// We can link to attachment within the same document so
// let's get all attachments for the document.
files, err := p.GetAttachments(documentID)
if err != nil && err != sql.ErrNoRows {
util.WriteServerError(w, method, err)
return
}
if len(files) == 0 {
files = []entity.Attachment{}
}
fc := []entity.LinkCandidate{}
for _, f := range files {
c := entity.LinkCandidate{
RefID: util.UniqueID(),
FolderID: folderID,
DocumentID: documentID,
TargetID: f.RefID,
LinkType: "file",
Title: f.Filename,
Context: f.Extension,
}
fc = append(fc, c)
}
// send back the payload
var payload struct {
Pages []entity.LinkCandidate `json:"pages"`
Attachments []entity.LinkCandidate `json:"attachments"`
}
payload.Pages = pc
payload.Attachments = fc
json, err := json.Marshal(payload)
if err != nil {
util.WriteMarshalError(w, err)
return
}
util.WriteSuccessBytes(w, json)
}
// SearchLinkCandidates endpoint takes a list of keywords and returns a list of document references matching those keywords.
func SearchLinkCandidates(w http.ResponseWriter, r *http.Request) {
method := "SearchLinkCandidates"
p := request.GetPersister(r)
query := r.URL.Query()
keywords := query.Get("keywords")
decoded, err := url.QueryUnescape(keywords)
log.IfErr(err)
docs, pages, attachments, err := p.SearchLinkCandidates(decoded)
if err != nil {
util.WriteServerError(w, method, err)
return
}
var payload struct {
Documents []entity.LinkCandidate `json:"documents"`
Pages []entity.LinkCandidate `json:"pages"`
Attachments []entity.LinkCandidate `json:"attachments"`
}
payload.Documents = docs
payload.Pages = pages
payload.Attachments = attachments
json, err := json.Marshal(payload)
if err != nil {
util.WriteMarshalError(w, err)
return
}
util.WriteSuccessBytes(w, json)
}

View file

@ -136,7 +136,7 @@ func init() {
log.IfErr(Add(RoutePrefixPublic, "forgot", []string{"POST", "OPTIONS"}, nil, ForgotUserPassword))
log.IfErr(Add(RoutePrefixPublic, "reset/{token}", []string{"POST", "OPTIONS"}, nil, ResetUserPassword))
log.IfErr(Add(RoutePrefixPublic, "share/{folderID}", []string{"POST", "OPTIONS"}, nil, AcceptSharedFolder))
log.IfErr(Add(RoutePrefixPublic, "attachments/{orgID}/{job}/{fileID}", []string{"GET", "OPTIONS"}, nil, AttachmentDownload))
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
@ -212,12 +212,15 @@ func init() {
log.IfErr(Add(RoutePrefixPrivate, "sections", []string{"POST", "OPTIONS"}, nil, RunSectionCommand))
log.IfErr(Add(RoutePrefixPrivate, "sections/refresh", []string{"GET", "OPTIONS"}, nil, RefreshSections))
// Links
log.IfErr(Add(RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates))
log.IfErr(Add(RoutePrefixPrivate, "links", []string{"GET", "OPTIONS"}, nil, SearchLinkCandidates))
// Global installation-wide config
log.IfErr(Add(RoutePrefixPrivate, "global", []string{"GET", "OPTIONS"}, nil, GetGlobalConfig))
log.IfErr(Add(RoutePrefixPrivate, "global", []string{"PUT", "OPTIONS"}, nil, SaveGlobalConfig))
// **** configure single page app handler.
// Single page app handler
log.IfErr(Add(RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, GetRobots))
log.IfErr(Add(RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, GetSitemap))
log.IfErr(Add(RoutePrefixRoot, "{rest:.*}", nil, nil, web.EmberHandler))

View file

@ -222,8 +222,10 @@ func (p *PageMeta) SetDefaults() {
// DocumentMeta details who viewed the document.
type DocumentMeta struct {
Viewers []DocumentMetaViewer `json:"viewers"`
Editors []DocumentMetaEditor `json:"editors"`
Viewers []DocumentMetaViewer `json:"viewers"`
Editors []DocumentMetaEditor `json:"editors"`
InboundLinks []Link `json:"inboundLinks"`
OutboundLinks []Link `json:"outboundLinks"`
}
// DocumentMetaViewer contains the "view" metatdata content.
@ -342,3 +344,28 @@ type SitemapDocument struct {
Folder string
Revised time.Time
}
// Link defines a reference between a section and another document/section/attachment.
type Link struct {
BaseEntity
OrgID string `json:"orgId"`
FolderID string `json:"folderId"`
UserID string `json:"userId"`
LinkType string `json:"linkType"`
SourceDocumentID string `json:"sourceDocumentId"`
SourcePageID string `json:"sourcePageId"`
TargetDocumentID string `json:"targetDocumentId"`
TargetID string `json:"targetId"`
Orphan bool `json:"orphan"`
}
// LinkCandidate defines a potential link to a document/section/attachment.
type LinkCandidate struct {
RefID string `json:"id"`
LinkType string `json:"linkType"`
FolderID string `json:"folderId"`
DocumentID string `json:"documentId"`
TargetID string `json:"targetId"`
Title string `json:"title"` // what we label the link
Context string `json:"context"` // additional context (e.g. excerpt, parent, file extension)
}

View file

@ -47,23 +47,23 @@ func (p *Persister) AddAttachment(a entity.Attachment) (err error) {
return
}
// GetAttachmentByJobAndFileID returns the database attachment record specified by the parameters.
func (p *Persister) GetAttachmentByJobAndFileID(orgID, job, fileID string) (attachment entity.Attachment, err error) {
// GetAttachment returns the database attachment record specified by the parameters.
func (p *Persister) GetAttachment(orgID, attachmentID string) (attachment entity.Attachment, err error) {
err = nil
stmt, err := Db.Preparex("SELECT id, refid, orgid, documentid, job, fileid, filename, data, extension, created, revised FROM attachment WHERE orgid=? and job=? and fileid=?")
stmt, err := Db.Preparex("SELECT id, refid, orgid, documentid, job, fileid, filename, data, extension, created, revised FROM attachment WHERE orgid=? and refid=?")
defer utility.Close(stmt)
if err != nil {
log.Error(fmt.Sprintf("Unable to prepare select for attachment %s/%s", job, fileID), err)
log.Error(fmt.Sprintf("Unable to prepare select for attachment %s", attachmentID), err)
return
}
err = stmt.Get(&attachment, orgID, job, fileID)
err = stmt.Get(&attachment, orgID, attachmentID)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute select for attachment %s/%s", job, fileID), err)
log.Error(fmt.Sprintf("Unable to execute select for attachment %s", attachmentID), err)
return
}
@ -102,5 +102,10 @@ func (p *Persister) GetAttachmentsWithData(docID string) (attachments []entity.A
// DeleteAttachment deletes the id record from the database attachment table.
func (p *Persister) DeleteAttachment(id string) (rows int64, err error) {
return p.Base.DeleteConstrained(p.Context.Transaction, "attachment", p.Context.OrgID, id)
rows, err = p.Base.DeleteConstrained(p.Context.Transaction, "attachment", p.Context.OrgID, id)
// Mark references to this document as orphaned
err = p.MarkOrphanAttachmentLink(id)
return
}

View file

@ -107,6 +107,13 @@ func (p *Persister) GetDocumentMeta(id string) (meta entity.DocumentMeta, err er
return
}
meta.OutboundLinks, err = p.GetDocumentOutboundLinks(id)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute GetDocumentOutboundLinks for document %s", id), err)
return
}
return
}
@ -400,6 +407,18 @@ func (p *Persister) DeleteDocument(documentID string) (rows int64, err error) {
return
}
// Mark references to this document as orphaned
err = p.MarkOrphanDocumentLink(documentID)
if err != nil {
return
}
// Remove all references from this document
_, err = p.DeleteSourceDocumentLinks(documentID)
if err != nil {
return
}
p.Base.Audit(p.Context, "delete-document", documentID, "")
return p.Base.DeleteConstrained(p.Context.Transaction, "document", p.Context.OrgID, documentID)

292
core/api/request/link.go Normal file
View file

@ -0,0 +1,292 @@
// 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 (
"fmt"
"time"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/util"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
)
// AddContentLink inserts wiki-link into the store.
// These links exist when content references another document or content.
func (p *Persister) AddContentLink(l entity.Link) (err error) {
l.Created = time.Now().UTC()
l.Revised = time.Now().UTC()
stmt, err := p.Context.Transaction.Preparex("INSERT INTO link (refid, orgid, folderid, userid, sourcedocumentid, sourcepageid, targetdocumentid, targetid, linktype, orphan, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer utility.Close(stmt)
if err != nil {
log.Error("Unable to prepare insert for link", err)
return
}
_, err = stmt.Exec(l.RefID, l.OrgID, l.FolderID, l.UserID, l.SourceDocumentID, l.SourcePageID, l.TargetDocumentID, l.TargetID, l.LinkType, l.Orphan, l.Created, l.Revised)
if err != nil {
log.Error("Unable to execute insert for link", err)
return
}
return
}
// SearchLinkCandidates returns matching documents, sections and attachments using keywords.
func (p *Persister) SearchLinkCandidates(keywords string) (docs []entity.LinkCandidate,
pages []entity.LinkCandidate, attachments []entity.LinkCandidate, err error) {
err = nil
// find matching documents
temp := []entity.LinkCandidate{}
likeQuery := "title LIKE '%" + keywords + "%'"
err = Db.Select(&temp,
`SELECT refid as documentid, labelid as folderid,title from document WHERE orgid=? AND `+likeQuery+` 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 search links for org %s", p.Context.OrgID), err)
return
}
for _, r := range temp {
c := entity.LinkCandidate{
RefID: util.UniqueID(),
FolderID: r.FolderID,
DocumentID: r.DocumentID,
TargetID: r.DocumentID,
LinkType: "document",
Title: r.Title,
Context: "",
}
docs = append(docs, c)
}
// find matching sections
likeQuery = "p.title LIKE '%" + keywords + "%'"
temp = []entity.LinkCandidate{}
err = Db.Select(&temp,
`SELECT p.refid as targetid, p.documentid as documentid, p.title as title, d.title as context, d.labelid as folderid from page p
LEFT JOIN document d ON d.refid=p.documentid WHERE p.orgid=? AND `+likeQuery+` AND d.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 p.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 search links for org %s", p.Context.OrgID), err)
return
}
for _, r := range temp {
c := entity.LinkCandidate{
RefID: util.UniqueID(),
FolderID: r.FolderID,
DocumentID: r.DocumentID,
TargetID: r.TargetID,
LinkType: "section",
Title: r.Title,
Context: r.Context,
}
pages = append(pages, c)
}
// find matching attachments
likeQuery = "a.filename LIKE '%" + keywords + "%'"
temp = []entity.LinkCandidate{}
err = Db.Select(&temp,
`SELECT a.refid as targetid, a.documentid as documentid, a.filename as title, a.extension as context, d.labelid as folderid from attachment a
LEFT JOIN document d ON d.refid=a.documentid WHERE a.orgid=? AND `+likeQuery+` AND d.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 a.filename`,
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 search links for org %s", p.Context.OrgID), err)
return
}
for _, r := range temp {
c := entity.LinkCandidate{
RefID: util.UniqueID(),
FolderID: r.FolderID,
DocumentID: r.DocumentID,
TargetID: r.TargetID,
LinkType: "file",
Title: r.Title,
Context: r.Context,
}
attachments = append(attachments, c)
}
if len(docs) == 0 {
docs = []entity.LinkCandidate{}
}
if len(pages) == 0 {
pages = []entity.LinkCandidate{}
}
if len(attachments) == 0 {
attachments = []entity.LinkCandidate{}
}
return
}
// GetDocumentOutboundLinks returns outbound links for specified document.
func (p *Persister) GetDocumentOutboundLinks(documentID string) (links []entity.Link, err error) {
err = nil
err = Db.Select(&links,
`select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetid, l.linktype, l.orphan, l.created, l.revised
FROM link l
WHERE l.orgid=? AND l.sourcedocumentid=?`,
p.Context.OrgID,
documentID)
if err != nil {
return
}
if len(links) == 0 {
links = []entity.Link{}
}
return
}
// GetPageLinks returns outbound links for specified page in document.
func (p *Persister) GetPageLinks(documentID, pageID string) (links []entity.Link, err error) {
err = nil
err = Db.Select(&links,
`select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetid, l.linktype, l.orphan, l.created, l.revised
FROM link l
WHERE l.orgid=? AND l.sourcedocumentid=? AND l.sourcepageid=?`,
p.Context.OrgID,
documentID,
pageID)
if err != nil {
return
}
if len(links) == 0 {
links = []entity.Link{}
}
return
}
// MarkOrphanDocumentLink marks all link records referencing specified document.
func (p *Persister) MarkOrphanDocumentLink(documentID string) (err error) {
revised := time.Now().UTC()
stmt, err := p.Context.Transaction.Preparex("UPDATE link SET orphan=1, revised=? WHERE linktype='document' AND orgid=? AND targetdocumentid=?")
if err != nil {
return
}
defer utility.Close(stmt)
_, err = stmt.Exec(revised, p.Context.OrgID, documentID)
return
}
// MarkOrphanPageLink marks all link records referencing specified page.
func (p *Persister) MarkOrphanPageLink(pageID string) (err error) {
revised := time.Now().UTC()
stmt, err := p.Context.Transaction.Preparex("UPDATE link SET orphan=1, revised=? WHERE linktype='section' AND orgid=? AND targetid=?")
if err != nil {
return
}
defer utility.Close(stmt)
_, err = stmt.Exec(revised, p.Context.OrgID, pageID)
return
}
// MarkOrphanAttachmentLink marks all link records referencing specified attachment.
func (p *Persister) MarkOrphanAttachmentLink(attachmentID string) (err error) {
revised := time.Now().UTC()
stmt, err := p.Context.Transaction.Preparex("UPDATE link SET orphan=1, revised=? WHERE linktype='file' AND orgid=? AND targetid=?")
if err != nil {
return
}
defer utility.Close(stmt)
_, err = stmt.Exec(revised, p.Context.OrgID, attachmentID)
return
}
// DeleteSourcePageLinks removes saved links for given source.
func (p *Persister) DeleteSourcePageLinks(pageID string) (rows int64, err error) {
return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourcepageid=\"%s\"", p.Context.OrgID, pageID))
}
// DeleteSourceDocumentLinks removes saved links for given document.
func (p *Persister) DeleteSourceDocumentLinks(documentID string) (rows int64, err error) {
return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourcedocumentid=\"%s\"", p.Context.OrgID, documentID))
}
// DeleteLink removes saved link from the store.
func (p *Persister) DeleteLink(id string) (rows int64, err error) {
return p.Base.DeleteConstrained(p.Context.Transaction, "link", p.Context.OrgID, id)
}

View file

@ -20,6 +20,7 @@ import (
"github.com/documize/community/core/api/endpoint/models"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/util"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
)
@ -286,6 +287,46 @@ func (p *Persister) UpdatePage(page entity.Page, refID, userID string, skipRevis
//}
//}
// find any content links in the HTML
links := util.GetContentLinks(page.Body)
// get a copy of previously saved links
previousLinks, _ := p.GetPageLinks(page.DocumentID, page.RefID)
fmt.Println(len(previousLinks))
// delete previous content links for this page
_, _ = p.DeleteSourcePageLinks(page.RefID)
// save latest content links for this page
for _, link := range links {
link.Orphan = false
link.OrgID = p.Context.OrgID
link.UserID = p.Context.UserID
link.SourceDocumentID = page.DocumentID
link.SourcePageID = page.RefID
if link.LinkType == "document" {
link.TargetID = ""
}
// We check if there was a previously saved version of this link.
// If we find one, we carry forward the orphan flag.
for _, p := range previousLinks {
if link.TargetID == p.TargetID && link.LinkType == p.LinkType {
link.Orphan = p.Orphan
break
}
}
// save
err := p.AddContentLink(link)
if err != nil {
log.Error(fmt.Sprintf("Unable to insert content links for page %s", page.RefID), err)
return err
}
}
p.Base.Audit(p.Context, "update-page", page.DocumentID, page.RefID)
return
@ -377,6 +418,12 @@ func (p *Persister) DeletePage(documentID, pageID string) (rows int64, err error
_, 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)
// delete content links from this page
_, err = p.DeleteSourcePageLinks(pageID)
// mark as orphan links to this page
err = p.MarkOrphanPageLink(pageID)
p.Base.Audit(p.Context, "remove-page", documentID, pageID)
}

View file

@ -14,11 +14,11 @@ package util
import "testing"
func TestHTMLEncoding(t *testing.T) {
html(t, "<script>alert('test')</script>", "&lt;script&gt;alert(&#39;test&#39;)&lt;/script&gt;")
testHTML(t, "<script>alert('test')</script>", "&lt;script&gt;alert(&#39;test&#39;)&lt;/script&gt;")
text(t, "<script>alert('test')</script>", "<script>alert('test')</script>")
}
func html(t *testing.T, in, out string) {
func testHTML(t *testing.T, in, out string) {
got := EncodeHTMLString(in)
if got != out {
t.Errorf("EncodeHTMLString `%s` got `%s` expected `%s`\n", in, got, out)

75
core/api/util/links.go Normal file
View file

@ -0,0 +1,75 @@
// 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 util
import (
"strings"
"golang.org/x/net/html"
"github.com/documize/community/core/api/entity"
)
// GetContentLinks returns Documize generated <a> links.
// such links have an identifying attribute e.g. <a data-documize='true'...
func GetContentLinks(body string) (links []entity.Link) {
z := html.NewTokenizer(strings.NewReader(body))
for {
tt := z.Next()
switch {
case tt == html.ErrorToken:
// End of the document, we're done
return
case tt == html.StartTagToken:
t := z.Token()
// Check if the token is an <a> tag
isAnchor := t.Data == "a"
if !isAnchor {
continue
}
// Extract the content link
ok, link := getLink(t)
if ok {
links = append(links, link)
}
}
}
}
// Helper function to pull the href attribute from a Token
func getLink(t html.Token) (ok bool, link entity.Link) {
ok = false
// Iterate over all of the Token's attributes until we find an "href"
for _, a := range t.Attr {
switch a.Key {
case "data-documize":
ok = true
case "data-link-id":
link.RefID = strings.TrimSpace(a.Val)
case "data-link-space-id":
link.FolderID = strings.TrimSpace(a.Val)
case "data-link-target-document-id":
link.TargetDocumentID = strings.TrimSpace(a.Val)
case "data-link-target-id":
link.TargetID = strings.TrimSpace(a.Val)
case "data-link-type":
link.LinkType = strings.TrimSpace(a.Val)
}
}
return
}

View file

@ -279,3 +279,56 @@ CREATE TABLE IF NOT EXISTS `userconfig` (
UNIQUE INDEX `idx_userconfig_orguserkey` (`orgid`, `userid`, `key` ASC))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB;
DROP TABLE IF EXISTS `share`;
CREATE TABLE IF NOT EXISTS `share` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`email` NVARCHAR(250) NOT NULL DEFAULT '',
`message` NVARCHAR(500) NOT NULL DEFAULT '',
`viewed` VARCHAR(500) NOT NULL DEFAULT '',
`secret` VARCHAR(200) NOT NULL DEFAULT '',
`expires` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`active` BOOL NOT NULL DEFAULT 1,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_id PRIMARY KEY (id))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB;
DROP TABLE IF EXISTS `feedback`;
CREATE TABLE IF NOT EXISTS `feedback` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`refid` CHAR(16) NOT NULL COLLATE utf8_bin,
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`email` NVARCHAR(250) NOT NULL DEFAULT '',
`feedback` LONGTEXT,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_id PRIMARY KEY (id))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB;
DROP TABLE IF EXISTS `link`;
CREATE TABLE IF NOT EXISTS `link` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`refid` CHAR(16) NOT NULL COLLATE utf8_bin,
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`folderid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourcedocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourcepageid` CHAR(16) NOT NULL COLLATE utf8_bin,
`linktype` CHAR(16) NOT NULL COLLATE utf8_bin,
`targetdocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`targetid` CHAR(16) NOT NULL DEFAULT '' COLLATE utf8_bin,
`orphan` BOOL NOT NULL DEFAULT 0,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_id PRIMARY KEY (id))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB;

View file

@ -0,0 +1,20 @@
/* community edition */
DROP TABLE IF EXISTS `link`;
CREATE TABLE IF NOT EXISTS `link` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`refid` CHAR(16) NOT NULL COLLATE utf8_bin,
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`folderid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourcedocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourcepageid` CHAR(16) NOT NULL COLLATE utf8_bin,
`linktype` CHAR(16) NOT NULL COLLATE utf8_bin,
`targetdocumentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`targetid` CHAR(16) NOT NULL DEFAULT '' COLLATE utf8_bin,
`orphan` BOOL NOT NULL DEFAULT 0,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_id PRIMARY KEY (id))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB;

View file

@ -26,7 +26,7 @@ type ProdInfo struct {
// Product returns product edition details
func Product() (p ProdInfo) {
p.Major = "0"
p.Minor = "27"
p.Minor = "28"
p.Patch = "0"
p.Version = fmt.Sprintf("%s.%s.%s", p.Major, p.Minor, p.Patch)
p.Edition = "Community"

File diff suppressed because one or more lines are too long