diff --git a/domain/category/endpoint.go b/domain/category/endpoint.go
index de20b753..c4c18828 100644
--- a/domain/category/endpoint.go
+++ b/domain/category/endpoint.go
@@ -309,7 +309,112 @@ func (h *Handler) GetSummary(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, s)
}
+// SetDocumentCategoryMembership will link/unlink document from categories (query string switch mode=link or mode=unlink).
+func (h *Handler) SetDocumentCategoryMembership(w http.ResponseWriter, r *http.Request) {
+ method := "category.addMember"
+ ctx := domain.GetRequestContext(r)
+
+ mode := request.Query(r, "mode")
+ if len(mode) == 0 {
+ response.WriteMissingDataError(w, method, "mode")
+ return
+ }
+
+ defer r.Body.Close()
+ body, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ response.WriteBadRequestError(w, method, "body")
+ h.Runtime.Log.Error(method, err)
+ return
+ }
+
+ var cats []category.Member
+ err = json.Unmarshal(body, &cats)
+ if err != nil {
+ response.WriteBadRequestError(w, method, "category")
+ h.Runtime.Log.Error(method, err)
+ return
+ }
+
+ if len(cats) == 0 {
+ response.WriteEmpty(w)
+ return
+ }
+
+ if !permission.HasPermission(ctx, *h.Store, cats[0].LabelID, pm.DocumentAdd, pm.DocumentEdit) {
+ response.WriteForbiddenError(w)
+ return
+ }
+
+ ctx.Transaction, err = h.Runtime.Db.Beginx()
+ if err != nil {
+ response.WriteServerError(w, method, err)
+ h.Runtime.Log.Error(method, err)
+ return
+ }
+
+ for _, c := range cats {
+ if mode == "link" {
+ c.OrgID = ctx.OrgID
+ c.RefID = uniqueid.Generate()
+ _, err = h.Store.Category.DisassociateDocument(ctx, c.CategoryID, c.DocumentID)
+ err = h.Store.Category.AssociateDocument(ctx, c)
+ } else {
+ _, err = h.Store.Category.DisassociateDocument(ctx, c.CategoryID, c.DocumentID)
+ }
+
+ if err != nil {
+ ctx.Transaction.Rollback()
+ response.WriteServerError(w, method, err)
+ h.Runtime.Log.Error(method, err)
+ return
+ }
+ }
+
+ h.Store.Audit.Record(ctx, audit.EventTypeCategoryLink)
+
+ ctx.Transaction.Commit()
+
+ response.WriteEmpty(w)
+}
+
+// GetDocumentCategoryMembership returns categories associated with given document.
+func (h *Handler) GetDocumentCategoryMembership(w http.ResponseWriter, r *http.Request) {
+ method := "category.GetDocumentCategoryMembership"
+ ctx := domain.GetRequestContext(r)
+
+ documentID := request.Param(r, "documentID")
+ if len(documentID) == 0 {
+ response.WriteMissingDataError(w, method, "documentID")
+ return
+ }
+
+ doc, err := h.Store.Document.Get(ctx, documentID)
+ if err != nil {
+ response.WriteServerError(w, method, err)
+ h.Runtime.Log.Error("no document for category", err)
+ return
+ }
+
+ if !permission.HasPermission(ctx, *h.Store, doc.LabelID, pm.DocumentAdd, pm.DocumentEdit) {
+ response.WriteForbiddenError(w)
+ return
+ }
+
+ cat, err := h.Store.Category.GetDocumentCategoryMembership(ctx, doc.RefID)
+ if err != nil && err != sql.ErrNoRows {
+ h.Runtime.Log.Error("get document category membership", err)
+ response.WriteServerError(w, method, err)
+ return
+ }
+
+ if len(cat) == 0 {
+ cat = []category.Category{}
+ }
+
+ response.WriteJSON(w, cat)
+}
+
/*
- - link/unlink document to category
- filter space documents by category -- URL param? nested route?
*/
diff --git a/domain/category/mysql/store.go b/domain/category/mysql/store.go
index f86a1127..86775e26 100644
--- a/domain/category/mysql/store.go
+++ b/domain/category/mysql/store.go
@@ -61,8 +61,7 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (c []catego
WHERE orgid=? AND labelid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='category' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND whoid=? AND location='category' 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='category'
- AND p.action='view' AND r.userid=?
+ 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='category' AND r.userid=?
))
ORDER BY category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
@@ -225,3 +224,20 @@ func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string
return
}
+
+// GetDocumentCategoryMembership returns all space categories associated with given document.
+func (s Scope) GetDocumentCategoryMembership(ctx domain.RequestContext, documentID string) (c []category.Category, err error) {
+ err = s.Runtime.Db.Select(&c, `
+ SELECT id, refid, orgid, labelid, category, created, revised FROM category
+ WHERE orgid=? AND refid IN (SELECT categoryid FROM categorymember WHERE orgid=? AND documentid=?)`, ctx.OrgID, ctx.OrgID, documentID)
+
+ if err == sql.ErrNoRows {
+ err = nil
+ }
+ if err != nil {
+ err = errors.Wrap(err, fmt.Sprintf("unable to execute select categories for document %s", documentID))
+ return
+ }
+
+ return
+}
diff --git a/domain/storer.go b/domain/storer.go
index 6638c547..bc6b86fd 100644
--- a/domain/storer.go
+++ b/domain/storer.go
@@ -74,6 +74,7 @@ type CategoryStorer interface {
DisassociateDocument(ctx RequestContext, categoryID, documentID string) (rows int64, err error)
RemoveCategoryMembership(ctx RequestContext, categoryID string) (rows int64, err error)
DeleteBySpace(ctx RequestContext, spaceID string) (rows int64, err error)
+ GetDocumentCategoryMembership(ctx RequestContext, documentID string) (c []category.Category, err error)
}
// PermissionStorer defines required methods for space/document permission management
diff --git a/gui/app/components/document/space-category.js b/gui/app/components/document/space-category.js
index b0fad8f9..1f63291a 100644
--- a/gui/app/components/document/space-category.js
+++ b/gui/app/components/document/space-category.js
@@ -15,11 +15,15 @@ import NotifierMixin from '../../mixins/notifier';
export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
documentService: Ember.inject.service('document'),
- sectionService: Ember.inject.service('section'),
+ categoryService: Ember.inject.service('category'),
sessionService: Ember.inject.service('session'),
- appMeta: Ember.inject.service(),
- userService: Ember.inject.service('user'),
- localStorage: Ember.inject.service(),
+ categories: [],
+ hasCategories: Ember.computed('categories', function() {
+ return this.get('categories').length > 0;
+ }),
+ canAdd: Ember.computed('categories', function() {
+ return this.get('categories').length > 0 && this.get('permissions.documentEdit');
+ }),
init() {
this._super(...arguments);
@@ -27,8 +31,62 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
didReceiveAttrs() {
this._super(...arguments);
+ this.load();
+ },
+
+ load() {
+ this.get('categoryService').getUserVisible(this.get('folder.id')).then((categories) => {
+ this.set('categories', categories);
+ this.get('categoryService').getDocumentCategories(this.get('document.id')).then((selected) => {
+ this.set('selectedCategories', selected);
+ selected.forEach((s) => {
+ let cats = this.set('categories', categories);
+ let cat = categories.findBy('id', s.id);
+ cat.set('selected', true);
+ this.set('categories', cats);
+ });
+ });
+ });
},
actions: {
+ onSave() {
+ let docId = this.get('document.id');
+ let folderId = this.get('folder.id');
+ let link = this.get('categories').filterBy('selected', true);
+ let unlink = this.get('categories').filterBy('selected', false);
+ let toLink = [];
+ let toUnlink = [];
+
+ // prepare links associated with document
+ link.forEach((l) => {
+ let t = {
+ folderId: folderId,
+ documentId: docId,
+ categoryId: l.get('id')
+ }
+
+ toLink.push(t);
+ });
+
+ // prepare links no longer associated with document
+ unlink.forEach((l) => {
+ let t = {
+ folderId: folderId,
+ documentId: docId,
+ categoryId: l.get('id')
+ }
+
+ toUnlink.pushObject(t);
+ });
+
+ this.get('categoryService').setCategoryMembership(toUnlink, 'unlink').then(() => {
+ this.get('categoryService').setCategoryMembership(toLink, 'link').then(() => {
+ this.load();
+ });
+ });
+
+ return true;
+ }
}
});
diff --git a/gui/app/pods/document/index/template.hbs b/gui/app/pods/document/index/template.hbs
index 9283d7db..0d757871 100644
--- a/gui/app/pods/document/index/template.hbs
+++ b/gui/app/pods/document/index/template.hbs
@@ -10,27 +10,29 @@
{{#layout/zone-content}}
-
- {{document/space-category document=model.document folder=model.folder folders=model.folders permissions=model.permissions}}
+
-
- {{document/document-toolbar
- document=model.document folder=model.folder folders=model.folders permissions=model.permissions
- onDocumentDelete=(action 'onDocumentDelete') onSaveTemplate=(action 'onSaveTemplate')
- onPageSequenceChange=(action 'onPageSequenceChange') onPageLevelChange=(action 'onPageLevelChange')
- onGotoPage=(action 'onGotoPage')}}
-
-
+ {{document/document-heading document=model.document permissions=model.permissions onSaveDocument=(action 'onSaveDocument')}}
{{#if model.document.template}}
{{/if}}
- {{document/document-heading document=model.document permissions=model.permissions onSaveDocument=(action 'onSaveDocument')}}
-
- {{document/tag-editor documentTags=model.document.tags permissions=model.permissions onChange=(action 'onTagChange')}}
-
{{document/document-view
document=model.document links=model.links pages=model.pages
folder=model.folder folders=model.folders sections=model.sections permissions=model.permissions pageId=pageId
diff --git a/gui/app/services/category.js b/gui/app/services/category.js
index bad37b9b..ae5d7c42 100644
--- a/gui/app/services/category.js
+++ b/gui/app/services/category.js
@@ -126,5 +126,22 @@ export default BaseService.extend({
}).then((response) => {
return response;
});
+ },
+
+ setCategoryMembership(categories, mode) {
+ return this.get('ajax').request(`category/member?mode=${mode}`, {
+ method: 'POST',
+ contentType: 'json',
+ data: JSON.stringify(categories)
+ });
+ },
+
+ // Get categories associated with a document.
+ getDocumentCategories(documentId) {
+ return this.get('ajax').request(`category/document/${documentId}`, {
+ method: 'GET'
+ }).then((response) => {
+ return response;
+ });
}
});
diff --git a/gui/app/styles/view/common.scss b/gui/app/styles/view/common.scss
index 019328ed..d1d96fac 100644
--- a/gui/app/styles/view/common.scss
+++ b/gui/app/styles/view/common.scss
@@ -6,7 +6,6 @@
font-size: 1.2rem;
color: $color-gray;
}
-
> .normal-state {
margin: 10px;
font-size: 1.2rem;
@@ -17,13 +16,4 @@
.back-to-space {
margin: 0 0 10px 0;
display: inline-block;
-
- > a {
- > .regular-button {
- > .name {
- // max-width: 150px;
- // @extend .truncate;
- }
- }
- }
}
diff --git a/gui/app/styles/view/document/all.scss b/gui/app/styles/view/document/all.scss
index 81367013..23943d1f 100644
--- a/gui/app/styles/view/document/all.scss
+++ b/gui/app/styles/view/document/all.scss
@@ -7,3 +7,4 @@
@import "toc.scss";
@import "view.scss";
@import "wysiwyg.scss";
+@import "space-category-tag.scss";
diff --git a/gui/app/styles/view/document/attachments.scss b/gui/app/styles/view/document/attachments.scss
index 616b45c9..ca45b7f3 100644
--- a/gui/app/styles/view/document/attachments.scss
+++ b/gui/app/styles/view/document/attachments.scss
@@ -1,5 +1,10 @@
.document-attachments {
- margin: 0;
+ margin: 0 0 50px 0;
+ // @include content-container();
+
+ > h2 {
+ color: $color-gray;
+ }
> .upload-document-files {
margin: 10px 0 0 0;
diff --git a/gui/app/styles/view/document/space-category-tag.scss b/gui/app/styles/view/document/space-category-tag.scss
new file mode 100644
index 00000000..95ad578d
--- /dev/null
+++ b/gui/app/styles/view/document/space-category-tag.scss
@@ -0,0 +1,53 @@
+.document-space {
+ display: inline-block;
+
+ > .caption {
+ text-transform: uppercase;
+ color: $color-gray;
+ font-weight: bold;
+ font-size: 1rem;
+ margin: 0 0 10px 0;
+ }
+}
+
+.document-category {
+ display: inline-block;
+ margin: 0 0 0 30px;
+
+ > .regular-button {
+ margin-right: 10px;
+ }
+
+ > .caption {
+ text-transform: uppercase;
+ color: $color-gray;
+ font-weight: bold;
+ font-size: 1rem;
+ margin: 0 0 10px 0;
+ }
+}
+
+.document-tags {
+ margin: 20px 0 0 0;
+
+ > .caption {
+ text-transform: uppercase;
+ color: $color-gray;
+ font-weight: bold;
+ font-size: 1rem;
+ margin: 0 0 10px 0;
+ }
+
+ > .regular-button {
+ margin-right: 10px;
+
+ &:hover {
+ visibility: visible;
+ }
+
+ > .material-icons {
+ visibility: hidden;
+ }
+ }
+}
+
diff --git a/gui/app/styles/view/document/view.scss b/gui/app/styles/view/document/view.scss
index 5cef4a18..f6677733 100644
--- a/gui/app/styles/view/document/view.scss
+++ b/gui/app/styles/view/document/view.scss
@@ -1,18 +1,25 @@
.zone-document-content {
+ > .document-header-zone {
+ padding: 20px 30px;
+ border: 1px solid $color-stroke;
+ @include border-radius(3px);
+ background-color: $color-off-white;
+ }
+
.doc-title {
font-size: 2rem;
- margin: 30px 0 10px;
+ margin: 50px 0 10px;
font-weight: normal;
}
.doc-excerpt {
font-size: 1rem;
color: $color-gray;
- margin: 0 0 60px;
+ margin: 0 0 45px;
}
.edit-document-heading {
- margin: 30px 0 0 0;
+ margin: 50px 0 0 0;
.edit-doc-title {
> input {
@@ -32,7 +39,7 @@
}
.document-view {
- margin: 0 0 50px 0;
+ margin: 0 0 0 0;
.is-a-page {
@include content-container();
@@ -359,7 +366,3 @@
font-size: 1.5em;
margin-bottom: 20px;
}
-
-.document-tags {
- margin-top: 15px;
-}
diff --git a/gui/app/styles/widget/widget-button.scss b/gui/app/styles/widget/widget-button.scss
index 9ef36c50..6966d9fd 100644
--- a/gui/app/styles/widget/widget-button.scss
+++ b/gui/app/styles/widget/widget-button.scss
@@ -242,6 +242,13 @@
border: 1px solid $color-gray;
}
+.button-chip {
+ background-color: $color-chip;
+ color: $color-chip-text;
+ border: 1px solid $color-chip-border;
+ @include button-hover-state($color-chip);
+}
+
.flat-button {
box-shadow: none;
background-color: transparent;
diff --git a/gui/app/styles/widget/widget-chip.scss b/gui/app/styles/widget/widget-chip.scss
index ce7b668c..b0dcef74 100644
--- a/gui/app/styles/widget/widget-chip.scss
+++ b/gui/app/styles/widget/widget-chip.scss
@@ -1,22 +1,24 @@
.chip {
display: inline-block;
border-radius: 3px;
- border: 1px solid $color-chip-border;
padding: 0;
height: 25px;
line-height: 0;
margin: 0 5px 10px 0;
+ border: 1px solid $color-chip-border;
background-color: $color-chip;
color: $color-chip-text;
&:hover {
- cursor: pointer;
+ > i.material-icons {
+ visibility: visible;
+ }
}
> .chip-text {
display: inline-block;
font-weight: 400;
- font-size: 12px;
+ font-size: 1rem;
color: $color-chip-text;
padding: 11px 10px 0 10px;
letter-spacing: 0.7px;
@@ -24,6 +26,7 @@
}
> i.material-icons {
+ visibility: hidden;
color: $color-chip-text;
font-size: 13px;
margin: 13px 8px 0 0;
diff --git a/gui/app/templates/components/document/document-attachments.hbs b/gui/app/templates/components/document/document-attachments.hbs
index aa145e63..21279b13 100644
--- a/gui/app/templates/components/document/document-attachments.hbs
+++ b/gui/app/templates/components/document/document-attachments.hbs
@@ -1,4 +1,5 @@
+
Attachments
{{#if hasAttachments}}
{{#each files key="id" as |a index|}}
diff --git a/gui/app/templates/components/document/document-view.hbs b/gui/app/templates/components/document/document-view.hbs
index 97375d1c..ce3cba0a 100644
--- a/gui/app/templates/components/document/document-view.hbs
+++ b/gui/app/templates/components/document/document-view.hbs
@@ -1,5 +1,3 @@
-{{document/document-attachments document=document permissions=permissions}}
-
{{#if hasPages}}
@@ -103,3 +101,5 @@
+
+{{document/document-attachments document=document permissions=permissions}}
diff --git a/gui/app/templates/components/document/space-category.hbs b/gui/app/templates/components/document/space-category.hbs
index e5d794dc..a03e281c 100644
--- a/gui/app/templates/components/document/space-category.hbs
+++ b/gui/app/templates/components/document/space-category.hbs
@@ -1,4 +1,5 @@
-
+
+
Space
{{#link-to 'folder' folder.id folder.slug}}
arrow_back
@@ -7,8 +8,23 @@
{{/link-to}}
-
-
- + category
-
+
+
Category
+
+ {{#each selectedCategories as |cat|}}
+
{{cat.category}}
+ {{/each}}
+
+ {{#if hasCategories}}
+
+ add
+
+
+ {{#dropdown-dialog target="document-category-button" position="bottom left" button="set" color="flat-green" onAction=(action 'onSave')}}
+
Select categories for document
+ {{ui/ui-list-picker items=categories nameField='category'}}
+ {{/dropdown-dialog}}
+ {{else}}
+
+ {{/if}}
diff --git a/gui/app/templates/components/document/tag-editor.hbs b/gui/app/templates/components/document/tag-editor.hbs
index 0d976881..0bd857e7 100644
--- a/gui/app/templates/components/document/tag-editor.hbs
+++ b/gui/app/templates/components/document/tag-editor.hbs
@@ -1,15 +1,11 @@
diff --git a/server/routing/routes.go b/server/routing/routes.go
index ded073cd..e6e01daa 100644
--- a/server/routing/routes.go
+++ b/server/routing/routes.go
@@ -127,6 +127,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get)
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update)
+ Add(rt, RoutePrefixPrivate, "category/document/{documentID}", []string{"GET", "OPTIONS"}, nil, category.GetDocumentCategoryMembership)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, []string{"filter", "all"}, category.GetAll)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.Get)
Add(rt, RoutePrefixPrivate, "category", []string{"POST", "OPTIONS"}, nil, category.Add)
@@ -136,6 +137,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"PUT", "OPTIONS"}, nil, permission.SetCategoryPermissions)
Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryPermissions)
Add(rt, RoutePrefixPrivate, "category/{categoryID}/user", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryViewers)
+ Add(rt, RoutePrefixPrivate, "category/member", []string{"POST", "OPTIONS"}, nil, category.SetDocumentCategoryMembership)
Add(rt, RoutePrefixPrivate, "users/{userID}/password", []string{"POST", "OPTIONS"}, nil, user.ChangePassword)
Add(rt, RoutePrefixPrivate, "users", []string{"POST", "OPTIONS"}, nil, user.Add)