From f0582e18f7a82b7e0521405255b586b2d33bd7f6 Mon Sep 17 00:00:00 2001 From: Harvey Kandola Date: Fri, 22 Sep 2017 17:23:14 +0100 Subject: [PATCH] add category to document --- domain/category/endpoint.go | 107 +++++++++++++++++- domain/category/mysql/store.go | 20 +++- domain/storer.go | 1 + gui/app/components/document/space-category.js | 66 ++++++++++- gui/app/pods/document/index/template.hbs | 30 ++--- gui/app/services/category.js | 17 +++ gui/app/styles/view/common.scss | 10 -- gui/app/styles/view/document/all.scss | 1 + gui/app/styles/view/document/attachments.scss | 7 +- .../view/document/space-category-tag.scss | 53 +++++++++ gui/app/styles/view/document/view.scss | 19 ++-- gui/app/styles/widget/widget-button.scss | 7 ++ gui/app/styles/widget/widget-chip.scss | 9 +- .../document/document-attachments.hbs | 1 + .../components/document/document-view.hbs | 4 +- .../components/document/space-category.hbs | 26 ++++- .../components/document/tag-editor.hbs | 10 +- .../components/folder/category-admin.hbs | 2 +- server/routing/routes.go | 2 + 19 files changed, 334 insertions(+), 58 deletions(-) create mode 100644 gui/app/styles/view/document/space-category-tag.scss 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/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/tag-editor documentTags=model.document.tags permissions=model.permissions onChange=(action 'onTagChange')}}
-
- {{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}}
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 @@
+
Tag
{{#each tagz as |tg|}} -
- #{{tg}} - {{#if permissions.documentEdit}} - close - {{/if}} -
+
{{concat '#' tg}}
{{/each}} {{#if canAdd}}
- + tag + +
{{#dropdown-dialog target="add-tag-button" position="bottom left" button="Add" color="flat-green" onAction=(action 'addTag') focusOn="add-tag-field" onOpenCallback=(action 'onTagEditor') targetOffset="20px 0"}}
diff --git a/gui/app/templates/components/folder/category-admin.hbs b/gui/app/templates/components/folder/category-admin.hbs index 3066a71a..be7c2870 100644 --- a/gui/app/templates/components/folder/category-admin.hbs +++ b/gui/app/templates/components/folder/category-admin.hbs @@ -68,7 +68,7 @@
cancel
-
grant access
+
set access
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)