mirror of
https://github.com/documize/community.git
synced 2025-07-22 22:59:43 +02:00
foundational layer for inserting content and attachment links into content
This commit is contained in:
parent
5ca53ecb04
commit
7db618dea0
20 changed files with 1397 additions and 721 deletions
59
app/app/components/link/content-linker.js
Normal file
59
app/app/components/link/content-linker.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
// 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.Component.extend({
|
||||
link: service(),
|
||||
hasSections: false,
|
||||
hasAttachments: false,
|
||||
linkName: '',
|
||||
selection: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
let self = this;
|
||||
|
||||
let documentId = this.get('document.id');
|
||||
let pageId = this.get('page.id');
|
||||
|
||||
this.get('link').getCandidates(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);
|
||||
});
|
||||
},
|
||||
|
||||
didReceiveAttrs() {},
|
||||
|
||||
didInsertElement() {},
|
||||
|
||||
willDestroyElement() {},
|
||||
|
||||
actions: {
|
||||
onInsertLink() {
|
||||
let selection = this.get('selection');
|
||||
let linkName = this.get('linkName');
|
||||
|
||||
if (linkName.length) {
|
||||
selection.title = linkName;
|
||||
}
|
||||
|
||||
if (is.not.null(selection)) {
|
||||
this.get('onInsertLink')(selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -11,15 +11,24 @@
|
|||
|
||||
import Ember from 'ember';
|
||||
|
||||
const {
|
||||
inject: { service }
|
||||
} = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
pageBody: "",
|
||||
appMeta: Ember.inject.service(),
|
||||
link: service(),
|
||||
pageBody: "",
|
||||
drop: null,
|
||||
showSidebar: false,
|
||||
|
||||
didReceiveAttrs() {
|
||||
this.set('pageBody', this.get('meta.rawBody'));
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
let self = this;
|
||||
|
||||
let options = {
|
||||
selector: "#rich-text-editor",
|
||||
relative_urls: false,
|
||||
|
@ -34,7 +43,7 @@ export default Ember.Component.extend({
|
|||
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,29 +57,29 @@ 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 | documizeLinkButton",
|
||||
save_onsavecallback: function () {
|
||||
Mousetrap.trigger('ctrl+s');
|
||||
},
|
||||
setup: function (editor) {
|
||||
editor.addButton('documizeLinkButton', {
|
||||
title: 'Insert Link',
|
||||
icon: false,
|
||||
image: '/favicon.ico',
|
||||
onclick: function () {
|
||||
let showSidebar = !self.get('showSidebar');
|
||||
self.set('showSidebar', showSidebar);
|
||||
|
||||
if (showSidebar) {
|
||||
self.send('showSidebar');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -91,6 +100,16 @@ export default Ember.Component.extend({
|
|||
},
|
||||
|
||||
actions: {
|
||||
showSidebar() {
|
||||
this.set('linkName', tinymce.activeEditor.selection.getContent());
|
||||
},
|
||||
|
||||
onInsertLink(link) {
|
||||
let linkHTML = this.get('link').buildLink(link);
|
||||
tinymce.activeEditor.insertContent(linkHTML);
|
||||
this.set('showSidebar', false);
|
||||
},
|
||||
|
||||
isDirty() {
|
||||
return is.not.undefined(tinymce) && is.not.undefined(tinymce.activeEditor) && tinymce.activeEditor.isDirty();
|
||||
},
|
||||
|
@ -110,3 +129,7 @@ export default Ember.Component.extend({
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
// editor.insertContent(' <b>It\'s my button!</b> ');
|
||||
// Selects the first paragraph found
|
||||
// tinyMCE.activeEditor.selection.select(tinyMCE.activeEditor.dom.select('p')[0]);
|
||||
|
|
54
app/app/services/link.js
Normal file
54
app/app/services/link.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
// 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(),
|
||||
|
||||
// Returns candidate links using provided parameters
|
||||
getCandidates(documentId, pageId /*, keywords*/ ) {
|
||||
return this.get('ajax').request(`links/${documentId}/${pageId}`, {
|
||||
method: 'GET'
|
||||
}).then((response) => {
|
||||
return response;
|
||||
});
|
||||
},
|
||||
|
||||
buildLink(link) {
|
||||
let result = "";
|
||||
let href = "";
|
||||
let endpoint = this.get('appMeta').get('endpoint');
|
||||
let orgId = this.get('appMeta').get('orgId');
|
||||
|
||||
if (link.linkType === "section") {
|
||||
href = `/link/${link.linkType}/${link.id}`;
|
||||
}
|
||||
if (link.linkType === "file") {
|
||||
href = `${endpoint}/public/attachments/${orgId}/${link.attachmentId}`;
|
||||
}
|
||||
if (link.linkType === "document") {
|
||||
href = `/link/${link.linkType}/${link.id}`;
|
||||
}
|
||||
|
||||
result = `<a data-link-id='${link.id}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`;
|
||||
|
||||
console.log(link);
|
||||
console.log(result);
|
||||
return result;
|
||||
}
|
||||
});
|
|
@ -15,6 +15,7 @@
|
|||
@import "base.scss";
|
||||
@import "widget/widget.scss";
|
||||
@import "view/layout.scss";
|
||||
@import "view/content-linker.scss";
|
||||
@import "view/page-search.scss";
|
||||
@import "view/page-documents.scss";
|
||||
@import "view/page-settings.scss";
|
||||
|
|
|
@ -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,7 +126,8 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -62,52 +137,81 @@ a.alt {
|
|||
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}#{"%"}; }
|
||||
.width-#{$i} {
|
||||
width: #{$i}#{"%"};
|
||||
}
|
||||
$i: $i - 5;
|
||||
}
|
||||
|
||||
.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 +231,12 @@ ul {
|
|||
}
|
||||
}
|
||||
|
||||
.clearfix:before,
|
||||
.clearfix:after {
|
||||
.clearfix:after,
|
||||
.clearfix:before {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
|
|
16
app/app/styles/view/content-linker.scss
Normal file
16
app/app/styles/view/content-linker.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
.content-linker {
|
||||
margin: 0 10px;
|
||||
padding: 20px;
|
||||
@include border(1px);
|
||||
|
||||
.input-control > .page-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> .item {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}}
|
||||
|
@ -38,8 +38,7 @@
|
|||
{{#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}}
|
||||
{{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}}
|
||||
|
|
21
app/app/templates/components/link/content-linker.hbs
Normal file
21
app/app/templates/components/link/content-linker.hbs
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div class="content-linker">
|
||||
<form>
|
||||
<div class="input-control">
|
||||
<label>Insert Link</label>
|
||||
<div class="tip">Give the link a clickable name</div>
|
||||
{{focus-input type="input" value=linkName}}
|
||||
</div>
|
||||
{{#if hasSections}}
|
||||
<div class="input-control">
|
||||
{{ui-select id="content-linker-section-list" content=candidates.pages action=(action (mut selection)) prompt="Link to existing section" optionValuePath="id" optionLabelPath="title" }}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if hasAttachments}}
|
||||
<div class="input-control">
|
||||
{{ui-select id="content-linker-attachment-list" content=candidates.attachments action=(action (mut selection)) prompt="Link to file attachment" optionValuePath="id" optionLabelPath="title" }}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="regular-button button-blue pull-right" {{ action 'onInsertLink' }}>Insert</div>
|
||||
<div class="clearfix" />
|
||||
</form>
|
||||
</div>
|
|
@ -1,3 +1,10 @@
|
|||
{{#section/base-editor document=document folder=folder page=page isDirty=(action 'isDirty') onCancel=(action 'onCancel') onAction=(action 'onAction')}}
|
||||
<div class="{{if showSidebar 'width-70' 'width-100'}} pull-left">
|
||||
{{focus-textarea value=pageBody id="rich-text-editor" class="mousetrap"}}
|
||||
</div>
|
||||
|
||||
<div class="{{if showSidebar 'width-30' 'no-display'}} pull-left">
|
||||
{{link/content-linker document=document folder=folder page=page linkName=linkName onInsertLink=(action 'onInsertLink')}}
|
||||
</div>
|
||||
|
||||
{{/section/base-editor}}
|
||||
|
|
10
app/vendor/markdown-it.min.js
vendored
10
app/vendor/markdown-it.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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"])
|
||||
|
|
135
core/api/endpoint/link_endpoint.go
Normal file
135
core/api/endpoint/link_endpoint.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
// 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"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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)
|
||||
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(),
|
||||
DocumentID: documentID,
|
||||
PageID: 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(),
|
||||
DocumentID: documentID,
|
||||
AttachmentID: f.RefID,
|
||||
LinkType: "file",
|
||||
Title: f.Filename,
|
||||
}
|
||||
|
||||
fc = append(fc, c)
|
||||
}
|
||||
|
||||
// send back the payload
|
||||
var payload struct {
|
||||
Pages []entity.LinkCandidate `json:"pages"`
|
||||
Attachments []entity.LinkCandidate `json:"attachments"`
|
||||
Matches []entity.LinkCandidate `json:"matches"`
|
||||
}
|
||||
|
||||
payload.Pages = pc
|
||||
payload.Attachments = fc
|
||||
|
||||
json, err := json.Marshal(payload)
|
||||
|
||||
if err != nil {
|
||||
util.WriteMarshalError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
util.WriteSuccessBytes(w, json)
|
||||
}
|
||||
|
||||
/*
|
||||
DocumentID string `json:"documentId"`
|
||||
PageID string `json:"pageId"`
|
||||
FileID string `json:"fileId"`
|
||||
LinkType string `json:"linkType"`
|
||||
Title string `json:"caption"` // what we label the link
|
||||
Context string `json:"context"` // additional context (e.g. excerpt, parent)
|
||||
|
||||
*/
|
|
@ -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,14 @@ 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/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates))
|
||||
|
||||
// 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))
|
||||
|
|
|
@ -342,3 +342,27 @@ 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"`
|
||||
UserID string `json:"userId"`
|
||||
SourceID string `json:"sourceId"`
|
||||
DocumentID string `json:"documentId"`
|
||||
PageID string `json:"pageId"`
|
||||
LinkType string `json:"linkType"`
|
||||
Orphan bool `json:"orphan"`
|
||||
}
|
||||
|
||||
// LinkCandidate defines a potential link to a document/section/attachment.
|
||||
type LinkCandidate struct {
|
||||
RefID string `json:"id"`
|
||||
OrgID string `json:"orgId"`
|
||||
DocumentID string `json:"documentId"`
|
||||
PageID string `json:"pageId"`
|
||||
AttachmentID string `json:"attachmentId"`
|
||||
LinkType string `json:"linkType"`
|
||||
Title string `json:"title"` // what we label the link
|
||||
Context string `json:"context"` // additional context (e.g. excerpt, parent)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
138
core/api/request/link.go
Normal file
138
core/api/request/link.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
// 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/log"
|
||||
"github.com/documize/community/core/utility"
|
||||
)
|
||||
|
||||
// AddLink inserts wiki-link into the store.
|
||||
// These links exist when content references another document or content.
|
||||
func (p *Persister) AddLink(l entity.Link) (err error) {
|
||||
l.UserID = p.Context.UserID
|
||||
l.Created = time.Now().UTC()
|
||||
l.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := p.Context.Transaction.Preparex("INSERT INTO link (refid, orgid, userid, sourceid, documentid, pageid, linktype, 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.UserID, l.SourceID, l.DocumentID, l.PageID, l.LinkType, l.Created, l.Revised)
|
||||
|
||||
if err != nil {
|
||||
log.Error("Unable to execute insert for link", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetReferencedLinks returns all links that the specified section is referencing.
|
||||
func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, err error) {
|
||||
err = nil
|
||||
|
||||
sql := "SELECT id,refid,orgid,userid,sourceid,documentid,sectionid,linktype,orphan,created,revised from link WHERE orgid=? AND sourceid=?"
|
||||
|
||||
err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetLinksToSection returns all links that are linking to the specified section.
|
||||
func (p *Persister) GetLinksToSection(sectionID string) (links []entity.Link, err error) {
|
||||
err = nil
|
||||
|
||||
sql := "SELECT id,refid,orgid,userid,sourceid,documentid,sectionid,linktype,orphan,created,revised from link WHERE orgid=? AND sectionid=?"
|
||||
|
||||
err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetLinksToDocument returns all links that are linking to the specified document.
|
||||
func (p *Persister) GetLinksToDocument(documentID string) (links []entity.Link, err error) {
|
||||
err = nil
|
||||
|
||||
sql := "SELECT id,refid,orgid,userid,sourceid,documentid,sectionid,linktype,orphan,created,revised from link WHERE orgid=? AND documentid=?"
|
||||
|
||||
err = Db.Select(&links, sql, p.Context.OrgID, documentID)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// MarkLinkAsOrphan marks the link record as being invalid.
|
||||
func (p *Persister) MarkLinkAsOrphan(l entity.Link) (err error) {
|
||||
l.Orphan = true
|
||||
l.Revised = time.Now().UTC()
|
||||
|
||||
stmt, err := p.Context.Transaction.PrepareNamed("UPDATE link SET orphan=1 revised=:revised WHERE orgid=:orgid AND refid=:refid")
|
||||
defer utility.Close(stmt)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to prepare update for link %s", l.RefID), err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = stmt.Exec(&l)
|
||||
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("Unable to execute update for link %s", l.RefID), err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// GetLinkCandidates returns matching results based upon specified parameters.
|
||||
// func (p *Persister) GetLinkCandidates(keywords string) (c []entity.LinkCandidate, err error) {
|
||||
// err = nil
|
||||
//
|
||||
// sql := "SELECT id,refid,orgid,userid,sourceid,documentid,sectionid,linktype,orphan,created,revised from link WHERE orgid=? AND sectionid=?"
|
||||
//
|
||||
// err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
|
||||
//
|
||||
// if err != nil {
|
||||
// log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// return
|
||||
// }
|
|
@ -279,3 +279,54 @@ 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,
|
||||
`sourceid` CHAR(16) NOT NULL COLLATE utf8_bin,
|
||||
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin,
|
||||
`sectionid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
|
||||
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
|
||||
`linktype` CHAR(16) NOT NULL 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;
|
||||
|
|
18
core/database/scripts/autobuild/db_00004.sql
Normal file
18
core/database/scripts/autobuild/db_00004.sql
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* 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,
|
||||
`sourceid` CHAR(16) NOT NULL COLLATE utf8_bin,
|
||||
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin,
|
||||
`sectionid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
|
||||
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
|
||||
`linktype` CHAR(16) NOT NULL 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;
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue