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

click handlers for content links

This commit is contained in:
Harvey Kandola 2016-10-26 17:31:05 -07:00
parent c27de6bcab
commit 899b4f978c
27 changed files with 265 additions and 83 deletions

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,32 @@ 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() {
let link = links.getLinkObject(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) {
self.showNotification('Broken link!');
return false;
} else {
self.attrs.gotoPage(link.targetId);
return false;
}
}
links.linkClick(doc, link);
return false;
});
},
actions: {
confirmDeleteAttachment(id, name) {
this.set('deleteAttachment', {

View file

@ -18,20 +18,34 @@ const {
export default Ember.Component.extend(TooltipMixin, {
link: service(),
hasSections: false,
hasAttachments: false,
linkName: '',
keywords: '',
selection: null,
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;
}),
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(documentId, pageId).then(function (candidates) {
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);
@ -69,6 +83,10 @@ export default Ember.Component.extend(TooltipMixin, {
}
return this.get('onInsertLink')(selection);
},
onTabSelect(tabs) {
this.set('tabs', tabs);
}
}
});

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

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

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

@ -20,6 +20,7 @@
{{document/document-view document=model pages=pages attachments=attachments folder=folder folders=folders
isEditor=isEditor
gotoPage=(action 'gotoPage')
onAttachmentDeleted=(action 'onAttachmentDeleted')
onDeletePage=(action 'onPageDeleted')}}
{{/layout/zone-content}}

View file

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

View file

@ -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() {

View file

@ -19,10 +19,11 @@ export default Ember.Service.extend({
sessionService: service('session'),
ajax: service(),
appMeta: service(),
store: service(),
// Returns candidate links using provided parameters
getCandidates(documentId, pageId /*, keywords*/ ) {
return this.get('ajax').request(`links/${documentId}/${pageId}`, {
getCandidates(folderId, documentId, pageId /*, keywords*/ ) {
return this.get('ajax').request(`links/${folderId}/${documentId}/${pageId}`, {
method: 'GET'
}).then((response) => {
return response;
@ -35,45 +36,85 @@ export default Ember.Service.extend({
let endpoint = this.get('appMeta').get('endpoint');
let orgId = this.get('appMeta').get('orgId');
if (link.linkType === "section") {
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-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}`;
}
if (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-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`;
}
result = `<a data-documize='true' data-link-id='${link.id}' data-link-document-id='${link.documentId}' data-link-target-id='${link.targetId}' data-link-type='${link.linkType}' href='${href}'>${link.title}</a>`;
return result;
},
getLinkObject(a) {
let link = {
linkId: a.attributes["data-link-id"].value,
linkType: a.attributes["data-link-type"].value,
documentId: a.attributes["data-link-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);
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.inkType === "document") {
router.transitionTo('document', link.folderId, folderSlug, link.documentId, documentSlug);
return;
}
// handle attachment links
if (link.linkType === "file") {
window.location.href = link.url;
return;
}
}
});
/*
Keyword search results - docs, section, files
link handler
- implement link redirect handler --
- for documents: client-side detect
- for sections:
- for attachments: direct link
-
The link id's get ZERO'd in Page.Body whenever:
- doc is moved to different space
- doc is deleted (set to ZERO and marked as orphan)
- page is deleted (set to ZERO and marked as orphan)
- page is moved to different doc (update data-document-id attribute value)
- attachment is deleted (remove HREF)
onDelete document/section/file:
- mark link table row as ORPHAN
- doc view: meta data fetch to load orphaned content
link/section/{documentId}/{sectionId}:
- if ZERO id show notification
- store previous positions -- localStorage, dropdown menu?
Keyword search results - docs, section, files
Markdown editor support
we should not redirect to a link that is in the same document!
what happens if we delete attachment?
UpdatePage(): find and persist links from saved content
1. We need to deal with links server-side
2. We need to click on links in the browser and 'navigate' to linked content
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]);
permission checks:
can view space
can view document
*/

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

@ -196,7 +196,7 @@ $i: 100;
.width-#{$i} {
width: #{$i}#{"%"};
}
$i: $i - 2;
$i: $i - 1;
}
.no-outline {

View file

@ -1,10 +1,10 @@
.edit-tools {
margin: 15px 0 0 20px;
margin: 0 0 0 20px;
min-height: 500px;
}
.content-linker-dialog {
width: 300px;
width: 350px;
height: 400px;
overflow-y: auto;

View file

@ -0,0 +1,27 @@
.widget-tab {
width: 100%;
margin: 0;
padding: 0 5px;
text-align: center;
border-bottom: 1px solid $color-border;
> .tab {
display: inline-block;
margin: 0;
padding: 5px 10px;
color: $color-off-black;
text-align: center;
cursor: pointer;
@include ease-in();
&:hover {
background-color: $color-off-white;
color: $color-link;
}
}
> .selected {
background-color: $color-off-white;
color: $color-link;
}
}

View file

@ -71,3 +71,4 @@
@import "widget-table";
@import "widget-tooltip";
@import "widget-checkbox";
@import "widget-tab";

View file

@ -4,15 +4,14 @@
<i class="material-icons color-white">link</i>
</div>
{{#dropdown-dialog target="content-linker-button" focusOn="content-linker-search" position="bottom right" button="Insert" color="flat-blue" onAction=(action 'onInsertLink')}}
{{#dropdown-dialog target="content-linker-button" position="bottom right" button="Insert" color="flat-blue" onAction=(action 'onInsertLink')}}
<div class="content-linker-dialog">
<form>
<div class="input-control">
<label>Insert Link</label>
<div class="tip">Choose content below or search</div>
{{focus-input id="content-linker-search" type="input" value=keywords placeholder="keyword search"}}
</div>
{{#if hasSections}}
{{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 }}>
@ -23,7 +22,8 @@
{{/each}}
</ul>
{{/if}}
{{#if hasAttachments}}
{{#if showAttachments}}
<ul class="link-list">
{{#each candidates.attachments as |a|}}
<li class="link-item" {{ action 'setSelection' a }}>
@ -35,6 +35,15 @@
{{/each}}
</ul>
{{/if}}
{{#if showSearch}}
<div class="input-control">
<label>Search</label>
<div class="tip">keywords</div>
{{focus-input id="content-linker-search" type="input" value=keywords placeholder="keyword search"}}
</div>
{{/if}}
<div class="hide regular-button button-blue pull-right" {{ action 'onInsertLink' }}>Insert</div>
<div class="hide clearfix" />
</form>

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

@ -29,6 +29,7 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) {
p := request.GetPersister(r)
params := mux.Vars(r)
folderID := params["folderID"]
documentID := params["documentID"]
pageID := params["pageID"]
@ -68,6 +69,7 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) {
if p.RefID != pageID {
c := entity.LinkCandidate{
RefID: util.UniqueID(),
FolderID: folderID,
DocumentID: documentID,
TargetID: p.RefID,
LinkType: "section",
@ -95,6 +97,7 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) {
for _, f := range files {
c := entity.LinkCandidate{
RefID: util.UniqueID(),
FolderID: folderID,
DocumentID: documentID,
TargetID: f.RefID,
LinkType: "file",
@ -124,13 +127,3 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) {
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

@ -213,7 +213,7 @@ func init() {
log.IfErr(Add(RoutePrefixPrivate, "sections/refresh", []string{"GET", "OPTIONS"}, nil, RefreshSections))
// Links
log.IfErr(Add(RoutePrefixPrivate, "links/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates))
log.IfErr(Add(RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates))
// Global installation-wide config
log.IfErr(Add(RoutePrefixPrivate, "global", []string{"GET", "OPTIONS"}, nil, GetGlobalConfig))

View file

@ -347,6 +347,7 @@ type SitemapDocument struct {
type Link struct {
BaseEntity
OrgID string `json:"orgId"`
FolderID string `json:"folderId"`
UserID string `json:"userId"`
LinkType string `json:"linkType"`
SourceID string `json:"sourceId"`
@ -359,6 +360,7 @@ type Link struct {
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

View file

@ -27,7 +27,7 @@ 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, userid, sourceid, documentid, targetid, linktype, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
stmt, err := p.Context.Transaction.Preparex("INSERT INTO link (refid, orgid, folderid, userid, sourceid, documentid, targetid, linktype, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer utility.Close(stmt)
if err != nil {
@ -35,7 +35,7 @@ func (p *Persister) AddContentLink(l entity.Link) (err error) {
return
}
_, err = stmt.Exec(l.RefID, l.OrgID, l.UserID, l.SourceID, l.DocumentID, l.TargetID, l.LinkType, l.Created, l.Revised)
_, err = stmt.Exec(l.RefID, l.OrgID, l.FolderID, l.UserID, l.SourceID, l.DocumentID, l.TargetID, l.LinkType, l.Created, l.Revised)
if err != nil {
log.Error("Unable to execute insert for link", err)
@ -49,7 +49,7 @@ func (p *Persister) AddContentLink(l entity.Link) (err error) {
func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, err error) {
err = nil
sql := "SELECT id,refid,orgid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND sourceid=?"
sql := "SELECT id,refid,orgid,folderid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND sourceid=?"
err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
@ -65,7 +65,7 @@ func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, e
func (p *Persister) GetContentLinksForSection(sectionID string) (links []entity.Link, err error) {
err = nil
sql := "SELECT id,refid,orgid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND sectionid=?"
sql := "SELECT id,refid,orgid,folderid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND sectionid=?"
err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
@ -81,7 +81,7 @@ func (p *Persister) GetContentLinksForSection(sectionID string) (links []entity.
func (p *Persister) GetContentLinksForDocument(documentID string) (links []entity.Link, err error) {
err = nil
sql := "SELECT id,refid,orgid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND documentid=?"
sql := "SELECT id,refid,orgid,folderid,userid,sourceid,documentid,targetid,linktype,orphan,created,revised from link WHERE orgid=? AND documentid=?"
err = Db.Select(&links, sql, p.Context.OrgID, documentID)

View file

@ -287,7 +287,7 @@ func (p *Persister) UpdatePage(page entity.Page, refID, userID string, skipRevis
//}
//}
// fimnd any content links
// find any content links in the HTML
links := util.GetContentLinks(page.Body)
// delete previous content links for this page

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)

View file

@ -60,6 +60,8 @@ func getLink(t html.Token) (ok bool, link entity.Link) {
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-document-id":
link.DocumentID = strings.TrimSpace(a.Val)
case "data-link-target-id":

View file

@ -319,7 +319,8 @@ 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,
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`folderid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourceid` CHAR(16) NOT NULL COLLATE utf8_bin,
`linktype` CHAR(16) NOT NULL COLLATE utf8_bin,
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin,

View file

@ -5,7 +5,8 @@ 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,
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`folderid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) NOT NULL COLLATE utf8_bin,
`sourceid` CHAR(16) NOT NULL COLLATE utf8_bin,
`linktype` CHAR(16) NOT NULL COLLATE utf8_bin,
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin,