1
0
Fork 0
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:
Harvey Kandola 2016-10-23 18:33:07 -07:00
parent 5ca53ecb04
commit 7db618dea0
20 changed files with 1397 additions and 721 deletions

View 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);
}
}
}
});

View file

@ -11,15 +11,24 @@
import Ember from 'ember'; import Ember from 'ember';
const {
inject: { service }
} = Ember;
export default Ember.Component.extend({ export default Ember.Component.extend({
pageBody: "",
appMeta: Ember.inject.service(), appMeta: Ember.inject.service(),
link: service(),
pageBody: "",
drop: null,
showSidebar: false,
didReceiveAttrs() { didReceiveAttrs() {
this.set('pageBody', this.get('meta.rawBody')); this.set('pageBody', this.get('meta.rawBody'));
}, },
didInsertElement() { didInsertElement() {
let self = this;
let options = { let options = {
selector: "#rich-text-editor", selector: "#rich-text-editor",
relative_urls: false, relative_urls: false,
@ -34,7 +43,7 @@ export default Ember.Component.extend({
image_advtab: true, image_advtab: true,
image_caption: true, image_caption: true,
media_live_embeds: 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: { formats: {
bold: { bold: {
inline: 'b' inline: 'b'
@ -48,29 +57,29 @@ export default Ember.Component.extend({
'advlist autolink lists link image charmap print preview hr anchor pagebreak', 'advlist autolink lists link image charmap print preview hr anchor pagebreak',
'searchreplace wordcount visualblocks visualchars code codesample fullscreen', 'searchreplace wordcount visualblocks visualchars code codesample fullscreen',
'insertdatetime media nonbreaking save table directionality', 'insertdatetime media nonbreaking save table directionality',
'emoticons template paste textcolor colorpicker textpattern imagetools' 'template paste textcolor colorpicker textpattern imagetools'
], ],
menu: { menu: {},
edit: { menubar: false,
title: 'Edit', toolbar1: "bold italic underline strikethrough superscript subscript | outdent indent bullist numlist forecolor backcolor | alignleft aligncenter alignright alignjustify | link unlink | table image media | hr codesample",
items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' toolbar2: "formatselect fontselect fontsizeselect | documizeLinkButton",
},
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",
save_onsavecallback: function () { save_onsavecallback: function () {
Mousetrap.trigger('ctrl+s'); 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: { 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() { isDirty() {
return is.not.undefined(tinymce) && is.not.undefined(tinymce.activeEditor) && tinymce.activeEditor.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('&nbsp;<b>It\'s my button!</b>&nbsp;');
// Selects the first paragraph found
// tinyMCE.activeEditor.selection.select(tinyMCE.activeEditor.dom.select('p')[0]);

54
app/app/services/link.js Normal file
View 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;
}
});

View file

@ -15,6 +15,7 @@
@import "base.scss"; @import "base.scss";
@import "widget/widget.scss"; @import "widget/widget.scss";
@import "view/layout.scss"; @import "view/layout.scss";
@import "view/content-linker.scss";
@import "view/page-search.scss"; @import "view/page-search.scss";
@import "view/page-documents.scss"; @import "view/page-documents.scss";
@import "view/page-settings.scss"; @import "view/page-settings.scss";

View file

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

View 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;
}
}
}

View file

@ -20,7 +20,7 @@
{{#each attachments key="id" as |a index|}} {{#each attachments key="id" as |a index|}}
<li class="item"> <li class="item">
<img class="icon" src="/assets/img/attachments/{{document/file-icon a.extension}}" /> <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> <span class="file">{{ a.filename }}</span>
</a> </a>
{{#if isEditor}} {{#if isEditor}}
@ -34,42 +34,41 @@
</div> </div>
{{/if}} {{/if}}
<div class="pages"> <div class="pages">
{{#each pages key="id" as |page index|}} {{#each pages key="id" as |page index|}}
<div class="wysiwyg"> <div class="wysiwyg">
<div id="page-{{ page.id }}" class="is-a-page" data-id="{{ page.id }}" data-type="{{ page.contentType }}"> <div id="page-{{ page.id }}" class="is-a-page" data-id="{{ page.id }}" data-type="{{ page.contentType }}">
{{document/page-heading tagName=page.tagName document=document folder=folder page=page isEditor=isEditor onDeletePage=(action 'onDeletePage')}} {{document/page-heading tagName=page.tagName document=document folder=folder page=page isEditor=isEditor onDeletePage=(action 'onDeletePage')}} {{section/base-renderer page=page}}
{{section/base-renderer page=page}} </div>
</div> </div>
</div> {{/each}}
{{/each}} </div>
</div>
<div class="dropdown-dialog delete-attachment-dialog"> <div class="dropdown-dialog delete-attachment-dialog">
<div class="content"> <div class="content">
<p>Are you sure you want to delete <span class="bold">{{deleteAttachment.name}}?</span></p> <p>Are you sure you want to delete <span class="bold">{{deleteAttachment.name}}?</span></p>
</div> </div>
<div class="actions"> <div class="actions">
<div class="flat-button" {{action 'cancel'}}> <div class="flat-button" {{action 'cancel'}}>
cancel cancel
</div> </div>
<div class="flat-button flat-red" {{action 'deleteAttachment'}}> <div class="flat-button flat-red" {{action 'deleteAttachment'}}>
delete delete
</div> </div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div> </div>
{{#if noSections}} {{#if noSections}}
<div class="no-sections"> <div class="no-sections">
<div class="box"> <div class="box">
<div class="message">Click the <div class="message">Click the
<div class="round-button-mono"> <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 class="name">section</div>
</div> </div>
to add a new section to this document</div> to add a new section to this document</div>
</div> </div>
</div> </div>
{{/if}} {{/if}}

View 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>

View file

@ -1,3 +1,10 @@
{{#section/base-editor document=document folder=folder page=page isDirty=(action 'isDirty') onCancel=(action 'onCancel') onAction=(action 'onAction')}} {{#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="{{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}} {{/section/base-editor}}

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) 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 { if err == sql.ErrNoRows {
writeNotFoundError(w, method, params["fileID"]) writeNotFoundError(w, method, params["fileID"])

View 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)
*/

View file

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

View file

@ -342,3 +342,27 @@ type SitemapDocument struct {
Folder string Folder string
Revised time.Time 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)
}

View file

@ -47,23 +47,23 @@ func (p *Persister) AddAttachment(a entity.Attachment) (err error) {
return return
} }
// GetAttachmentByJobAndFileID returns the database attachment record specified by the parameters. // GetAttachment returns the database attachment record specified by the parameters.
func (p *Persister) GetAttachmentByJobAndFileID(orgID, job, fileID string) (attachment entity.Attachment, err error) { func (p *Persister) GetAttachment(orgID, attachmentID string) (attachment entity.Attachment, err error) {
err = nil 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) defer utility.Close(stmt)
if err != nil { 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 return
} }
err = stmt.Get(&attachment, orgID, job, fileID) err = stmt.Get(&attachment, orgID, attachmentID)
if err != nil { 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 return
} }

138
core/api/request/link.go Normal file
View 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
// }

View file

@ -279,3 +279,54 @@ CREATE TABLE IF NOT EXISTS `userconfig` (
UNIQUE INDEX `idx_userconfig_orguserkey` (`orgid`, `userid`, `key` ASC)) UNIQUE INDEX `idx_userconfig_orguserkey` (`orgid`, `userid`, `key` ASC))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB; 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;

View 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;

View file

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

File diff suppressed because one or more lines are too long