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

search results for links

This commit is contained in:
Harvey Kandola 2016-10-27 13:40:54 -07:00
parent 899b4f978c
commit ad716a23ba
12 changed files with 390 additions and 69 deletions

View file

@ -21,6 +21,11 @@ export default Ember.Component.extend(TooltipMixin, {
linkName: '', linkName: '',
keywords: '', keywords: '',
selection: null, selection: null,
matches: {
documents: [],
pages: [],
attachments: []
},
tabs: [ tabs: [
{ label: 'Section', selected: true }, { label: 'Section', selected: true },
{ label: 'Attachment', selected: false }, { label: 'Attachment', selected: false },
@ -36,6 +41,11 @@ export default Ember.Component.extend(TooltipMixin, {
showSearch: Ember.computed('tabs.@each.selected', function() { showSearch: Ember.computed('tabs.@each.selected', function() {
return this.get('tabs').findBy('label', 'Search').selected; 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() { init() {
this._super(...arguments); this._super(...arguments);
@ -60,11 +70,30 @@ export default Ember.Component.extend(TooltipMixin, {
this.destroyTooltips(); 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: { actions: {
setSelection(i) { setSelection(i) {
this.set('selection', i);
let candidates = this.get('candidates'); let candidates = this.get('candidates');
let matches = this.get('matches');
this.set('selection', i);
candidates.pages.forEach(c => { candidates.pages.forEach(c => {
Ember.set(c, 'selected', c.id === i.id); Ember.set(c, 'selected', c.id === i.id);
@ -73,6 +102,18 @@ export default Ember.Component.extend(TooltipMixin, {
candidates.attachments.forEach(c => { candidates.attachments.forEach(c => {
Ember.set(c, 'selected', c.id === i.id); 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() { onInsertLink() {

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

@ -22,7 +22,7 @@ export default Ember.Service.extend({
store: service(), store: service(),
// Returns candidate links using provided parameters // Returns candidate links using provided parameters
getCandidates(folderId, documentId, pageId /*, keywords*/ ) { getCandidates(folderId, documentId, pageId) {
return this.get('ajax').request(`links/${folderId}/${documentId}/${pageId}`, { return this.get('ajax').request(`links/${folderId}/${documentId}/${pageId}`, {
method: 'GET' method: 'GET'
}).then((response) => { }).then((response) => {
@ -30,6 +30,27 @@ export default Ember.Service.extend({
}); });
}, },
// 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) { buildLink(link) {
let result = ""; let result = "";
let href = ""; let href = "";
@ -45,7 +66,6 @@ export default Ember.Service.extend({
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-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>`;
} }
return result; return result;
}, },
@ -85,7 +105,7 @@ export default Ember.Service.extend({
} }
// handle document link // handle document link
if (link.inkType === "document") { if (link.linkType === "document") {
router.transitionTo('document', link.folderId, folderSlug, link.documentId, documentSlug); router.transitionTo('document', link.folderId, folderSlug, link.documentId, documentSlug);
return; return;
} }
@ -99,7 +119,6 @@ export default Ember.Service.extend({
}); });
/* /*
Keyword search results - docs, section, files
The link id's get ZERO'd in Page.Body whenever: The link id's get ZERO'd in Page.Body whenever:
- doc is moved to different space - doc is moved to different space

View file

@ -1,11 +1,11 @@
.edit-tools { .edit-tools {
margin: 0 0 0 20px; margin: 0 0 0 20px;
min-height: 500px; min-height: 600px;
} }
.content-linker-dialog { .content-linker-dialog {
width: 350px; width: 350px;
height: 400px; height: 500px;
overflow-y: auto; overflow-y: auto;
.link-list { .link-list {
@ -14,7 +14,7 @@
.link-item { .link-item {
margin: 0; margin: 0;
padding: 2px 0; padding: 0;
font-size: 0.9rem; font-size: 0.9rem;
color: $color-gray; color: $color-gray;
cursor: pointer; cursor: pointer;

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

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

View file

@ -60,6 +60,7 @@
.z-depth-5 { .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"; @import "widget-avatar";
@import "widget-button"; @import "widget-button";
@import "widget-card"; @import "widget-card";
@ -72,3 +73,4 @@
@import "widget-tooltip"; @import "widget-tooltip";
@import "widget-checkbox"; @import "widget-checkbox";
@import "widget-tab"; @import "widget-tab";
@import "widget-selection";

View file

@ -15,9 +15,9 @@
<ul class="link-list"> <ul class="link-list">
{{#each candidates.pages as |p|}} {{#each candidates.pages as |p|}}
<li class="link-item" {{ action 'setSelection' p }}> <li class="link-item" {{ action 'setSelection' p }}>
{{#ui/ui-checkbox selected=p.selected}} {{#ui/ui-selection selected=p.selected}}
{{p.title}} {{p.title}}
{{/ui/ui-checkbox}} {{/ui/ui-selection}}
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
@ -27,10 +27,10 @@
<ul class="link-list"> <ul class="link-list">
{{#each candidates.attachments as |a|}} {{#each candidates.attachments as |a|}}
<li class="link-item" {{ action 'setSelection' a }}> <li class="link-item" {{ action 'setSelection' a }}>
{{#ui/ui-checkbox selected=a.selected}} {{#ui/ui-selection selected=a.selected}}
<img class="icon" src="/assets/img/attachments/{{document/file-icon a.context}}" /> <img class="icon" src="/assets/img/attachments/{{document/file-icon a.context}}" />
{{ a.title }} {{ a.title }}
{{/ui/ui-checkbox}} {{/ui/ui-selection}}
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
@ -39,9 +39,36 @@
{{#if showSearch}} {{#if showSearch}}
<div class="input-control"> <div class="input-control">
<label>Search</label> <label>Search</label>
<div class="tip">keywords</div> <div class="tip">For content or attachments</div>
{{focus-input id="content-linker-search" type="input" value=keywords placeholder="keyword search"}} {{focus-input id="content-linker-search" type="input" value=keywords placeholder="keyword search"}}
</div> </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}} {{/if}}
<div class="hide regular-button button-blue pull-right" {{ action 'onInsertLink' }}>Insert</div> <div class="hide regular-button button-blue pull-right" {{ action 'onInsertLink' }}>Insert</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

@ -15,12 +15,14 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/url"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/documize/community/core/api/entity" "github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request" "github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/util" "github.com/documize/community/core/api/util"
"github.com/documize/community/core/log"
) )
// GetLinkCandidates returns references to documents/sections/attachments. // GetLinkCandidates returns references to documents/sections/attachments.
@ -112,7 +114,6 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) {
var payload struct { var payload struct {
Pages []entity.LinkCandidate `json:"pages"` Pages []entity.LinkCandidate `json:"pages"`
Attachments []entity.LinkCandidate `json:"attachments"` Attachments []entity.LinkCandidate `json:"attachments"`
Matches []entity.LinkCandidate `json:"matches"`
} }
payload.Pages = pc payload.Pages = pc
@ -127,3 +128,40 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) {
util.WriteSuccessBytes(w, json) 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

@ -214,6 +214,7 @@ func init() {
// Links // Links
log.IfErr(Add(RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates)) 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 // Global installation-wide config
log.IfErr(Add(RoutePrefixPrivate, "global", []string{"GET", "OPTIONS"}, nil, GetGlobalConfig)) log.IfErr(Add(RoutePrefixPrivate, "global", []string{"GET", "OPTIONS"}, nil, GetGlobalConfig))

View file

@ -16,6 +16,7 @@ import (
"time" "time"
"github.com/documize/community/core/api/entity" "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/log"
"github.com/documize/community/core/utility" "github.com/documize/community/core/utility"
) )
@ -45,53 +46,188 @@ func (p *Persister) AddContentLink(l entity.Link) (err error) {
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
}
// GetReferencedLinks returns all links that the specified section is referencing. // GetReferencedLinks returns all links that the specified section is referencing.
func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, err error) { // func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, err error) {
err = nil // err = nil
//
sql := "SELECT id,refid,orgid,folderid,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) // err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
//
if err != nil { // if err != nil {
log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) // log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
return // return
} // }
//
return // return
} // }
//
// GetContentLinksForSection returns all links that are linking to the specified section. // // GetContentLinksForSection returns all links that are linking to the specified section.
func (p *Persister) GetContentLinksForSection(sectionID string) (links []entity.Link, err error) { // func (p *Persister) GetContentLinksForSection(sectionID string) (links []entity.Link, err error) {
err = nil // err = nil
//
sql := "SELECT id,refid,orgid,folderid,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) // err = Db.Select(&links, sql, p.Context.OrgID, sectionID)
//
if err != nil { // if err != nil {
log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) // log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
return // return
} // }
//
return // return
} // }
//
// GetContentLinksForDocument returns all links that are linking to the specified document. // // GetContentLinksForDocument returns all links that are linking to the specified document.
func (p *Persister) GetContentLinksForDocument(documentID string) (links []entity.Link, err error) { // func (p *Persister) GetContentLinksForDocument(documentID string) (links []entity.Link, err error) {
err = nil // err = nil
//
sql := "SELECT id,refid,orgid,folderid,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) // err = Db.Select(&links, sql, p.Context.OrgID, documentID)
//
if err != nil { // if err != nil {
log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) // log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err)
return // return
} // }
//
return // return
} // }
// MarkOrphanContentLink marks the link record as being invalid. // MarkOrphanContentLink marks the link record as being invalid.
func (p *Persister) MarkOrphanContentLink(l entity.Link) (err error) { func (p *Persister) MarkOrphanContentLink(l entity.Link) (err error) {