From ad716a23ba120c5127b570f60ea32940a4bfc812 Mon Sep 17 00:00:00 2001 From: Harvey Kandola Date: Thu, 27 Oct 2016 13:40:54 -0700 Subject: [PATCH] search results for links --- app/app/components/document/edit-tools.js | 45 +++- app/app/components/ui/ui-selection.js | 15 ++ app/app/services/link.js | 27 ++- app/app/styles/view/document/edit-tools.scss | 6 +- app/app/styles/widget/widget-selection.scss | 34 +++ app/app/styles/widget/widget-tab.scss | 18 +- app/app/styles/widget/widget.scss | 2 + .../components/document/edit-tools.hbs | 37 ++- .../templates/components/ui/ui-selection.hbs | 6 + core/api/endpoint/link_endpoint.go | 40 ++- core/api/endpoint/router.go | 1 + core/api/request/link.go | 228 ++++++++++++++---- 12 files changed, 390 insertions(+), 69 deletions(-) create mode 100644 app/app/components/ui/ui-selection.js create mode 100644 app/app/styles/widget/widget-selection.scss create mode 100644 app/app/templates/components/ui/ui-selection.hbs diff --git a/app/app/components/document/edit-tools.js b/app/app/components/document/edit-tools.js index d507ea0d..6563e794 100644 --- a/app/app/components/document/edit-tools.js +++ b/app/app/components/document/edit-tools.js @@ -21,6 +21,11 @@ export default Ember.Component.extend(TooltipMixin, { linkName: '', keywords: '', selection: null, + matches: { + documents: [], + pages: [], + attachments: [] + }, tabs: [ { label: 'Section', selected: true }, { label: 'Attachment', selected: false }, @@ -36,6 +41,11 @@ export default Ember.Component.extend(TooltipMixin, { showSearch: Ember.computed('tabs.@each.selected', function() { return this.get('tabs').findBy('label', 'Search').selected; }), + hasMatches: Ember.computed('matches', function() { + let m = this.get('matches'); + return m.documents.length || m.pages.length || m.attachments.length; + }), + init() { this._super(...arguments); @@ -60,11 +70,30 @@ export default Ember.Component.extend(TooltipMixin, { this.destroyTooltips(); }, + onKeywordChange: function () { + Ember.run.debounce(this, this.fetch, 750); + }.observes('keywords'), + + fetch() { + let keywords = this.get('keywords'); + let self = this; + + if (_.isEmpty(keywords)) { + this.set('matches', { documents: [], pages: [], attachments: [] }); + return; + } + + this.get('link').searchCandidates(keywords).then(function (matches) { + self.set('matches', matches); + }); + }, + actions: { setSelection(i) { - this.set('selection', i); - let candidates = this.get('candidates'); + let matches = this.get('matches'); + + this.set('selection', i); candidates.pages.forEach(c => { Ember.set(c, 'selected', c.id === i.id); @@ -73,6 +102,18 @@ export default Ember.Component.extend(TooltipMixin, { candidates.attachments.forEach(c => { Ember.set(c, 'selected', c.id === i.id); }); + + matches.documents.forEach(c => { + Ember.set(c, 'selected', c.id === i.id); + }); + + matches.pages.forEach(c => { + Ember.set(c, 'selected', c.id === i.id); + }); + + matches.attachments.forEach(c => { + Ember.set(c, 'selected', c.id === i.id); + }); }, onInsertLink() { diff --git a/app/app/components/ui/ui-selection.js b/app/app/components/ui/ui-selection.js new file mode 100644 index 00000000..cb25c125 --- /dev/null +++ b/app/app/components/ui/ui-selection.js @@ -0,0 +1,15 @@ +// Copyright 2016 Documize Inc. . 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 . +// +// https://documize.com + +import Ember from 'ember'; + +export default Ember.Component.extend({ +}); diff --git a/app/app/services/link.js b/app/app/services/link.js index 3f4b9060..58eeae8b 100644 --- a/app/app/services/link.js +++ b/app/app/services/link.js @@ -22,7 +22,7 @@ export default Ember.Service.extend({ store: service(), // Returns candidate links using provided parameters - getCandidates(folderId, documentId, pageId /*, keywords*/ ) { + getCandidates(folderId, documentId, pageId) { return this.get('ajax').request(`links/${folderId}/${documentId}/${pageId}`, { method: 'GET' }).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) { let result = ""; let href = ""; @@ -45,7 +66,6 @@ export default Ember.Service.extend({ result = `${link.title}`; } - return result; }, @@ -85,7 +105,7 @@ export default Ember.Service.extend({ } // handle document link - if (link.inkType === "document") { + if (link.linkType === "document") { router.transitionTo('document', link.folderId, folderSlug, link.documentId, documentSlug); 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: - doc is moved to different space diff --git a/app/app/styles/view/document/edit-tools.scss b/app/app/styles/view/document/edit-tools.scss index 1cef28f6..466d2e80 100644 --- a/app/app/styles/view/document/edit-tools.scss +++ b/app/app/styles/view/document/edit-tools.scss @@ -1,11 +1,11 @@ .edit-tools { margin: 0 0 0 20px; - min-height: 500px; + min-height: 600px; } .content-linker-dialog { width: 350px; - height: 400px; + height: 500px; overflow-y: auto; .link-list { @@ -14,7 +14,7 @@ .link-item { margin: 0; - padding: 2px 0; + padding: 0; font-size: 0.9rem; color: $color-gray; cursor: pointer; diff --git a/app/app/styles/widget/widget-selection.scss b/app/app/styles/widget/widget-selection.scss new file mode 100644 index 00000000..0476c322 --- /dev/null +++ b/app/app/styles/widget/widget-selection.scss @@ -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; + } + } +} diff --git a/app/app/styles/widget/widget-tab.scss b/app/app/styles/widget/widget-tab.scss index d3f7ef4d..281ddfef 100644 --- a/app/app/styles/widget/widget-tab.scss +++ b/app/app/styles/widget/widget-tab.scss @@ -1,27 +1,29 @@ .widget-tab { width: 100%; margin: 0; - padding: 0 5px; + padding: 0 10px; text-align: center; - border-bottom: 1px solid $color-border; + @extend .no-select; > .tab { display: inline-block; margin: 0; padding: 5px 10px; - color: $color-off-black; + background-color: $color-off-white; + color: $color-gray; text-align: center; cursor: pointer; - @include ease-in(); + margin-right: -3px; &:hover { - background-color: $color-off-white; - color: $color-link; + @include ease-in(); + background-color: $color-gray; + color: $color-off-white; } } > .selected { - background-color: $color-off-white; - color: $color-link; + background-color: $color-gray; + color: $color-off-white; } } diff --git a/app/app/styles/widget/widget.scss b/app/app/styles/widget/widget.scss index c88bdce8..376f6187 100644 --- a/app/app/styles/widget/widget.scss +++ b/app/app/styles/widget/widget.scss @@ -60,6 +60,7 @@ .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); } + @import "widget-avatar"; @import "widget-button"; @import "widget-card"; @@ -72,3 +73,4 @@ @import "widget-tooltip"; @import "widget-checkbox"; @import "widget-tab"; +@import "widget-selection"; diff --git a/app/app/templates/components/document/edit-tools.hbs b/app/app/templates/components/document/edit-tools.hbs index c3aa3e48..2237ed8e 100644 --- a/app/app/templates/components/document/edit-tools.hbs +++ b/app/app/templates/components/document/edit-tools.hbs @@ -15,9 +15,9 @@ @@ -27,10 +27,10 @@ @@ -39,9 +39,36 @@ {{#if showSearch}}
-
keywords
+
For content or attachments
{{focus-input id="content-linker-search" type="input" value=keywords placeholder="keyword search"}}
+ {{#unless hasMatches}} + Nothing found. + {{/unless}} + {{/if}}
Insert
diff --git a/app/app/templates/components/ui/ui-selection.hbs b/app/app/templates/components/ui/ui-selection.hbs new file mode 100644 index 00000000..ee451e88 --- /dev/null +++ b/app/app/templates/components/ui/ui-selection.hbs @@ -0,0 +1,6 @@ +
+
+ {{yield}} + check +
+
diff --git a/core/api/endpoint/link_endpoint.go b/core/api/endpoint/link_endpoint.go index f01f2a6b..7ff5d9ee 100644 --- a/core/api/endpoint/link_endpoint.go +++ b/core/api/endpoint/link_endpoint.go @@ -15,12 +15,14 @@ import ( "database/sql" "encoding/json" "net/http" + "net/url" "github.com/gorilla/mux" "github.com/documize/community/core/api/entity" "github.com/documize/community/core/api/request" "github.com/documize/community/core/api/util" + "github.com/documize/community/core/log" ) // GetLinkCandidates returns references to documents/sections/attachments. @@ -112,7 +114,6 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) { var payload struct { Pages []entity.LinkCandidate `json:"pages"` Attachments []entity.LinkCandidate `json:"attachments"` - Matches []entity.LinkCandidate `json:"matches"` } payload.Pages = pc @@ -127,3 +128,40 @@ func GetLinkCandidates(w http.ResponseWriter, r *http.Request) { 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) +} diff --git a/core/api/endpoint/router.go b/core/api/endpoint/router.go index 2d5aa904..ddbfa9a3 100644 --- a/core/api/endpoint/router.go +++ b/core/api/endpoint/router.go @@ -214,6 +214,7 @@ func init() { // Links log.IfErr(Add(RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, GetLinkCandidates)) + log.IfErr(Add(RoutePrefixPrivate, "links", []string{"GET", "OPTIONS"}, nil, SearchLinkCandidates)) // Global installation-wide config log.IfErr(Add(RoutePrefixPrivate, "global", []string{"GET", "OPTIONS"}, nil, GetGlobalConfig)) diff --git a/core/api/request/link.go b/core/api/request/link.go index fbf5be59..00a87f1c 100644 --- a/core/api/request/link.go +++ b/core/api/request/link.go @@ -16,6 +16,7 @@ import ( "time" "github.com/documize/community/core/api/entity" + "github.com/documize/community/core/api/util" "github.com/documize/community/core/log" "github.com/documize/community/core/utility" ) @@ -45,53 +46,188 @@ func (p *Persister) AddContentLink(l entity.Link) (err error) { 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. -func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, err error) { - err = nil - - 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) - - if err != nil { - log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) - return - } - - return -} - -// GetContentLinksForSection returns all links that are linking to the specified section. -func (p *Persister) GetContentLinksForSection(sectionID string) (links []entity.Link, err error) { - err = nil - - 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) - - if err != nil { - log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) - return - } - - return -} - -// GetContentLinksForDocument returns all links that are linking to the specified document. -func (p *Persister) GetContentLinksForDocument(documentID string) (links []entity.Link, err error) { - err = nil - - 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) - - if err != nil { - log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) - return - } - - return -} +// func (p *Persister) GetReferencedLinks(sectionID string) (links []entity.Link, err error) { +// err = nil +// +// 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) +// +// if err != nil { +// log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) +// return +// } +// +// return +// } +// +// // GetContentLinksForSection returns all links that are linking to the specified section. +// func (p *Persister) GetContentLinksForSection(sectionID string) (links []entity.Link, err error) { +// err = nil +// +// 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) +// +// if err != nil { +// log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) +// return +// } +// +// return +// } +// +// // GetContentLinksForDocument returns all links that are linking to the specified document. +// func (p *Persister) GetContentLinksForDocument(documentID string) (links []entity.Link, err error) { +// err = nil +// +// 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) +// +// if err != nil { +// log.Error(fmt.Sprintf("Unable to execute select links for org %s", p.Context.OrgID), err) +// return +// } +// +// return +// } // MarkOrphanContentLink marks the link record as being invalid. func (p *Persister) MarkOrphanContentLink(l entity.Link) (err error) {