1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 13:19:43 +02:00

add category to document

This commit is contained in:
Harvey Kandola 2017-09-22 17:23:14 +01:00
parent 508ec00c6a
commit f0582e18f7
19 changed files with 334 additions and 58 deletions

View file

@ -309,7 +309,112 @@ func (h *Handler) GetSummary(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, s) 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? - filter space documents by category -- URL param? nested route?
*/ */

View file

@ -61,8 +61,7 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (c []catego
WHERE orgid=? AND labelid=? WHERE orgid=? AND labelid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='category' AND refid IN ( 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 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' 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=?
AND p.action='view' AND r.userid=?
)) ))
ORDER BY category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.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 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
}

View file

@ -74,6 +74,7 @@ type CategoryStorer interface {
DisassociateDocument(ctx RequestContext, categoryID, documentID string) (rows int64, err error) DisassociateDocument(ctx RequestContext, categoryID, documentID string) (rows int64, err error)
RemoveCategoryMembership(ctx RequestContext, categoryID string) (rows int64, err error) RemoveCategoryMembership(ctx RequestContext, categoryID string) (rows int64, err error)
DeleteBySpace(ctx RequestContext, spaceID 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 // PermissionStorer defines required methods for space/document permission management

View file

@ -15,11 +15,15 @@ import NotifierMixin from '../../mixins/notifier';
export default Ember.Component.extend(TooltipMixin, NotifierMixin, { export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
documentService: Ember.inject.service('document'), documentService: Ember.inject.service('document'),
sectionService: Ember.inject.service('section'), categoryService: Ember.inject.service('category'),
sessionService: Ember.inject.service('session'), sessionService: Ember.inject.service('session'),
appMeta: Ember.inject.service(), categories: [],
userService: Ember.inject.service('user'), hasCategories: Ember.computed('categories', function() {
localStorage: Ember.inject.service(), return this.get('categories').length > 0;
}),
canAdd: Ember.computed('categories', function() {
return this.get('categories').length > 0 && this.get('permissions.documentEdit');
}),
init() { init() {
this._super(...arguments); this._super(...arguments);
@ -27,8 +31,62 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); 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: { 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;
}
} }
}); });

View file

