diff --git a/core/database/scripts/autobuild/db_00024.sql b/core/database/scripts/autobuild/db_00024.sql index c6ea150b..ad2e9142 100644 --- a/core/database/scripts/autobuild/db_00024.sql +++ b/core/database/scripts/autobuild/db_00024.sql @@ -3,4 +3,8 @@ -- max tags per document setting ALTER TABLE organization ADD COLUMN `maxtags` INT NOT NULL DEFAULT 3 AFTER `authconfig`; +-- support for network location link types +ALTER TABLE link ADD COLUMN `externalid` NVARCHAR(1000) NOT NULL DEFAULT '' AFTER `targetid`; + -- deprecations +ALTER TABLE organization DROP COLUMN `url`; diff --git a/domain/link/link.go b/domain/link/link.go index 77713978..ea3b8642 100644 --- a/domain/link/link.go +++ b/domain/link/link.go @@ -67,6 +67,8 @@ func getLink(t html.Token) (ok bool, link link.Link) { link.TargetID = strings.TrimSpace(a.Val) case "data-link-type": link.LinkType = strings.TrimSpace(a.Val) + case "data-external-id": + link.ExternalID = strings.TrimSpace(a.Val) } } diff --git a/domain/link/mysql/store.go b/domain/link/mysql/store.go index 5ec65c1f..d2a7b4a3 100644 --- a/domain/link/mysql/store.go +++ b/domain/link/mysql/store.go @@ -36,8 +36,8 @@ func (s Scope) Add(ctx domain.RequestContext, l link.Link) (err error) { l.Created = time.Now().UTC() l.Revised = time.Now().UTC() - _, err = ctx.Transaction.Exec("INSERT INTO link (refid, orgid, folderid, userid, sourcedocumentid, sourcepageid, targetdocumentid, targetid, linktype, orphan, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - l.RefID, l.OrgID, l.FolderID, l.UserID, l.SourceDocumentID, l.SourcePageID, l.TargetDocumentID, l.TargetID, l.LinkType, l.Orphan, l.Created, l.Revised) + _, err = ctx.Transaction.Exec("INSERT INTO link (refid, orgid, folderid, userid, sourcedocumentid, sourcepageid, targetdocumentid, targetid, externalid, linktype, orphan, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + l.RefID, l.OrgID, l.FolderID, l.UserID, l.SourceDocumentID, l.SourcePageID, l.TargetDocumentID, l.TargetID, l.ExternalID, l.LinkType, l.Orphan, l.Created, l.Revised) if err != nil { err = errors.Wrap(err, "execute link insert") @@ -49,7 +49,7 @@ func (s Scope) Add(ctx domain.RequestContext, l link.Link) (err error) { // GetDocumentOutboundLinks returns outbound links for specified document. func (s Scope) GetDocumentOutboundLinks(ctx domain.RequestContext, documentID string) (links []link.Link, err error) { err = s.Runtime.Db.Select(&links, - `select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetid, l.linktype, l.orphan, l.created, l.revised + `select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetid, l.externalid, l.linktype, l.orphan, l.created, l.revised FROM link l WHERE l.orgid=? AND l.sourcedocumentid=?`, ctx.OrgID, @@ -70,7 +70,7 @@ func (s Scope) GetDocumentOutboundLinks(ctx domain.RequestContext, documentID st // GetPageLinks returns outbound links for specified page in document. func (s Scope) GetPageLinks(ctx domain.RequestContext, documentID, pageID string) (links []link.Link, err error) { err = s.Runtime.Db.Select(&links, - `select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetid, l.linktype, l.orphan, l.created, l.revised + `select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetid, l.externalid, l.linktype, l.orphan, l.created, l.revised FROM link l WHERE l.orgid=? AND l.sourcedocumentid=? AND l.sourcepageid=?`, ctx.OrgID, @@ -159,12 +159,12 @@ func (s Scope) SearchCandidates(ctx domain.RequestContext, keywords string) (doc err = s.Runtime.Db.Select(&temp, ` SELECT d.refid as documentid, d. labelid as folderid, d.title, l.label as context - FROM document d LEFT JOIN label l ON d.labelid=l.refid WHERE l.orgid=? AND `+likeQuery+` + FROM document d LEFT JOIN label l ON d.labelid=l.refid WHERE l.orgid=? AND `+likeQuery+` AND d.labelid IN ( SELECT refid FROM label WHERE orgid=? AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( - SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' + SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' UNION ALL SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space' AND p.action='view' AND (r.userid=? OR r.userid='0') @@ -197,12 +197,12 @@ func (s Scope) SearchCandidates(ctx domain.RequestContext, keywords string) (doc err = s.Runtime.Db.Select(&temp, `SELECT p.refid as targetid, p.documentid as documentid, p.title as title, p.pagetype as linktype, 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+` + 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 refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( - SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' + SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' UNION ALL SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space' AND p.action='view' AND (r.userid=? OR r.userid='0') @@ -235,12 +235,12 @@ func (s Scope) SearchCandidates(ctx domain.RequestContext, keywords string) (doc err = s.Runtime.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+` + 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 refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( - SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' + SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' UNION ALL SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space' AND p.action='view' AND (r.userid=? OR r.userid='0') diff --git a/domain/organization/mysql/store.go b/domain/organization/mysql/store.go index 912f78f9..dd717276 100644 --- a/domain/organization/mysql/store.go +++ b/domain/organization/mysql/store.go @@ -36,8 +36,8 @@ func (s Scope) AddOrganization(ctx domain.RequestContext, org org.Organization) org.Revised = time.Now().UTC() _, err = ctx.Transaction.Exec( - "INSERT INTO organization (refid, company, title, message, url, domain, email, allowanonymousaccess, serial, maxtags, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.URL), strings.ToLower(org.Domain), + "INSERT INTO organization (refid, company, title, message, domain, email, allowanonymousaccess, serial, maxtags, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.Domain), strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.MaxTags, org.Created, org.Revised) if err != nil { @@ -49,7 +49,7 @@ func (s Scope) AddOrganization(ctx domain.RequestContext, org org.Organization) // GetOrganization returns the Organization reocrod from the organization database table with the given id. func (s Scope) GetOrganization(ctx domain.RequestContext, id string) (org org.Organization, err error) { - stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE refid=?") + stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE refid=?") defer streamutil.Close(stmt) if err != nil { @@ -80,14 +80,14 @@ func (s Scope) GetOrganizationByDomain(subdomain string) (o org.Organization, er } // match on given domain name - err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE domain=? AND active=1", subdomain) + err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE domain=? AND active=1", subdomain) if err == nil { return } err = nil // match on empty domain as last resort - err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE domain='' AND active=1") + err = s.Runtime.Db.Get(&o, "SELECT id, refid, company, title, message, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, maxtags, created, revised FROM organization WHERE domain='' AND active=1") if err != nil && err != sql.ErrNoRows { err = errors.Wrap(err, "unable to execute select for empty subdomain") } diff --git a/domain/page/endpoint.go b/domain/page/endpoint.go index 3186308a..fa8ebb5e 100644 --- a/domain/page/endpoint.go +++ b/domain/page/endpoint.go @@ -464,14 +464,21 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { link.SourceDocumentID = model.Page.DocumentID link.SourcePageID = model.Page.RefID - if link.LinkType == "document" { + if link.LinkType == "document" || link.LinkType == "network" { link.TargetID = "" } + if link.LinkType != "network" { + link.ExternalID = "" + } // We check if there was a previously saved version of this link. // If we find one, we carry forward the orphan flag. for _, p := range previousLinks { - if link.TargetID == p.TargetID && link.LinkType == p.LinkType { + if link.LinkType == p.LinkType && link.TargetID == p.TargetID && link.LinkType != "network" { + link.Orphan = p.Orphan + break + } + if link.LinkType == p.LinkType && link.ExternalID == p.ExternalID && link.LinkType == "network" { link.Orphan = p.Orphan break } diff --git a/gui/app/components/document/content-linker.js b/gui/app/components/document/content-linker.js index df5f0b1a..58dbe839 100644 --- a/gui/app/components/document/content-linker.js +++ b/gui/app/components/document/content-linker.js @@ -12,9 +12,10 @@ import { debounce } from '@ember/runloop'; import { computed, set } from '@ember/object'; import { inject as service } from '@ember/service'; -import Component from '@ember/component'; +import stringUtil from '../../utils/string'; import TooltipMixin from '../../mixins/tooltip'; import ModalMixin from '../../mixins/modal'; +import Component from '@ember/component'; export default Component.extend(ModalMixin, TooltipMixin, { link: service(), @@ -26,6 +27,8 @@ export default Component.extend(ModalMixin, TooltipMixin, { showSections: computed('tab1Selected', function() { return this.get('tab1Selected'); }), showAttachments: computed('tab2Selected', function() { return this.get('tab2Selected'); }), showSearch: computed('tab3Selected', function() { return this.get('tab3Selected'); }), + showNetwork: computed('tab4Selected', function() { return this.get('tab4Selected'); }), + networkLocation: '', keywords: '', hasMatches: computed('matches', function () { let m = this.get('matches'); @@ -66,6 +69,8 @@ export default Component.extend(ModalMixin, TooltipMixin, { didRender() { this._super(...arguments); + + this.$('#content-linker-networklocation').removeClass('is-invalid'); this.renderTooltips(); }, @@ -114,7 +119,25 @@ export default Component.extend(ModalMixin, TooltipMixin, { onInsertLink() { let selection = this.get('selection'); + if (this.get('tab4Selected')) { + let loc = this.get('networkLocation').trim(); + let folderId = this.get('folder.id'); + let documentId = this.get('document.id'); + + selection = { + context: '', + documentId: documentId, + folderId: folderId, + id: stringUtil.makeId(16), + linkType: 'network', + targetId: '', + externalId: loc, + title: loc + } + } + if (is.null(selection)) { + if (this.get('tab4Selected')) this.$('#content-linker-networklocation').addClass('is-invalid').focus(); return; } @@ -125,6 +148,7 @@ export default Component.extend(ModalMixin, TooltipMixin, { this.set('tab1Selected', id === 1); this.set('tab2Selected', id === 2); this.set('tab3Selected', id === 3); + this.set('tab4Selected', id === 4); } } }); diff --git a/gui/app/components/layout/bottom-bar.js b/gui/app/components/layout/bottom-bar.js index 30c573fe..5f24dadc 100644 --- a/gui/app/components/layout/bottom-bar.js +++ b/gui/app/components/layout/bottom-bar.js @@ -17,6 +17,10 @@ export default Component.extend({ classNames: ['layout-footer', 'non-printable'], tagName: 'footer', appMeta: service(), + showWait: false, + showDone: false, + showMessage: false, + message: '', init() { this._super(...arguments); @@ -40,5 +44,17 @@ export default Component.extend({ $('.progress-done').removeClass('zoomIn').addClass('zoomOut'); }, 3000); } + + if (msg !== 'done' && msg !== 'wait') { + $('.progress-notification').removeClass('zoomOut').addClass('zoomIn'); + this.set('showWait', false); + this.set('showDone', false); + this.set('showMessage', true); + this.set('message', msg); + + setTimeout(function() { + $('.progress-notification').removeClass('zoomIn').addClass('zoomOut'); + }, 3000); + } } }); diff --git a/gui/app/services/link.js b/gui/app/services/link.js index e2c0315d..f83bab9f 100644 --- a/gui/app/services/link.js +++ b/gui/app/services/link.js @@ -10,12 +10,14 @@ // https://documize.com import Service, { inject as service } from '@ember/service'; +import Notifier from '../mixins/notifier'; -export default Service.extend({ +export default Service.extend(Notifier, { sessionService: service('session'), ajax: service(), appMeta: service(), store: service(), + eventBus: service(), // Returns links within specified document getDocumentLinks(documentId) { @@ -67,6 +69,10 @@ export default Service.extend({ href = `${endpoint}/public/attachments/${orgId}/${link.targetId}`; result = `${link.title}`; } + if (link.linkType === "network") { + href = `fileto://${link.externalId}`; + result = `${link.title}`; + } return result; }, @@ -78,11 +84,12 @@ export default Service.extend({ documentId: a.attributes["data-link-target-document-id"].value, folderId: a.attributes["data-link-space-id"].value, targetId: a.attributes["data-link-target-id"].value, + externalId: a.attributes["data-link-external-id"].value, url: a.attributes["href"].value, orphan: false }; - link.orphan = _.isEmpty(link.linkId) || _.isEmpty(link.documentId) || _.isEmpty(link.folderId) || _.isEmpty(link.targetId); + link.orphan = _.isEmpty(link.linkId) || _.isEmpty(link.documentId) || _.isEmpty(link.folderId) || (_.isEmpty(link.targetId) && _.isEmpty(link.externalId)); // we check latest state of link using database data let existing = outboundLinks.findBy('id', link.linkId); @@ -126,5 +133,23 @@ export default Service.extend({ window.location.href = link.url; return; } + + // handle network share/drive links + if (link.linkType === "network") { + // window.location.href = link.externalId; + const el = document.createElement('textarea'); + el.value = link.externalId; + el.setAttribute('readonly', ''); + el.style.position = 'absolute'; + el.style.left = '-9999px'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + + this.showNotification('Copied location to clipboard'); + + return; + } } }); diff --git a/gui/app/styles/layout/layout-footer.scss b/gui/app/styles/layout/layout-footer.scss index eb4b9de4..8668902b 100644 --- a/gui/app/styles/layout/layout-footer.scss +++ b/gui/app/styles/layout/layout-footer.scss @@ -22,6 +22,7 @@ footer { > .progress { display: inline-block; + text-align: right; > img { padding: 0; @@ -40,4 +41,11 @@ footer { width: 20px; @include border-radius(20px); } + + > .progress-notification { + display: inline-block; + font-size: 1rem; + color: $color-green; + font-weight: 500; + } } diff --git a/gui/app/templates/components/document/content-linker.hbs b/gui/app/templates/components/document/content-linker.hbs index 4e79e4a4..1b7119b3 100644 --- a/gui/app/templates/components/document/content-linker.hbs +++ b/gui/app/templates/components/document/content-linker.hbs @@ -3,15 +3,14 @@