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,