diff --git a/app/app/components/document/document-view.js b/app/app/components/document/document-view.js index 297c2af5..526e9cd2 100644 --- a/app/app/components/document/document-view.js +++ b/app/app/components/document/document-view.js @@ -71,21 +71,26 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, { let self = this; $("a[data-documize='true']").off('click').on('click', function() { - let link = links.getLinkObject(this); + let link = links.getLinkObject(self.get('meta.outboundLinks'), 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; + if (_.isUndefined(exists)) { + link.orphan = true; } else { self.attrs.gotoPage(link.targetId); return false; } } + if (link.orphan) { + $(this).addClass('broken-link'); + self.showNotification('Broken link!'); + return false; + } + links.linkClick(doc, link); return false; }); diff --git a/app/app/pods/document/index/template.hbs b/app/app/pods/document/index/template.hbs index 3302f944..b808c5d4 100644 --- a/app/app/pods/document/index/template.hbs +++ b/app/app/pods/document/index/template.hbs @@ -18,7 +18,7 @@ onAttachmentUpload=(action 'onAttachmentUpload') onDocumentDelete=(action 'onDocumentDelete')}} - {{document/document-view document=model pages=pages attachments=attachments folder=folder folders=folders + {{document/document-view document=model meta=meta pages=pages attachments=attachments folder=folder folders=folders isEditor=isEditor gotoPage=(action 'gotoPage') onAttachmentDeleted=(action 'onAttachmentDeleted') diff --git a/app/app/services/link.js b/app/app/services/link.js index 58eeae8b..030d58b3 100644 --- a/app/app/services/link.js +++ b/app/app/services/link.js @@ -50,7 +50,6 @@ export default Ember.Service.extend({ }); }, - buildLink(link) { let result = ""; let href = ""; @@ -59,21 +58,21 @@ export default Ember.Service.extend({ if (link.linkType === "section" || link.linkType === "document") { href = `/link/${link.linkType}/${link.id}`; - result = `${link.title}`; + result = `${link.title}`; } if (link.linkType === "file") { href = `${endpoint}/public/attachments/${orgId}/${link.targetId}`; - result = `${link.title}`; + result = `${link.title}`; } return result; }, - getLinkObject(a) { + getLinkObject(outboundLinks, 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, + 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, url: a.attributes["href"].value, @@ -82,6 +81,15 @@ export default Ember.Service.extend({ link.orphan = _.isEmpty(link.linkId) || _.isEmpty(link.documentId) || _.isEmpty(link.folderId) || _.isEmpty(link.targetId); + // we check latest state of link using database data + let existing = outboundLinks.findBy('id', link.linkId); + + if (_.isUndefined(existing)) { + link.orphan = true; + } else { + link.orphan = existing.orphan; + } + return link; }, @@ -119,21 +127,12 @@ export default Ember.Service.extend({ }); /* - - 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) - - link/section/{documentId}/{sectionId}: - - if ZERO id show notification - - store previous positions -- localStorage, dropdown menu? - - Markdown editor support + when attachment deleted: + mark as orphan references where link.documentid = document.refId permission checks: can view space can view document + + Markdown editor support */ diff --git a/app/app/styles/base.scss b/app/app/styles/base.scss index c300ac56..49668131 100644 --- a/app/app/styles/base.scss +++ b/app/app/styles/base.scss @@ -132,6 +132,11 @@ a { } } +a.broken-link { + color: $color-red; + text-decoration: line-through; +} + a.alt { color: $color-blue; text-decoration: none; diff --git a/core/api/entity/objects.go b/core/api/entity/objects.go index 75dbfa7c..a50cb353 100644 --- a/core/api/entity/objects.go +++ b/core/api/entity/objects.go @@ -222,8 +222,10 @@ func (p *PageMeta) SetDefaults() { // DocumentMeta details who viewed the document. type DocumentMeta struct { - Viewers []DocumentMetaViewer `json:"viewers"` - Editors []DocumentMetaEditor `json:"editors"` + Viewers []DocumentMetaViewer `json:"viewers"` + Editors []DocumentMetaEditor `json:"editors"` + InboundLinks []Link `json:"inboundLinks"` + OutboundLinks []Link `json:"outboundLinks"` } // DocumentMetaViewer contains the "view" metatdata content. @@ -346,14 +348,15 @@ type SitemapDocument struct { // Link defines a reference between a section and another document/section/attachment. type Link struct { BaseEntity - OrgID string `json:"orgId"` - FolderID string `json:"folderId"` - UserID string `json:"userId"` - LinkType string `json:"linkType"` - SourceID string `json:"sourceId"` - DocumentID string `json:"documentId"` - TargetID string `json:"targetId"` - Orphan bool `json:"orphan"` + OrgID string `json:"orgId"` + FolderID string `json:"folderId"` + UserID string `json:"userId"` + LinkType string `json:"linkType"` + SourceDocumentID string `json:"sourceDocumentId"` + SourcePageID string `json:"sourcePageId"` + TargetDocumentID string `json:"targetDocumentId"` + TargetPageID string `json:"targetPageId"` + Orphan bool `json:"orphan"` } // LinkCandidate defines a potential link to a document/section/attachment. diff --git a/core/api/request/document.go b/core/api/request/document.go index 63fe0f95..6a4bf2f2 100644 --- a/core/api/request/document.go +++ b/core/api/request/document.go @@ -107,6 +107,13 @@ func (p *Persister) GetDocumentMeta(id string) (meta entity.DocumentMeta, err er return } + meta.OutboundLinks, err = p.GetDocumentOutboundLinks(id) + + if err != nil { + log.Error(fmt.Sprintf("Unable to execute GetDocumentOutboundLinks for document %s", id), err) + return + } + return } @@ -400,6 +407,18 @@ func (p *Persister) DeleteDocument(documentID string) (rows int64, err error) { return } + // Mark references to this document as orphaned + err = p.MarkOrphanDocumentLink(documentID) + if err != nil { + return + } + + // Remove all references from this document + _, err = p.DeleteSourceDocumentLinks(documentID) + if err != nil { + return + } + p.Base.Audit(p.Context, "delete-document", documentID, "") return p.Base.DeleteConstrained(p.Context.Transaction, "document", p.Context.OrgID, documentID) diff --git a/core/api/request/link.go b/core/api/request/link.go index 00a87f1c..e44204f3 100644 --- a/core/api/request/link.go +++ b/core/api/request/link.go @@ -24,11 +24,10 @@ import ( // AddContentLink inserts wiki-link into the store. // These links exist when content references another document or content. func (p *Persister) AddContentLink(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, folderid, userid, sourceid, documentid, targetid, linktype, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + stmt, err := p.Context.Transaction.Preparex("INSERT INTO link (refid, orgid, folderid, userid, sourcedocumentid, sourcepageid, targetdocumentid, targetpageid, linktype, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") defer utility.Close(stmt) if err != nil { @@ -36,7 +35,7 @@ func (p *Persister) AddContentLink(l entity.Link) (err error) { return } - _, err = stmt.Exec(l.RefID, l.OrgID, l.FolderID, 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.SourceDocumentID, l.SourcePageID, l.TargetDocumentID, l.TargetPageID, l.LinkType, l.Created, l.Revised) if err != nil { log.Error("Unable to execute insert for link", err) @@ -181,80 +180,78 @@ func (p *Persister) SearchLinkCandidates(keywords string) (docs []entity.LinkCan 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 -// } +// GetDocumentOutboundLinks returns outbound links for specified document. +func (p *Persister) GetDocumentOutboundLinks(documentID string) (links []entity.Link, err error) { + err = nil -// MarkOrphanContentLink marks the link record as being invalid. -func (p *Persister) MarkOrphanContentLink(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) + err = Db.Select(&links, + `select l.refid, l.orgid, l.folderid, l.userid, l.sourcedocumentid, l.sourcepageid, l.targetdocumentid, l.targetpageid, l.linktype, l.orphan, l.created, l.revised + FROM link l + WHERE l.orgid=? AND l.sourcedocumentid=?`, + p.Context.OrgID, + documentID) if err != nil { - log.Error(fmt.Sprintf("Unable to prepare update for link %s", l.RefID), err) return } - _, err = stmt.Exec(&l) + if len(links) == 0 { + links = []entity.Link{} + } + + return +} + +// MarkOrphanDocumentLink marks all link records referencing specified document. +func (p *Persister) MarkOrphanDocumentLink(documentID string) (err error) { + revised := time.Now().UTC() + + stmt, err := p.Context.Transaction.Preparex("UPDATE link SET orphan=1, revised=? WHERE linktype='document' AND orgid=? AND targetdocumentid=?") + + if err != nil { + return + } + + defer utility.Close(stmt) + + _, err = stmt.Exec(revised, p.Context.OrgID, documentID) if err != nil { - log.Error(fmt.Sprintf("Unable to execute update for link %s", l.RefID), err) return } return } -// DeleteSourceLinks removes saved links for given source. -func (p *Persister) DeleteSourceLinks(sourceID string) (rows int64, err error) { - return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourceid=\"%s\"", p.Context.OrgID, sourceID)) +// MarkOrphanPageLink marks all link records referencing specified page. +func (p *Persister) MarkOrphanPageLink(pageID string) (err error) { + revised := time.Now().UTC() + + stmt, err := p.Context.Transaction.Preparex("UPDATE link SET orphan=1, revised=? WHERE linktype='section' AND orgid=? AND targetpageid=?") + + if err != nil { + return + } + + defer utility.Close(stmt) + + _, err = stmt.Exec(revised, p.Context.OrgID, pageID) + + if err != nil { + return + } + + return +} + +// DeleteSourcePageLinks removes saved links for given source. +func (p *Persister) DeleteSourcePageLinks(pageID string) (rows int64, err error) { + return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourcepageid=\"%s\"", p.Context.OrgID, pageID)) +} + +// DeleteSourceDocumentLinks removes saved links for given document. +func (p *Persister) DeleteSourceDocumentLinks(documentID string) (rows int64, err error) { + return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM link WHERE orgid=\"%s\" AND sourcedocumentid=\"%s\"", p.Context.OrgID, documentID)) } // DeleteLink removes saved link from the store. diff --git a/core/api/request/page.go b/core/api/request/page.go index 52cee603..d4170a1f 100644 --- a/core/api/request/page.go +++ b/core/api/request/page.go @@ -291,14 +291,18 @@ func (p *Persister) UpdatePage(page entity.Page, refID, userID string, skipRevis links := util.GetContentLinks(page.Body) // delete previous content links for this page - _, _ = p.DeleteSourceLinks(page.RefID) + _, _ = p.DeleteSourcePageLinks(page.RefID) // save latest content links for this page for _, link := range links { + link.Orphan = false link.OrgID = p.Context.OrgID link.UserID = p.Context.UserID - link.SourceID = page.RefID - link.Orphan = false + link.SourceDocumentID = page.DocumentID + + if link.LinkType == "section" { + link.SourcePageID = page.RefID + } err := p.AddContentLink(link) @@ -399,6 +403,12 @@ func (p *Persister) DeletePage(documentID, pageID string) (rows int64, err error _, err = p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", p.Context.OrgID, pageID)) _, err = searches.Delete(&databaseRequest{OrgID: p.Context.OrgID}, documentID, pageID) + // delete content links from this page + _, err = p.DeleteSourcePageLinks(pageID) + + // mark as orphan links to this page + err = p.MarkOrphanPageLink(pageID) + p.Base.Audit(p.Context, "remove-page", documentID, pageID) } diff --git a/core/api/util/links.go b/core/api/util/links.go index 620c10b0..93c32e38 100644 --- a/core/api/util/links.go +++ b/core/api/util/links.go @@ -62,10 +62,10 @@ func getLink(t html.Token) (ok bool, link entity.Link) { 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-document-id": + link.TargetDocumentID = strings.TrimSpace(a.Val) case "data-link-target-id": - link.TargetID = strings.TrimSpace(a.Val) + link.TargetPageID = strings.TrimSpace(a.Val) case "data-link-type": link.LinkType = strings.TrimSpace(a.Val) } diff --git a/core/database/scripts/autobuild/db_00000.sql b/core/database/scripts/autobuild/db_00000.sql index 4a4b727c..a489768d 100644 --- a/core/database/scripts/autobuild/db_00000.sql +++ b/core/database/scripts/autobuild/db_00000.sql @@ -321,10 +321,11 @@ CREATE TABLE IF NOT EXISTS `link` ( `orgid` CHAR(16) NOT NULL 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, + `sourcedocumentid` CHAR(16) NOT NULL COLLATE utf8_bin, + `sourcepageid` CHAR(16) NOT NULL COLLATE utf8_bin, `linktype` CHAR(16) NOT NULL COLLATE utf8_bin, - `documentid` CHAR(16) NOT NULL COLLATE utf8_bin, - `targetid` CHAR(16) DEFAULT '' COLLATE utf8_bin, + `targetdocumentid` CHAR(16) NOT NULL COLLATE utf8_bin, + `targetpageid` CHAR(16) DEFAULT '' COLLATE utf8_bin, `orphan` BOOL NOT NULL DEFAULT 0, `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, diff --git a/core/database/scripts/autobuild/db_00004.sql b/core/database/scripts/autobuild/db_00004.sql index 6e4aa26a..0e1c7d07 100644 --- a/core/database/scripts/autobuild/db_00004.sql +++ b/core/database/scripts/autobuild/db_00004.sql @@ -7,10 +7,11 @@ CREATE TABLE IF NOT EXISTS `link` ( `orgid` CHAR(16) NOT NULL 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, + `sourcedocumentid` CHAR(16) NOT NULL COLLATE utf8_bin, + `sourcepageid` CHAR(16) NOT NULL COLLATE utf8_bin, `linktype` CHAR(16) NOT NULL COLLATE utf8_bin, - `documentid` CHAR(16) NOT NULL COLLATE utf8_bin, - `targetid` CHAR(16) DEFAULT '' COLLATE utf8_bin, + `targetdocumentid` CHAR(16) NOT NULL COLLATE utf8_bin, + `targetpageid` CHAR(16) DEFAULT '' COLLATE utf8_bin, `orphan` BOOL NOT NULL DEFAULT 0, `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,