1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-21 22:29:41 +02:00

category permission admin, re-vamped view layout

This commit is contained in:
Harvey Kandola 2017-09-21 15:48:00 +01:00
parent 0c152c219f
commit 3f31d6d15e
48 changed files with 753 additions and 373 deletions

View file

@ -279,7 +279,9 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
} }
/* /*
6. add category view permission !!! - category view permission handling
7. link/unlink document to category - filter users using new permission
8. filter space documents by category -- URL param? nested route? - link/unlink document to category
- check print/pdf
- filter space documents by category -- URL param? nested route?
*/ */

View file

@ -30,6 +30,7 @@ import (
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
"github.com/documize/community/model/permission" "github.com/documize/community/model/permission"
"github.com/documize/community/model/space" "github.com/documize/community/model/space"
"github.com/documize/community/model/user"
) )
// Handler contains the runtime information such as logging and database. // Handler contains the runtime information such as logging and database.
@ -43,25 +44,20 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
method := "space.SetPermissions" method := "space.SetPermissions"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !ctx.Editor {
response.WriteForbiddenError(w)
return
}
id := request.Param(r, "spaceID") id := request.Param(r, "spaceID")
if len(id) == 0 { if len(id) == 0 {
response.WriteMissingDataError(w, method, "spaceID") response.WriteMissingDataError(w, method, "spaceID")
return return
} }
sp, err := h.Store.Space.Get(ctx, id) if !HasPermission(ctx, *h.Store, id, permission.SpaceManage, permission.SpaceOwner) {
if err != nil { response.WriteForbiddenError(w)
response.WriteNotFoundError(w, method, "space not found")
return return
} }
if sp.UserID != ctx.UserID { sp, err := h.Store.Space.Get(ctx, id)
response.WriteForbiddenError(w) if err != nil {
response.WriteNotFoundError(w, method, "space not found")
return return
} }
@ -220,7 +216,7 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
response.WriteEmpty(w) response.WriteEmpty(w)
} }
// GetSpacePermissions returns permissions for alll users for given space. // GetSpacePermissions returns permissions for all users for given space.
func (h *Handler) GetSpacePermissions(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetSpacePermissions(w http.ResponseWriter, r *http.Request) {
method := "space.GetPermissions" method := "space.GetPermissions"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
@ -276,3 +272,105 @@ func (h *Handler) GetUserSpacePermissions(w http.ResponseWriter, r *http.Request
record := permission.DecodeUserPermissions(perms) record := permission.DecodeUserPermissions(perms)
response.WriteJSON(w, record) response.WriteJSON(w, record)
} }
// GetCategoryPermissions returns user permissions for given category.
func (h *Handler) GetCategoryPermissions(w http.ResponseWriter, r *http.Request) {
method := "space.GetCategoryPermissions"
ctx := domain.GetRequestContext(r)
categoryID := request.Param(r, "categoryID")
if len(categoryID) == 0 {
response.WriteMissingDataError(w, method, "categoryID")
return
}
u, err := h.Store.Permission.GetCategoryUsers(ctx, categoryID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(u) == 0 {
u = []user.User{}
}
response.WriteJSON(w, u)
}
// SetCategoryPermissions persists specified category permissions
func (h *Handler) SetCategoryPermissions(w http.ResponseWriter, r *http.Request) {
method := "permission.SetCategoryPermissions"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "categoryID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "categoryID")
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
var model = []permission.CategoryViewRequestModel{}
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(model) == 0 {
response.WriteEmpty(w)
return
}
spaceID := model[0].SpaceID
if !HasPermission(ctx, *h.Store, spaceID, permission.SpaceManage, permission.SpaceOwner) {
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
}
// Nuke all previous permissions for this category
_, err = h.Store.Permission.DeleteCategoryPermissions(ctx, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
for _, m := range model {
perm := permission.Permission{}
perm.OrgID = ctx.OrgID
perm.Who = "user"
perm.WhoID = m.UserID
perm.Scope = "object"
perm.Location = "category"
perm.RefID = m.CategoryID
perm.Action = permission.CategoryView
err = h.Store.Permission.AddPermission(ctx, perm)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
}
h.Store.Audit.Record(ctx, audit.EventTypeCategoryPermission)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}

View file

@ -22,6 +22,7 @@ import (
"github.com/documize/community/domain" "github.com/documize/community/domain"
"github.com/documize/community/domain/store/mysql" "github.com/documize/community/domain/store/mysql"
"github.com/documize/community/model/permission" "github.com/documize/community/model/permission"
"github.com/documize/community/model/user"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -155,3 +156,50 @@ func (s Scope) DeleteSpaceCategoryPermissions(ctx domain.RequestContext, spaceID
return b.DeleteWhere(ctx.Transaction, sql) return b.DeleteWhere(ctx.Transaction, sql)
} }
// GetCategoryPermissions returns category permissions for all users.
func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (r []permission.Permission, err error) {
err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission WHERE orgid=? AND location='category' AND refid=? AND who='user'
UNION ALL
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='space' AND p.refid=?
AND p.who='role'`,
ctx.OrgID, catID, ctx.OrgID, catID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions %s", catID))
return
}
return
}
// GetCategoryUsers returns space permissions for all users.
func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []user.User, err error) {
err = s.Runtime.Db.Select(&u, `
SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN (
SELECT whoid from permission WHERE orgid=? AND who='user' AND location='category' AND refid=? UNION ALL
SELECT r.userid from rolemember r LEFT JOIN permission p ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role'
AND p.location='category' AND p.refid=?
)
GROUP by u.id
ORDER BY firstname, lastname`,
ctx.OrgID, ctx.OrgID, catID, ctx.OrgID, catID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category user %s", catID))
return
}
return
}

View file

@ -86,6 +86,8 @@ type PermissionStorer interface {
DeleteUserPermissions(ctx RequestContext, userID string) (rows int64, err error) DeleteUserPermissions(ctx RequestContext, userID string) (rows int64, err error)
DeleteCategoryPermissions(ctx RequestContext, categoryID string) (rows int64, err error) DeleteCategoryPermissions(ctx RequestContext, categoryID string) (rows int64, err error)
DeleteSpaceCategoryPermissions(ctx RequestContext, spaceID string) (rows int64, err error) DeleteSpaceCategoryPermissions(ctx RequestContext, spaceID string) (rows int64, err error)
GetCategoryPermissions(ctx RequestContext, catID string) (r []permission.Permission, err error)
GetCategoryUsers(ctx RequestContext, catID string) (u []user.User, err error)
} }
// UserStorer defines required methods for user management // UserStorer defines required methods for user management

View file

@ -17,7 +17,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
documentService: Ember.inject.service('document'), documentService: Ember.inject.service('document'),
appMeta: Ember.inject.service(), appMeta: Ember.inject.service(),
drop: null, drop: null,
emptyState: Ember.computed.empty('files'), hasAttachments: Ember.computed.notEmpty('files'),
deleteAttachment: { deleteAttachment: {
id: "", id: "",
name: "", name: "",
@ -104,7 +104,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
target: $(".delete-attachment-" + id)[0], target: $(".delete-attachment-" + id)[0],
content: $(".delete-attachment-dialog")[0], content: $(".delete-attachment-dialog")[0],
classes: 'drop-theme-basic', classes: 'drop-theme-basic',
position: "bottom left", position: "bottom right",
openOn: "always", openOn: "always",
tetherOptions: { tetherOptions: {
offset: "5px 0", offset: "5px 0",

View file

@ -31,38 +31,31 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
name: "", name: "",
description: "" description: ""
}, },
tab: '',
init() { init() {
this._super(...arguments); this._super(...arguments);
if (is.empty(this.get('tab')) || is.undefined(this.get('tab'))) {
this.set('tab', 'index');
}
}, },
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
this.set('saveTemplate.name', this.get('document.name')); this.set('saveTemplate.name', this.get('document.name'));
this.set('saveTemplate.description', this.get('document.excerpt')); this.set('saveTemplate.description', this.get('document.excerpt'));
this.set('pinState.pinId', this.get('pinned').isDocumentPinned(this.get('document.id'))); this.set('pinState.pinId', this.get('pinned').isDocumentPinned(this.get('document.id')));
this.set('pinState.isPinned', this.get('pinState.pinId') !== ''); this.set('pinState.isPinned', this.get('pinState.pinId') !== '');
this.set('pinState.newName', this.get('document.name').substring(0,3).toUpperCase()); this.set('pinState.newName', this.get('document.name'));
},
didRender() {
this.destroyTooltips();
if (this.get('permissions.documentEdit')) {
this.addTooltip(document.getElementById("document-activity-button"));
}
}, },
actions: { actions: {
onChangeTab(tab) {
this.set('tab', tab);
},
onTagChange(tags) {
let doc = this.get('document');
doc.set('tags', tags);
this.get('documentService').save(doc);
},
onMenuOpen() { onMenuOpen() {
this.set('menuOpen', !this.get('menuOpen')); this.set('menuOpen', !this.get('menuOpen'));
}, },
@ -78,15 +71,15 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
onPageSequenceChange(changes) { onPageSequenceChange(changes) {
this.get('onPageSequenceChange')(changes); this.get('onPageSequenceChange')(changes);
}, },
onPageLevelChange(changes) { onPageLevelChange(changes) {
this.get('onPageLevelChange')(changes); this.get('onPageLevelChange')(changes);
}, },
onGotoPage(id) { onGotoPage(id) {
this.get('onGotoPage')(id); this.get('onGotoPage')(id);
}, },
onUnpin() { onUnpin() {
this.get('pinned').unpinItem(this.get('pinState.pinId')).then(() => { this.get('pinned').unpinItem(this.get('pinState.pinId')).then(() => {
@ -136,12 +129,15 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
return true; return true;
}, },
onLayoutChange(layout) { onLayoutChange(layout) {
let doc = this.get('document'); let doc = this.get('document');
doc.set('layout', layout); doc.set('layout', layout);
this.get('documentService').save(doc);
if (this.get('permissions.documentEdit')) {
this.get('documentService').save(doc);
}
return true; return true;
} }
} }

View file

@ -76,7 +76,7 @@ export default Ember.Component.extend(TooltipMixin, {
let permissions = this.get('permissions'); let permissions = this.get('permissions');
return permissions.get('documentDelete') || permissions.get('documentCopy') || return permissions.get('documentDelete') || permissions.get('documentCopy') ||
permissions.get('documentMove') || permissions.get('documentTemplate');; permissions.get('documentMove') || permissions.get('documentTemplate');
}), }),
didRender() { didRender() {

View file

@ -0,0 +1,34 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import TooltipMixin from '../../mixins/tooltip';
import NotifierMixin from '../../mixins/notifier';
export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
documentService: Ember.inject.service('document'),
sectionService: Ember.inject.service('section'),
sessionService: Ember.inject.service('session'),
appMeta: Ember.inject.service(),
userService: Ember.inject.service('user'),
localStorage: Ember.inject.service(),
init() {
this._super(...arguments);
},
didReceiveAttrs() {
this._super(...arguments);
},
actions: {
}
});

View file

@ -11,25 +11,59 @@
import Ember from 'ember'; import Ember from 'ember';
import NotifierMixin from '../../mixins/notifier'; import NotifierMixin from '../../mixins/notifier';
import TooltipMixin from '../../mixins/tooltip';
const { const {
inject: { service } inject: { service }
} = Ember; } = Ember;
export default Ember.Component.extend(NotifierMixin, { export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
folderService: service('folder'), userService: service('user'),
categoryService: service('category'), categoryService: service('category'),
appMeta: service(), appMeta: service(),
store: service(), store: service(),
newCategory: '', newCategory: '',
drop: null,
users: [],
didReceiveAttrs() { didReceiveAttrs() {
this.load(); this.load();
}, },
didRender() {
// this.addTooltip(this.$(".action"));
},
willDestroyElement() {
let drop = this.get('drop');
if (is.not.null(drop)) {
drop.destroy();
}
},
load() { load() {
// get categories
this.get('categoryService').getAll(this.get('folder.id')).then((c) => { this.get('categoryService').getAll(this.get('folder.id')).then((c) => {
this.set('category', c); this.set('category', c);
// get users that this space admin user can see
this.get('userService').getAll().then((users) => {
// set up Everyone user
let u = {
orgId: this.get('folder.orgId'),
folderId: this.get('folder.id'),
userId: '',
firstname: 'Everyone',
lastname: '',
};
let data = this.get('store').normalize('user', u)
users.pushObject(this.get('store').push(data));
users = users.sortBy('firstname', 'lastname');
this.set('users', users);
});
}); });
}, },
@ -76,7 +110,7 @@ export default Ember.Component.extend(NotifierMixin, {
this.setEdit(id, true); this.setEdit(id, true);
}, },
onCancel(id) { onEditCancel(id) {
this.setEdit(id, false); this.setEdit(id, false);
this.load(); this.load();
}, },
@ -94,6 +128,69 @@ export default Ember.Component.extend(NotifierMixin, {
this.get('categoryService').save(cat).then(() => { this.get('categoryService').save(cat).then(() => {
this.load(); this.load();
}); });
},
onShowAccessPicker(catId) {
let users = this.get('users');
let category = this.get('category').findBy('id', catId);
this.get('categoryService').getViewers(category.get('id')).then((viewers) => {
// mark those users as selected that have already been given permission
// to see the current category;
console.log(viewers);
users.forEach((user) => {
let selected = viewers.isAny('id', user.get('id'));
user.set('selected', selected);
});
this.set('categoryUsers', users);
this.set('currentCategory', category);
$(".category-access-dialog").css("display", "block");
let drop = new Drop({
target: $("#category-access-button-" + catId)[0],
content: $(".category-access-dialog")[0],
classes: 'drop-theme-basic',
position: "bottom right",
openOn: "always",
tetherOptions: {
offset: "5px 0",
targetOffset: "10px 0"
},
remove: false
});
this.set('drop', drop);
});
},
onGrantCancel() {
let drop = this.get('drop');
drop.close();
},
onGrantAccess() {
let category = this.get('currentCategory');
let users = this.get('categoryUsers').filterBy('selected', true);
let viewers = [];
users.forEach((user) => {
let v = {
orgId: this.get('folder.orgId'),
folderId: this.get('folder.id'),
categoryId: category.get('id'),
userId: user.get('id')
};
viewers.push(v);
});
this.get('categoryService').setViewers(category.get('id'), viewers).then( () => {});
let drop = this.get('drop');
drop.close();
} }
} }
}); });

View file

@ -60,6 +60,7 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, AuthMixin, {
if (this.get('permissions.documentMove')) { if (this.get('permissions.documentMove')) {
this.addTooltip(document.getElementById("move-documents-button")); this.addTooltip(document.getElementById("move-documents-button"));
} }
if (this.get('permissions.documentDelete')) { if (this.get('permissions.documentDelete')) {
this.addTooltip(document.getElementById("delete-documents-button")); this.addTooltip(document.getElementById("delete-documents-button"));
} }
@ -67,11 +68,13 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, AuthMixin, {
if (this.get('permissions.spaceOwner')) { if (this.get('permissions.spaceOwner')) {
this.addTooltip(document.getElementById("space-delete-button")); this.addTooltip(document.getElementById("space-delete-button"));
} }
if (this.get('permissions.spaceManage')) { if (this.get('permissions.spaceManage')) {
this.addTooltip(document.getElementById("space-settings-button")); this.addTooltip(document.getElementById("space-settings-button"));
} }
if (this.get('session.authenticated')) {
this.addTooltip(document.getElementById("space-unpin-button")); if (this.get('pinState.isPinned')) {
this.addTooltip(document.getElementById("space-unpin-button"));
} else { } else {
this.addTooltip(document.getElementById("space-pin-button")); this.addTooltip(document.getElementById("space-pin-button"));
} }

View file

@ -69,7 +69,7 @@ export default Ember.Component.extend(NotifierMixin, {
documentMove: false, documentMove: false,
documentCopy: false, documentCopy: false,
documentTemplate: false documentTemplate: false
}; };
let data = this.get('store').normalize('space-permission', u) let data = this.get('store').normalize('space-permission', u)
folderPermissions.pushObject(this.get('store').push(data)); folderPermissions.pushObject(this.get('store').push(data));

View file

@ -0,0 +1,23 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
export default Ember.Component.extend({
nameField: 'category',
items: [],
actions: {
onToggle(item) {
Ember.set(item, 'selected', !item.get('selected'));
}
}
});

View file

@ -15,10 +15,9 @@ export default Ember.Mixin.create({
tooltips: [], tooltips: [],
addTooltip(elem) { addTooltip(elem) {
if (elem == null) {
if(elem == null) {
return; return;
} }
let t = new Tooltip({ let t = new Tooltip({
target: elem target: elem

View file

@ -226,6 +226,12 @@ export default Ember.Controller.extend(NotifierMixin, {
if (this.get('pageId') !== id && id !== '') { if (this.get('pageId') !== id && id !== '') {
this.set('pageId', id); this.set('pageId', id);
} }
},
onTagChange(tags) {
let doc = this.get('model.document');
doc.set('tags', tags);
this.get('documentService').save(doc);
} }
} }
}); });

View file

@ -1,27 +1,44 @@
{{#layout/zone-container}} {{#layout/zone-container}}
{{#layout/zone-sidebar}} {{#layout/zone-sidebar}}
{{document/sidebar-zone folders=model.folders folder=model.folder document=model.document {{document/document-index
pages=model.pages sections=model.section links=model.links permissions=model.permissions tab=tab document=model.document folder=model.folder pages=model.pages page=model.page permissions=model.permissions
onDocumentDelete=(action 'onDocumentDelete') onSaveTemplate=(action 'onSaveTemplate')
onPageSequenceChange=(action 'onPageSequenceChange') onPageLevelChange=(action 'onPageLevelChange') onPageSequenceChange=(action 'onPageSequenceChange') onPageLevelChange=(action 'onPageLevelChange')
onGotoPage=(action 'onGotoPage')}} onGotoPage=(action 'onGotoPage')}}
{{/layout/zone-sidebar}} {{/layout/zone-sidebar}}
{{#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="back-to-space">
{{#link-to 'folder' model.folder.id model.folder.slug}} <div class="pull-left">
<div class="regular-button button-gray"> {{document/space-category document=model.document folder=model.folder folders=model.folders permissions=model.permissions}}
<i class="material-icons">arrow_back</i>
<div class="name">{{model.folder.name}}</div>
</div>
{{/link-to}}
</div> </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"/>
{{#if model.document.template}}
<div class="document-template-header">Template</div>
{{/if}}
{{document/document-heading document=model.document permissions=model.permissions onSaveDocument=(action 'onSaveDocument')}} {{document/document-heading document=model.document permissions=model.permissions onSaveDocument=(action 'onSaveDocument')}}
{{document/document-view document=model.document links=model.links pages=model.pages
{{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 folder=model.folder folders=model.folders sections=model.sections permissions=model.permissions pageId=pageId
onSavePage=(action 'onSavePage') onInsertSection=(action 'onInsertSection') onSavePage=(action 'onSavePage') onInsertSection=(action 'onInsertSection')
onSavePageAsBlock=(action 'onSavePageAsBlock') onDeleteBlock=(action 'onDeleteBlock') onGotoPage=(action 'onGotoPage') onSavePageAsBlock=(action 'onSavePageAsBlock') onDeleteBlock=(action 'onDeleteBlock') onGotoPage=(action 'onGotoPage')
onCopyPage=(action 'onCopyPage') onMovePage=(action 'onMovePage') onDeletePage=(action 'onPageDeleted')}} onCopyPage=(action 'onCopyPage') onMovePage=(action 'onMovePage') onDeletePage=(action 'onPageDeleted')}}
</div> </div>
{{/layout/zone-content}} {{/layout/zone-content}}
{{/layout/zone-container}} {{/layout/zone-container}}

View file

@ -15,7 +15,7 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout
export default Ember.Route.extend(AuthenticatedRouteMixin, { export default Ember.Route.extend(AuthenticatedRouteMixin, {
beforeModel: function (transition) { beforeModel: function (transition) {
if (is.equal(transition.targetName, 'folder.settings.index')) { if (is.equal(transition.targetName, 'folder.settings.index')) {
this.transitionTo('folder.settings.security'); this.transitionTo('folder.settings.invitation');
} }
}, },

View file

@ -10,7 +10,7 @@
{{#link-to 'folder.settings.invitation' activeClass='selected' class="option" tagName="li"}}Invite{{/link-to}} {{#link-to 'folder.settings.invitation' activeClass='selected' class="option" tagName="li"}}Invite{{/link-to}}
{{/if}} {{/if}}
{{#link-to 'folder.settings.security' activeClass='selected' class="option" tagName="li"}}Secure{{/link-to}} {{#link-to 'folder.settings.security' activeClass='selected' class="option" tagName="li"}}Secure{{/link-to}}
{{#link-to 'folder.settings.category' activeClass='selected' class="option" tagName="li"}}Organize{{/link-to}} {{#link-to 'folder.settings.category' activeClass='selected' class="option" tagName="li"}}Categorize{{/link-to}}
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -83,5 +83,30 @@ export default BaseService.extend({
return this.get('ajax').request(`category/${categoryId}`, { return this.get('ajax').request(`category/${categoryId}`, {
method: 'DELETE' method: 'DELETE'
}); });
} },
// Get list of users who can see given category
getViewers(categoryId) {
return this.get('ajax').request(`category/${categoryId}/permission`, {
method: 'GET'
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('user', obj);
return this.get('store').push(data);
});
return data;
});
},
// Save list of users who can see given category
setViewers(categoryId, viewers) {
return this.get('ajax').request(`category/${categoryId}/permission`, {
method: 'PUT',
contentType: 'json',
data: JSON.stringify(viewers)
});
},
}); });

View file

@ -66,7 +66,6 @@ export default Ember.Service.extend({
}); });
}, },
// Returns all users that can see folder. // Returns all users that can see folder.
getFolderUsers(folderId) { getFolderUsers(folderId) {
let url = `users/folder/${folderId}`; let url = `users/folder/${folderId}`;

View file

@ -48,13 +48,12 @@
#sidebar-wrapper, #sidebar-wrapper,
.sidebar-wrapper, .sidebar-wrapper,
.sidebar-common, .sidebar-common,
.sidebar-toolbar,
.sidebar-panel, .sidebar-panel,
.edit-document-heading, .edit-document-heading,
.back-to-space, .back-to-space,
.start-section, .start-section,
.start-button, .start-button,
.is-a-tab, .is-a-tab,
.new-section-wizard { .new-section-wizard {
float: none !important; float: none !important;
display: none !important; display: none !important;

View file

@ -15,7 +15,8 @@
} }
.back-to-space { .back-to-space {
margin: 10px 0; margin: 0 0 10px 0;
display: inline-block;
> a { > a {
> .regular-button { > .regular-button {

View file

@ -2,8 +2,8 @@
@import "history.scss"; @import "history.scss";
@import "inline-editor.scss"; @import "inline-editor.scss";
@import "section-editor.scss"; @import "section-editor.scss";
@import "sidebar-view-activity.scss"; @import "activity.scss";
@import "sidebar-view-attachments.scss"; @import "attachments.scss";
@import "sidebar-view-index.scss"; @import "toc.scss";
@import "view.scss"; @import "view.scss";
@import "wysiwyg.scss"; @import "wysiwyg.scss";

View file

@ -1,31 +1,17 @@
.document-sidebar-view-attachments { .document-attachments {
margin: 0; margin: 0;
> .upload-document-files { > .upload-document-files {
width: 100%; margin: 10px 0 0 0;
padding: 20px;
margin-bottom: 20px;
text-align: center;
color: $color-gray;
border: 1px solid $color-stroke;
cursor: pointer;
font-size: 0.9rem;
line-height: 1.7rem;
@include ease-in(); @include ease-in();
&:hover { > .dz-preview, .dz-processing {
border-color: $color-link;
color: $color-link;
}
> .dz-preview,
.dz-processing {
display: none !important; display: none !important;
} }
} }
> .list { > .list {
margin: 0 0 50px; margin: 20px 0 0 0;
padding: 7px 0; padding: 7px 0;
> .item { > .item {
@ -45,7 +31,7 @@
> a { > a {
@extend .truncate; @extend .truncate;
width: 200px; width: 80%;
color: $color-gray; color: $color-gray;
&:hover { &:hover {
@ -56,7 +42,8 @@
@extend .truncate; @extend .truncate;
display: inline-block; display: inline-block;
font-size: 0.9rem; font-size: 0.9rem;
width: 200px; width: 80%;
vertical-align: text-top;
} }
} }

View file

@ -8,7 +8,7 @@
.doc-excerpt { .doc-excerpt {
font-size: 1rem; font-size: 1rem;
color: $color-gray; color: $color-gray;
margin: 0 0 45px; margin: 0 0 60px;
} }
.edit-document-heading { .edit-document-heading {
@ -341,3 +341,25 @@
.dropdown-page-toolbar { .dropdown-page-toolbar {
width: 300px; width: 300px;
} }
.document-toolbar {
> .round-button-mono {
background-color: $color-white;
border: 1px solid $color-gray;
> .material-icons {
@include ease-in();
color: $color-gray;
}
}
}
.document-template-header {
color: $color-goldy;
font-size: 1.5em;
margin-bottom: 20px;
}
.document-tags {
margin-top: 15px;
}

View file

@ -43,15 +43,26 @@
> .row { > .row {
margin: 15px 0; margin: 15px 0;
padding: 8px 10px; padding: 15px;
background-color: $color-off-white; background-color: $color-off-white;
@include border-radius(2px); @include border-radius(2px);
> .category { > .category {
font-size: 1.2rem;
vertical-align: bottom;
display: inline-block; display: inline-block;
margin-top: 8px;
> .name {
font-size: 1.2rem;
}
> .info {
font-size: 0.9rem;
margin-top: 8px;
color: $color-gray;
}
}
> .buttons {
margin-top: 5px;
} }
> .action { > .action {
@ -63,7 +74,7 @@
display: inline-block; display: inline-block;
> input { > input {
margin: 0; margin: 0 0 8px 0;
padding: 0; padding: 0;
font-size: 1.2rem; font-size: 1.2rem;
} }
@ -72,3 +83,6 @@
} }
} }
.category-access-dialog {
display: none;
}

View file

@ -58,48 +58,6 @@ $sidebar-width: 400px;
#sidebar-wrapper { #sidebar-wrapper {
width: $sidebar-width; width: $sidebar-width;
} }
// #page-content-wrapper {
// padding: 30px;
// position: relative;
// }
}
.sidebar-toolbar {
display: inline-block;
width: 60px;
background-color: $color-primary;
text-align: center;
position: fixed;
left: 0;
top: 0;
height: 100%;
padding: 40px 0 0 0;
> .selected {
background-color: $color-link !important;
border: 1px solid $color-link !important;
> .material-icons {
color: $color-white !important;
}
}
> .round-button-mono {
background-color: $color-off-white;
border: 1px solid $color-off-white;
> .material-icons {
@include ease-in();
color: $color-gray;
}
&:hover {
> .material-icons {
color: $color-link;
}
}
}
} }
.sidebar-common { .sidebar-common {
@ -108,20 +66,6 @@ $sidebar-width: 400px;
padding: 40px 20px 0px 20px; padding: 40px 20px 0px 20px;
margin-left: 20px; margin-left: 20px;
> .pinner {
cursor: pointer;
> .material-icons {
color: $color-primary;
}
}
> .template-header {
color: $color-goldy;
font-size: 1.5em;
margin-bottom: 20px;
}
.zone-sidebar-page-title { .zone-sidebar-page-title {
color: $color-primary; color: $color-primary;
font-size: 1.3rem; font-size: 1.3rem;
@ -150,7 +94,7 @@ $sidebar-width: 400px;
margin-bottom: 30px; margin-bottom: 30px;
} }
.folder-sidebar-form-wrapper, .document-sidebar-form-wrapper { .document-sidebar-form-wrapper {
padding: 20px; padding: 20px;
border: 1px solid $color-stroke; border: 1px solid $color-stroke;
@include border-radius(3px); @include border-radius(3px);

View file

@ -0,0 +1,41 @@
.widget-list-picker {
margin: 10px 0;
> .options {
width: 300px;
max-height: 400px;
overflow: auto;
> .option {
margin: 0 0 5px 0;
padding: 10px 15px;
color: $color-gray;
background-color: $color-off-white;
cursor: pointer;
position: relative;
&:hover {
color: $color-white;
background-color: $color-gray;
}
> .text {
width: 220px;
overflow: hidden;
font-weight: bold;
}
> .material-icons {
position: absolute;
top: 10px;
right: 10px;
color: $color-white;
}
}
> .selected {
color: $color-white !important;
background-color: $color-link !important;
}
}
}

View file

@ -64,15 +64,16 @@
@import "widget-avatar"; @import "widget-avatar";
@import "widget-button"; @import "widget-button";
@import "widget-card"; @import "widget-card";
@import "widget-checkbox";
@import "widget-chip"; @import "widget-chip";
@import "widget-dropdown"; @import "widget-dropdown";
@import "widget-input"; @import "widget-input";
@import "widget-list-picker";
@import "widget-notification"; @import "widget-notification";
@import "widget-radio";
@import "widget-selection";
@import "widget-sidebar-menu"; @import "widget-sidebar-menu";
@import "widget-symbol";
@import "widget-tab";
@import "widget-table"; @import "widget-table";
@import "widget-tooltip"; @import "widget-tooltip";
@import "widget-checkbox";
@import "widget-radio";
@import "widget-tab";
@import "widget-selection";
@import "widget-symbol";

View file

@ -0,0 +1,41 @@
<div class="document-attachments">
{{#if hasAttachments}}
<ul class="list">
{{#each files key="id" as |a index|}}
<li class="item">
<img class="icon" src="/assets/img/attachments/{{document/file-icon a.extension}}" />
<a href="{{ appMeta.endpoint }}/public/attachments/{{ appMeta.orgId }}/{{ a.id }}">
<span class="file">{{ a.filename }}</span>
</a>
{{#if permissions.documentEdit}}
<div class="action round-button-mono">
<i class="material-icons color-gray delete-attachment-{{a.id}}" title="Delete" {{action 'onConfirmDelete' a.id a.filename}}>delete</i>
</div>
{{/if}}
</li>
{{/each}}
</ul>
{{/if}}
{{#if permissions.documentEdit}}
<div class="upload-document-files">
<div class="chip chip-action">
<span id="upload-document-files" class="chip-text">+ attachment</span>
</div>
</div>
{{/if}}
</div>
<div class="dropdown-dialog delete-attachment-dialog">
<div class="content">
<p>Are you sure you want to delete <span class="bold">{{deleteAttachment.name}}?</span></p>
</div>
<div class="actions">
<div class="flat-button" {{action 'onCancel'}}>
cancel
</div>
<div class="flat-button flat-red" {{action 'onDelete'}}>
delete
</div>
</div>
<div class="clearfix"></div>
</div>

View file

@ -0,0 +1,35 @@
<div class="sidebar-wrapper">
<div class="sidebar-panel">
<div class="document-sidebar-view-index">
<div class="structure">
{{#if session.authenticated}}
{{#unless emptyState}}
<div id="tocToolbar" class="hidden-xs hidden-sm toc-controls {{if state.actionablePage 'current-page' ''}}">
<div id="toc-up-button" class="round-button-mono {{if state.upDisabled 'disabled'}}" data-tooltip="Move up" data-tooltip-position="top center" {{action 'pageUp'}}>
<i class="material-icons">arrow_upward</i>
</div>
<div class="button-gap" />
<div id="toc-down-button" class="round-button-mono {{if state.downDisabled 'disabled'}}" data-tooltip="Move down" data-tooltip-position="top center" {{action 'pageDown'}}>
<i class="material-icons">arrow_downward</i>
</div>
<div class="button-gap" />
<div id="toc-outdent-button" class="round-button-mono {{if state.outdentDisabled 'disabled'}}" data-tooltip="Outdent" data-tooltip-position="top center" {{action 'pageOutdent'}}>
<i class="material-icons">format_indent_decrease</i>
</div>
<div class="button-gap" />
<div id="toc-indent-button" class="round-button-mono {{if state.indentDisabled 'disabled'}}" data-tooltip="Indent" data-tooltip-position="top center" {{action 'pageIndent'}}>
<i class="material-icons">format_indent_increase</i>
</div>
</div>
{{/unless}}
{{/if}}
<ul class="index-list">
{{#each pages key="id" as |p index|}}
{{document/index-entry page=p index=index onClick=(action 'onEntryClick')}}
{{/each}}
</ul>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,80 @@
{{#if (is-equal tab 'activitysdsd')}}
{{document/sidebar-view-activity document=document pages=pages permissions=permissions}}
{{/if}}
<div class="document-toolbar">
<div class="round-button-mono" id="document-more-button">
<i class="material-icons">more_vert</i>
</div>
</div>
{{#dropdown-menu target="document-more-button" position="bottom right" open="click" onOpenCallback=(action 'onMenuOpen') onCloseCallback=(action 'onMenuOpen')}}
<ul class="menu">
{{#if session.authenticated}}
{{#if (is-equal document.layout 'section')}}
<li class="item" {{action 'onLayoutChange' 'doc'}}>Flat view</li>
{{else}}
<li class="item" {{action 'onLayoutChange' 'section'}}>Section view</li>
{{/if}}
{{#if pinState.isPinned}}
<li class="item" {{action 'onUnpin'}}>Unfavorite</li>
{{else}}
<li class="item" id="pin-document-button">Favorite</li>
{{/if}}
{{#if permissions.documentEdit}}
<li class="item">{{#link-to 'document.history'}}History{{/link-to}}</li>
{{/if}}
{{/if}}
{{#if permissions.documentTemplate}}
<li class="item" id="save-template-button">Template</li>
{{/if}}
<li class="divider"/>
<li class="item" id="print-document-button" {{action 'onPrintDocument'}}>Print</li>
{{#if permissions.documentDelete}}
<li class="item danger" id="delete-document-button">Delete</li>
{{/if}}
</ul>
{{/dropdown-menu}}
{{#if session.authenticated}}
{{#if menuOpen}}
{{#unless pinState.isPinned}}
{{#dropdown-dialog target="pin-document-button" position="bottom right" button="Pin" color="flat-green" onAction=(action 'onPin') focusOn="pin-document-name" }}
<div class="input-control">
<label>Favorite Document</label>
<div class="tip">Provide short name</div>
{{input type='text' id="pin-document-name" value=pinState.newName}}
</div>
{{/dropdown-dialog}}
{{/unless}}
{{/if}}
{{#if permissions.documentDelete}}
{{#if menuOpen}}
{{#dropdown-dialog target="delete-document-button" position="bottom right" button="Delete" color="flat-red" onAction=(action 'onDeleteDocument')}}
<p>Are you sure you want to delete this document?</p>
<p>There is no undo, so be careful.</p>
{{/dropdown-dialog}}
{{/if}}
{{/if}}
{{#if permissions.documentTemplate}}
{{#if menuOpen}}
{{#dropdown-dialog target="save-template-button" position="bottom right" button="Save as Template" color="flat-green" onAction=(action 'onSaveTemplate') focusOn="new-template-name" }}
<div class="input-control">
<label>Name</label>
<div class="tip">Short name for this type of document</div>
{{input type='text' id="new-template-name" value=saveTemplate.name}}
</div>
<div class="input-control">
<label>Excerpt</label>
<div class="tip">Explain use case for this template</div>
{{textarea value=saveTemplate.description rows="3" id="new-template-desc"}}
</div>
{{/dropdown-dialog}}
{{/if}}
{{/if}}
{{/if}}

View file

@ -1,3 +1,5 @@
{{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}}

View file

@ -1,47 +0,0 @@
<div class="sidebar-panel">
<div class="title">Attachments</div>
<div class="document-sidebar-view-attachments">
{{#if permissions.documentEdit}}
<div id="upload-document-files" class="upload-document-files">
Drag-drop files or click to select files
</div>
{{/if}}
<ul class="list">
{{#each files key="id" as |a index|}}
<li class="item">
<img class="icon" src="/assets/img/attachments/{{document/file-icon a.extension}}" />
<a href="{{ appMeta.endpoint }}/public/attachments/{{ appMeta.orgId }}/{{ a.id }}">
<span class="file">{{ a.filename }}</span>
</a>
{{#if permissions.documentEdit}}
<div class="action round-button-mono">
<i class="material-icons color-gray delete-attachment-{{a.id}}" title="Delete" {{action 'onConfirmDelete' a.id a.filename}}>delete</i>
</div>
{{/if}}
</li>
{{/each}}
</ul>
{{#if emptyState}}
<div class="explainer">
<div class="empty-state">
There are no attachments
</div>
</div>
{{/if}}
</div>
<div class="dropdown-dialog delete-attachment-dialog">
<div class="content">
<p>Are you sure you want to delete <span class="bold">{{deleteAttachment.name}}?</span></p>
</div>
<div class="actions">
<div class="flat-button" {{action 'onCancel'}}>
cancel
</div>
<div class="flat-button flat-red" {{action 'onDelete'}}>
delete
</div>
</div>
<div class="clearfix"></div>
</div>
</div>

View file

@ -1,34 +0,0 @@
<div class="sidebar-panel">
<div class="title">Index</div>
<div class="document-sidebar-view-index">
<div class="structure">
{{#if this.session.authenticated}}
{{#unless emptyState}}
<div id="tocToolbar" class="hidden-xs hidden-sm toc-controls {{if state.actionablePage 'current-page' ''}}">
<div id="toc-up-button" class="round-button-mono {{if state.upDisabled 'disabled'}}" data-tooltip="Move up" data-tooltip-position="top center" {{action 'pageUp'}}>
<i class="material-icons">arrow_upward</i>
</div>
<div class="button-gap" />
<div id="toc-down-button" class="round-button-mono {{if state.downDisabled 'disabled'}}" data-tooltip="Move down" data-tooltip-position="top center" {{action 'pageDown'}}>
<i class="material-icons">arrow_downward</i>
</div>
<div class="button-gap" />
<div id="toc-outdent-button" class="round-button-mono {{if state.outdentDisabled 'disabled'}}" data-tooltip="Outdent" data-tooltip-position="top center" {{action 'pageOutdent'}}>
<i class="material-icons">format_indent_decrease</i>
</div>
<div class="button-gap" />
<div id="toc-indent-button" class="round-button-mono {{if state.indentDisabled 'disabled'}}" data-tooltip="Indent" data-tooltip-position="top center" {{action 'pageIndent'}}>
<i class="material-icons">format_indent_increase</i>
</div>
</div>
{{/unless}}
{{/if}}
<ul class="index-list">
{{#each pages key="id" as |p index|}}
{{document/sidebar-view-index-entry page=p index=index onClick=(action 'onEntryClick')}}
{{/each}}
</ul>
</div>
</div>
</div>

View file

@ -1,118 +0,0 @@
<div class="sidebar-toolbar">
<div class="round-button-mono" id="sidebar-zone-more-button">
<i class="material-icons">more_horiz</i>
</div>
<div class="margin-top-20"></div>
<div class="round-button-mono {{if (is-equal tab 'index') 'selected'}}" {{action 'onChangeTab' 'index'}}>
<i class="material-icons">view_headline</i>
</div>
<div class="margin-top-20"></div>
<div class="round-button-mono {{if (is-equal tab 'attachments') 'selected'}}" {{action 'onChangeTab' 'attachments'}}>
<i class="material-icons">attach_file</i>
</div>
{{#if permissions.documentEdit}}
<div class="margin-top-20"></div>
<div class="round-button-mono {{if (is-equal tab 'activity') 'selected'}}" {{action 'onChangeTab' 'activity'}}>
<i class="material-icons">timeline</i>
</div>
{{/if}}
</div>
<div class="sidebar-common">
{{#if document.template}}
<div class="template-header">Template</div>
{{/if}}
{{document/tag-editor documentTags=document.tags permissions=permissions onChange=(action 'onTagChange')}}
</div>
<div class="sidebar-wrapper">
{{#if (is-equal tab 'index')}}
{{document/sidebar-view-index document=document folder=folder pages=pages page=page permissions=permissions
onPageSequenceChange=(action 'onPageSequenceChange') onPageLevelChange=(action 'onPageLevelChange') onGotoPage=(action 'onGotoPage')}}
{{/if}}
{{#if (is-equal tab 'attachments')}}
{{document/sidebar-view-attachments document=document permissions=permissions}}
{{/if}}
{{#if (is-equal tab 'activity')}}
{{document/sidebar-view-activity document=document pages=pages permissions=permissions}}
{{/if}}
</div>
{{#dropdown-menu target="sidebar-zone-more-button" position="bottom left" open="click" onOpenCallback=(action 'onMenuOpen') onCloseCallback=(action 'onMenuOpen')}}
<ul class="menu">
{{#if session.authenticated}}
{{#if (is-equal document.layout 'section')}}
<li class="item" {{action 'onLayoutChange' 'doc'}}>Flat view</li>
<li class="divider"></li>
{{else}}
<li class="item" {{action 'onLayoutChange' 'section'}}>Section view</li>
<li class="divider"></li>
{{/if}}
{{#if pinState.isPinned}}
<li class="item" {{action 'onUnpin'}}>Unpin</li>
{{else}}
<li class="item" id="pin-document-button">Pin</li>
{{/if}}
{{#if permissions.documentEdit}}
<li class="item">
{{#link-to 'document.history'}}History{{/link-to}}
</li>
<li class="divider"></li>
{{/if}}
{{/if}}
{{#if permissions.documentTemplate}}
<li class="item" id="save-template-button">Template</li>
<li class="divider"></li>
{{/if}}
<li class="item" id="print-document-button" {{action 'onPrintDocument'}}>Print</li>
{{#if permissions.documentDelete}}
<li class="divider"></li>
<li class="item danger" id="delete-document-button">Delete</li>
{{/if}}
</ul>
{{/dropdown-menu}}
{{#if session.authenticated}}
{{#if menuOpen}}
{{#unless pinState.isPinned}}
{{#dropdown-dialog target="pin-document-button" position="bottom left" button="Pin" color="flat-green" onAction=(action 'onPin') focusOn="pin-document-name" }}
<div class="input-control">
<label>Pin Document</label>
<div class="tip">A 3 or 4 character name</div>
{{input type='text' id="pin-document-name" value=pinState.newName}}
</div>
{{/dropdown-dialog}}
{{/unless}}
{{/if}}
{{#if permissions.documentDelete}}
{{#if menuOpen}}
{{#dropdown-dialog target="delete-document-button" position="bottom left" button="Delete" color="flat-red" onAction=(action 'onDeleteDocument')}}
<p>Are you sure you want to delete this document?</p>
<p>There is no undo, so be careful.</p>
{{/dropdown-dialog}}
{{/if}}
{{/if}}
{{#if permissions.documentTemplate}}
{{#if menuOpen}}
{{#dropdown-dialog target="save-template-button" position="bottom left" button="Save as Template" color="flat-green" onAction=(action 'onSaveTemplate') focusOn="new-template-name" }}
<div class="input-control">
<label>Name</label>
<div class="tip">Short name for this type of document</div>
{{input type='text' id="new-template-name" value=saveTemplate.name}}
</div>
<div class="input-control">
<label>Excerpt</label>
<div class="tip">Explain use case for this template</div>
{{textarea value=saveTemplate.description rows="3" id="new-template-desc"}}
</div>
{{/dropdown-dialog}}
{{/if}}
{{/if}}
{{/if}}

View file

@ -0,0 +1,14 @@
<div class="back-to-space">
{{#link-to 'folder' folder.id folder.slug}}
<div class="regular-button button-gray">
<i class="material-icons">arrow_back</i>
<div class="name">{{folder.name}}</div>
</div>
{{/link-to}}
</div>
<div class="document-category hide">
<div class="chip chip-action">
<span id="upload-document-files" class="chip-text">+ category</span>
</div>
</div>

View file

@ -9,7 +9,7 @@
{{/each}} {{/each}}
{{#if canAdd}} {{#if canAdd}}
<div class="chip-action"> <div class="chip-action">
<span id="add-tag-button" class="chip-text">add tag</span> <span id="add-tag-button" class="chip-text">+ tag</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

@ -2,7 +2,7 @@
<div class="panel"> <div class="panel">
<div class="form-header"> <div class="form-header">
<div class="title">Categories</div> <div class="title">Categories</div>
<div class="tip">Organize and secure document access with optional categories</div> <div class="tip">Sub-divide spaces and secure document access with categories</div>
</div> </div>
<form id="category-form" {{action 'onAdd' on='submit'}}> <form id="category-form" {{action 'onAdd' on='submit'}}>
<div class="input-control"> <div class="input-control">
@ -14,26 +14,34 @@
{{focus-input id=(concat 'edit-category-' cat.id) type="text" value=cat.category class="input-inline"}} {{focus-input id=(concat 'edit-category-' cat.id) type="text" value=cat.category class="input-inline"}}
</div> </div>
{{else}} {{else}}
<div class="category">{{cat.category}}</div> <div class="category">
<div class="name">{{cat.category}}</div>
<div class="info">7 documents, 14 people</div>
</div>
{{/if}} {{/if}}
<div class="pull-right"> <div class="pull-right buttons">
{{#if cat.editMode}} {{#if cat.editMode}}
<button type="submit" class="round-button-mono" {{action 'onSave' cat.id}}> <button type="submit" class="round-button-mono" {{action 'onSave' cat.id}}>
<i class="material-icons color-green">check</i> <i class="material-icons color-green">check</i>
</button> </button>
<div class="round-button-mono" {{action 'onCancel' cat.id}}> <div class="round-button-mono" {{action 'onEditCancel' cat.id}}>
<i class="material-icons color-gray">close</i> <i class="material-icons color-gray">close</i>
</div> </div>
{{else}} {{else}}
<div {{action 'onEdit' cat.id}} class="action round-button-mono button-white"> <div class="">
<i class="material-icons">edit</i> <div id="category-access-button-{{cat.id}}" data-tooltip="Set user access" data-tooltip-position="top center" class="action round-button-mono button-white" {{action 'onShowAccessPicker' cat.id}}>
<i class="material-icons">person</i>
</div>
<div {{action 'onEdit' cat.id}} class="action round-button-mono button-white">
<i class="material-icons">edit</i>
</div>
<div id="{{concat 'delete-category-' cat.id}}" class="action round-button-mono button-white">
<i class="material-icons">delete</i>
</div>
{{#dropdown-dialog target=(concat 'delete-category-' cat.id) position="bottom right" button="Delete" color="flat-red" onAction=(action 'onDelete' cat.id)}}
<p>Are you sure you want to delete category <b>{{cat.category}}?</b></p>
{{/dropdown-dialog}}
</div> </div>
<div id="{{concat 'delete-category-' cat.id}}" class="action round-button-mono button-white">
<i class="material-icons">delete</i>
</div>
{{#dropdown-dialog target=(concat 'delete-category-' cat.id) position="bottom right" button="Delete" color="flat-red" onAction=(action 'onDelete' cat.id)}}
<p>Are you sure you want to delete category <b>{{cat.category}}?</b></p>
{{/dropdown-dialog}}
{{/if}} {{/if}}
</div> </div>
</div> </div>
@ -51,3 +59,18 @@
</form> </form>
</div> </div>
</div> </div>
<div class="dropdown-dialog category-access-dialog">
<div class="content">
{{ui/ui-list-picker items=categoryUsers nameField='fullname'}}
</div>
<div class="actions">
<div class="flat-button" {{action 'onGrantCancel'}}>
cancel
</div>
<div class="flat-button flat-blue" {{action 'onGrantAccess'}}>
grant access
</div>
</div>
<div class="clearfix"></div>
</div>

View file

@ -54,7 +54,7 @@
{{#if permissions.spaceManage}} {{#if permissions.spaceManage}}
<div class="button-gap"></div> <div class="button-gap"></div>
{{#link-to 'folder.settings' folder.id folder.slug}}{{model.document.name}} {{#link-to 'folder.settings' folder.id folder.slug}}{{model.document.name}}
<div class="round-button button-blue" id="space-settings-button" data-tooltip="Manage permissions" data-tooltip-position="top center"> <div class="round-button button-gray" id="space-settings-button" data-tooltip="Manage permissions" data-tooltip-position="top center">
<i class="material-icons">settings</i> <i class="material-icons">settings</i>
</div> </div>
{{/link-to}} {{/link-to}}

View file

@ -0,0 +1,12 @@
<div class="widget-list-picker">
<ul class="options">
{{#each items as |item|}}
<li class="option {{if item.selected 'selected'}}" {{action 'onToggle' item}}>
<div class="text">{{get item nameField}}</div>
{{#if item.selected}}
<i class="material-icons">check</i>
{{/if}}
</li>
{{/each}}
</ul>
</div>

View file

@ -40,6 +40,7 @@ const (
EventTypeSpacePermission EventType = "changed-space-permissions" EventTypeSpacePermission EventType = "changed-space-permissions"
EventTypeSpaceJoin EventType = "joined-space" EventTypeSpaceJoin EventType = "joined-space"
EventTypeSpaceInvite EventType = "invited-space" EventTypeSpaceInvite EventType = "invited-space"
EventTypeCategoryPermission EventType = "changed-category-permissions"
EventTypeSectionAdd EventType = "added-document-section" EventTypeSectionAdd EventType = "added-document-section"
EventTypeSectionUpdate EventType = "updated-document-section" EventTypeSectionUpdate EventType = "updated-document-section"
EventTypeSectionDelete EventType = "removed-document-section" EventTypeSectionDelete EventType = "removed-document-section"

View file

@ -16,7 +16,7 @@ import "time"
// Permission represents a permission for a space and is persisted to the database. // Permission represents a permission for a space and is persisted to the database.
type Permission struct { type Permission struct {
ID uint64 `json:"id"` ID uint64 `json:"id"`
OrgID string `json:"-"` OrgID string `json:"orgId"`
Who string `json:"who"` // user, role Who string `json:"who"` // user, role
WhoID string `json:"whoId"` // either a user or role ID WhoID string `json:"whoId"` // either a user or role ID
Action Action `json:"action"` // view, edit, delete Action Action `json:"action"` // view, edit, delete
@ -49,6 +49,9 @@ const (
DocumentCopy Action = "doc-copy" DocumentCopy Action = "doc-copy"
// DocumentTemplate means you can create, edit and delete document templates and content blocks // DocumentTemplate means you can create, edit and delete document templates and content blocks
DocumentTemplate Action = "doc-template" DocumentTemplate Action = "doc-template"
// CategoryView action means you can view a category and documents therein
CategoryView Action = "view"
) )
// Record represents space permissions for a user on a space. // Record represents space permissions for a user on a space.
@ -178,3 +181,11 @@ func EncodeRecord(r Record, a Action) (p Permission) {
return return
} }
// CategoryViewRequestModel represents who should be allowed to see a category.
type CategoryViewRequestModel struct {
OrgID string `json:"orgId"`
SpaceID string `json:"folderId"`
CategoryID string `json:"categoryID"`
UserID string `json:"userId"`
}

View file

@ -132,6 +132,8 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "category", []string{"POST", "OPTIONS"}, nil, category.Add) Add(rt, RoutePrefixPrivate, "category", []string{"POST", "OPTIONS"}, nil, category.Add)
Add(rt, RoutePrefixPrivate, "category/{categoryID}", []string{"PUT", "OPTIONS"}, nil, category.Update) Add(rt, RoutePrefixPrivate, "category/{categoryID}", []string{"PUT", "OPTIONS"}, nil, category.Update)
Add(rt, RoutePrefixPrivate, "category/{categoryID}", []string{"DELETE", "OPTIONS"}, nil, category.Delete) Add(rt, RoutePrefixPrivate, "category/{categoryID}", []string{"DELETE", "OPTIONS"}, nil, category.Delete)
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, "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)