@ -10,27 +10,29 @@
{{#layout/zone-content}} {{#layout/zone-content}}
<div id="zone-document-content" class="zone-document-content"> <div id="zone-document-content" class="zone-document-content">
<div class="pull-left"> <div class="document-header-zone">
{{document/space-category document=model.document folder=model.folder folders=model.folders permissions=model.permissions}} <div class="pull-left">
{{document/space-category document=model.document folder=model.folder folders=model.folders permissions=model.permissions}}
</div>
<div class="pull-right">
{{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')}}
</div>
<div class="clearfix"/>
{{document/tag-editor documentTags=model.document.tags permissions=model.permissions onChange=(action 'onTagChange')}}
</div> </div>
<div class="pull-right"> {{document/document-heading document=model.document permissions=model.permissions onSaveDocument=(action 'onSaveDocument')}}
{{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')}}
</div>
<div class="clearfix"/>
{{#if model.document.template}} {{#if model.document.template}}
<div class="document-template-header">Template</div> <div class="document-template-header">Template</div>
{{/if}} {{/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/document-view
document=model.document links=model.links pages=model.pages document=model.document links=model.links pages=model.pages
folder=model.folder folders=model.folders sections=model.sections permissions=model.permissions pageId=pageId folder=model.folder folders=model.folders sections=model.sections permissions=model.permissions pageId=pageId

View file

@ -126,5 +126,22 @@ export default BaseService.extend({
}).then((response) => { }).then((response) => {
return 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;
});
} }
}); });

View file

@ -6,7 +6,6 @@
font-size: 1.2rem; font-size: 1.2rem;
color: $color-gray; color: $color-gray;
} }
> .normal-state { > .normal-state {
margin: 10px; margin: 10px;
font-size: 1.2rem; font-size: 1.2rem;
@ -17,13 +16,4 @@
.back-to-space { .back-to-space {
margin: 0 0 10px 0; margin: 0 0 10px 0;
display: inline-block; display: inline-block;
> a {
> .regular-button {
> .name {
// max-width: 150px;
// @extend .truncate;
}
}
}
} }

View file

@ -7,3 +7,4 @@
@import "toc.scss"; @import "toc.scss";
@import "view.scss"; @import "view.scss";
@import "wysiwyg.scss"; @import "wysiwyg.scss";
@import "space-category-tag.scss";

View file

@ -1,5 +1,10 @@
.document-attachments { .document-attachments {
margin: 0; margin: 0 0 50px 0;
// @include content-container();
> h2 {
color: $color-gray;
}
> .upload-document-files { > .upload-document-files {
margin: 10px 0 0 0; margin: 10px 0 0 0;

View file

@ -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;
}
}
}

View file

@ -1,18 +1,25 @@
.zone-document-content { .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 { .doc-title {
font-size: 2rem; font-size: 2rem;
margin: 30px 0 10px; margin: 50px 0 10px;
font-weight: normal; font-weight: normal;
} }
.doc-excerpt { .doc-excerpt {
font-size: 1rem; font-size: 1rem;
color: $color-gray; color: $color-gray;
margin: 0 0 60px; margin: 0 0 45px;
} }
.edit-document-heading { .edit-document-heading {
margin: 30px 0 0 0; margin: 50px 0 0 0;
.edit-doc-title { .edit-doc-title {
> input { > input {
@ -32,7 +39,7 @@
} }
.document-view { .document-view {
margin: 0 0 50px 0; margin: 0 0 0 0;
.is-a-page { .is-a-page {
@include content-container(); @include content-container();
@ -359,7 +366,3 @@
font-size: 1.5em; font-size: 1.5em;
margin-bottom: 20px; margin-bottom: 20px;
} }
.document-tags {
margin-top: 15px;
}

View file

@ -242,6 +242,13 @@
border: 1px solid $color-gray; 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 { .flat-button {
box-shadow: none; box-shadow: none;
background-color: transparent; background-color: transparent;

View file

@ -1,22 +1,24 @@
.chip { .chip {
display: inline-block; display: inline-block;
border-radius: 3px; border-radius: 3px;
border: 1px solid $color-chip-border;
padding: 0; padding: 0;
height: 25px; height: 25px;
line-height: 0; line-height: 0;
margin: 0 5px 10px 0; margin: 0 5px 10px 0;
border: 1px solid $color-chip-border;
background-color: $color-chip; background-color: $color-chip;
color: $color-chip-text; color: $color-chip-text;
&:hover { &:hover {
cursor: pointer; > i.material-icons {
visibility: visible;
}
} }
> .chip-text { > .chip-text {
display: inline-block; display: inline-block;
font-weight: 400; font-weight: 400;
font-size: 12px; font-size: 1rem;
color: $color-chip-text; color: $color-chip-text;
padding: 11px 10px 0 10px; padding: 11px 10px 0 10px;
letter-spacing: 0.7px; letter-spacing: 0.7px;
@ -24,6 +26,7 @@
} }
> i.material-icons { > i.material-icons {
visibility: hidden;
color: $color-chip-text; color: $color-chip-text;
font-size: 13px; font-size: 13px;
margin: 13px 8px 0 0; margin: 13px 8px 0 0;

View file

@ -1,4 +1,5 @@
<div class="document-attachments"> <div class="document-attachments">
<h2>Attachments</h2>
{{#if hasAttachments}} {{#if hasAttachments}}
<ul class="list"> <ul class="list">
{{#each files key="id" as |a index|}} {{#each files key="id" as |a index|}}

View file

@ -1,5 +1,3 @@
{{document/document-attachments document=document permissions=permissions}}
<div class="document-view {{if (is-equal document.layout 'doc') 'document-view-unified'}}"> <div class="document-view {{if (is-equal document.layout 'doc') 'document-view-unified'}}">
{{#if hasPages}} {{#if hasPages}}
@ -103,3 +101,5 @@
</div> </div>
</div> </div>
{{document/document-attachments document=document permissions=permissions}}

View file

@ -1,4 +1,5 @@
<div class="back-to-space"> <div class="back-to-space document-space">
<div class="caption">Space</div>
{{#link-to 'folder' folder.id folder.slug}} {{#link-to 'folder' folder.id folder.slug}}
<div class="regular-button button-gray"> <div class="regular-button button-gray">
<i class="material-icons">arrow_back</i> <i class="material-icons">arrow_back</i>
@ -7,8 +8,23 @@
{{/link-to}} {{/link-to}}
</div> </div>
<div class="document-category hide"> <div class="document-category">
<div class="chip chip-action"> <div class="caption">Category</div>
<span id="upload-document-files" class="chip-text">+ category</span>
</div> {{#each selectedCategories as |cat|}}
<div class="regular-button button-blue">{{cat.category}}</div>
{{/each}}
{{#if hasCategories}}
<div class="regular-button button-white" id="document-category-button">
<i class="material-icons">add</i>
</div>
{{#dropdown-dialog target="document-category-button" position="bottom left" button="set" color="flat-green" onAction=(action 'onSave')}}
<p class="heading">Select categories for document</p>
{{ui/ui-list-picker items=categories nameField='category'}}
{{/dropdown-dialog}}
{{else}}
<p>&nbsp;</p>
{{/if}}
</div> </div>

View file

@ -1,15 +1,11 @@
<div class="document-tags"> <div class="document-tags">
<div class="caption">Tag</div>
{{#each tagz as |tg|}} {{#each tagz as |tg|}}
<div class="chip"> <div class="regular-button button-chip">{{concat '#' tg}}</div>
<span class="chip-text">#{{tg}}</span>
{{#if permissions.documentEdit}}
<i class="material-icons pull-right" {{action 'removeTag' tg}}>close</i>
{{/if}}
</div>
{{/each}} {{/each}}
{{#if canAdd}} {{#if canAdd}}
<div class="chip-action"> <div class="chip-action">
<span id="add-tag-button" class="chip-text">+ tag</span> <span id="add-tag-button" class="chip-text">+</span>
</div> </div>
{{#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"}} {{#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"}}
<div class="input-control"> <div class="input-control">

View file

@ -68,7 +68,7 @@
</div> </div>
<div class="actions"> <div class="actions">
<div class="flat-button" {{action 'onGrantCancel'}}>cancel</div> <div class="flat-button" {{action 'onGrantCancel'}}>cancel</div>
<div class="flat-button flat-blue" {{action 'onGrantAccess'}}>grant access</div> <div class="flat-button flat-blue" {{action 'onGrantAccess'}}>set access</div>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>

View file

@ -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{"GET", "OPTIONS"}, nil, space.Get)
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update) 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"}, []string{"filter", "all"}, category.GetAll)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.Get) Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.Get)
Add(rt, RoutePrefixPrivate, "category", []string{"POST", "OPTIONS"}, nil, category.Add) 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{"PUT", "OPTIONS"}, nil, permission.SetCategoryPermissions)
Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryPermissions) 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/{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/{userID}/password", []string{"POST", "OPTIONS"}, nil, user.ChangePassword)
Add(rt, RoutePrefixPrivate, "users", []string{"POST", "OPTIONS"}, nil, user.Add) Add(rt, RoutePrefixPrivate, "users", []string{"POST", "OPTIONS"}, nil, user.Add)