1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-21 14:19:43 +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 !!!
7. link/unlink document to category
8. filter space documents by category -- URL param? nested route?
- category view permission handling
- filter users using new permission
- 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/permission"
"github.com/documize/community/model/space"
"github.com/documize/community/model/user"
)
// 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"
ctx := domain.GetRequestContext(r)
if !ctx.Editor {
response.WriteForbiddenError(w)
return
}
id := request.Param(r, "spaceID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "spaceID")
return
}
sp, err := h.Store.Space.Get(ctx, id)
if err != nil {
response.WriteNotFoundError(w, method, "space not found")
if !HasPermission(ctx, *h.Store, id, permission.SpaceManage, permission.SpaceOwner) {
response.WriteForbiddenError(w)
return
}
if sp.UserID != ctx.UserID {
response.WriteForbiddenError(w)
sp, err := h.Store.Space.Get(ctx, id)
if err != nil {
response.WriteNotFoundError(w, method, "space not found")
return
}
@ -220,7 +216,7 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
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) {
method := "space.GetPermissions"
ctx := domain.GetRequestContext(r)
@ -276,3 +272,105 @@ func (h *Handler) GetUserSpacePermissions(w http.ResponseWriter, r *http.Request
record := permission.DecodeUserPermissions(perms)
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/store/mysql"
"github.com/documize/community/model/permission"
"github.com/documize/community/model/user"
"github.com/pkg/errors"
)
@ -155,3 +156,50 @@ func (s Scope) DeleteSpaceCategoryPermissions(ctx domain.RequestContext, spaceID
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)
DeleteCategoryPermissions(ctx RequestContext, categoryID 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

View file

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

View file

@ -31,14 +31,9 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
name: "",
description: ""
},
tab: '',
init() {
this._super(...arguments);
if (is.empty(this.get('tab')) || is.undefined(this.get('tab'))) {
this.set('tab', 'index');
}
},
didReceiveAttrs() {
@ -49,20 +44,18 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
this.set('pinState.pinId', this.get('pinned').isDocumentPinned(this.get('document.id')));
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: {
onChangeTab(tab) {
this.set('tab', tab);
},
onTagChange(tags) {
let doc = this.get('document');
doc.set('tags', tags);
this.get('documentService').save(doc);
},
onMenuOpen() {
this.set('menuOpen', !this.get('menuOpen'));
},
@ -140,7 +133,10 @@ export default Ember.Component.extend(TooltipMixin, NotifierMixin, {
onLayoutChange(layout) {
let doc = this.get('document');
doc.set('layout', layout);
if (this.get('permissions.documentEdit')) {
this.get('documentService').save(doc);
}
return true;
}

View file

@ -76,7 +76,7 @@ export default Ember.Component.extend(TooltipMixin, {
let permissions = this.get('permissions');
return permissions.get('documentDelete') || permissions.get('documentCopy') ||
permissions.get('documentMove') || permissions.get('documentTemplate');;
permissions.get('documentMove') || permissions.get('documentTemplate');
}),
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 NotifierMixin from '../../mixins/notifier';
import TooltipMixin from '../../mixins/tooltip';
const {
inject: { service }
} = Ember;
export default Ember.Component.extend(NotifierMixin, {
folderService: service('folder'),
export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
userService: service('user'),
categoryService: service('category'),
appMeta: service(),
store: service(),
newCategory: '',
drop: null,
users: [],
didReceiveAttrs() {
this.load();
},
didRender() {
// this.addTooltip(this.$(".action"));
},
willDestroyElement() {
let drop = this.get('drop');
if (is.not.null(drop)) {
drop.destroy();
}
},
load() {
// get categories
this.get('categoryService').getAll(this.get('folder.id')).then((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);
},
onCancel(id) {
onEditCancel(id) {
this.setEdit(id, false);
this.load();
},
@ -94,6 +128,69 @@ export default Ember.Component.extend(NotifierMixin, {
this.get('categoryService').save(cat).then(() => {
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')) {
this.addTooltip(document.getElementById("move-documents-button"));
}
if (this.get('permissions.documentDelete')) {
this.addTooltip(document.getElementById("delete-documents-button"));
}
@ -67,10 +68,12 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, AuthMixin, {
if (this.get('permissions.spaceOwner')) {
this.addTooltip(document.getElementById("space-delete-button"));
}
if (this.get('permissions.spaceManage')) {
this.addTooltip(document.getElementById("space-settings-button"));
}
if (this.get('session.authenticated')) {
if (this.get('pinState.isPinned')) {
this.addTooltip(document.getElementById("space-unpin-button"));
} else {
this.addTooltip(document.getElementById("space-pin-button"));

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,7 +15,6 @@ export default Ember.Mixin.create({
tooltips: [],
addTooltip(elem) {
if (elem == null) {
return;
}

View file

@ -226,6 +226,12 @@ export default Ember.Controller.extend(NotifierMixin, {
if (this.get('pageId') !== id && 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-sidebar}}
{{document/sidebar-zone folders=model.folders folder=model.folder document=model.document
pages=model.pages sections=model.section links=model.links permissions=model.permissions tab=tab
onDocumentDelete=(action 'onDocumentDelete') onSaveTemplate=(action 'onSaveTemplate')
{{document/document-index
document=model.document folder=model.folder pages=model.pages page=model.page permissions=model.permissions
onPageSequenceChange=(action 'onPageSequenceChange') onPageLevelChange=(action 'onPageLevelChange')
onGotoPage=(action 'onGotoPage')}}
{{/layout/zone-sidebar}}
{{#layout/zone-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="regular-button button-gray">
<i class="material-icons">arrow_back</i>
<div class="name">{{model.folder.name}}</div>
<div class="pull-left">
{{document/space-category document=model.document folder=model.folder folders=model.folders permissions=model.permissions}}
</div>
{{/link-to}}
<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-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
onSavePage=(action 'onSavePage') onInsertSection=(action 'onInsertSection')
onSavePageAsBlock=(action 'onSavePageAsBlock') onDeleteBlock=(action 'onDeleteBlock') onGotoPage=(action 'onGotoPage')
onCopyPage=(action 'onCopyPage') onMovePage=(action 'onMovePage') onDeletePage=(action 'onPageDeleted')}}
</div>
{{/layout/zone-content}}
{{/layout/zone-container}}

View file

@ -15,7 +15,7 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout
export default Ember.Route.extend(AuthenticatedRouteMixin, {
beforeModel: function (transition) {
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}}
{{/if}}
{{#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>
</div>
</div>

View file

@ -83,5 +83,30 @@ export default BaseService.extend({
return this.get('ajax').request(`category/${categoryId}`, {
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.
getFolderUsers(folderId) {
let url = `users/folder/${folderId}`;

View file

@ -48,7 +48,6 @@
#sidebar-wrapper,
.sidebar-wrapper,
.sidebar-common,
.sidebar-toolbar,
.sidebar-panel,
.edit-document-heading,
.back-to-space,

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@
.doc-excerpt {
font-size: 1rem;
color: $color-gray;
margin: 0 0 45px;
margin: 0 0 60px;
}
.edit-document-heading {
@ -341,3 +341,25 @@
.dropdown-page-toolbar {
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 {
margin: 15px 0;
padding: 8px 10px;
padding: 15px;
background-color: $color-off-white;
@include border-radius(2px);
> .category {
font-size: 1.2rem;
vertical-align: bottom;
display: inline-block;
> .name {
font-size: 1.2rem;
}
> .info {
font-size: 0.9rem;
margin-top: 8px;
color: $color-gray;
}
}
> .buttons {
margin-top: 5px;
}
> .action {
@ -63,7 +74,7 @@
display: inline-block;
> input {
margin: 0;
margin: 0 0 8px 0;
padding: 0;
font-size: 1.2rem;
}
@ -72,3 +83,6 @@
}
}
.category-access-dialog {
display: none;
}

View file

@ -58,48 +58,6 @@ $sidebar-width: 400px;
#sidebar-wrapper {
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 {
@ -108,20 +66,6 @@ $sidebar-width: 400px;
padding: 40px 20px 0px 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 {
color: $color-primary;
font-size: 1.3rem;
@ -150,7 +94,7 @@ $sidebar-width: 400px;
margin-bottom: 30px;
}
.folder-sidebar-form-wrapper, .document-sidebar-form-wrapper {
.document-sidebar-form-wrapper {
padding: 20px;
border: 1px solid $color-stroke;
@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-button";
@import "widget-card";
@import "widget-checkbox";
@import "widget-chip";
@import "widget-dropdown";
@import "widget-input";
@import "widget-list-picker";
@import "widget-notification";
@import "widget-radio";
@import "widget-selection";
@import "widget-sidebar-menu";
@import "widget-symbol";
@import "widget-tab";
@import "widget-table";
@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'}}">
{{#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}}
{{#if canAdd}}
<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>
{{#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">

View file

@ -2,7 +2,7 @@
<div class="panel">
<div class="form-header">
<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>
<form id="category-form" {{action 'onAdd' on='submit'}}>
<div class="input-control">
@ -14,17 +14,24 @@
{{focus-input id=(concat 'edit-category-' cat.id) type="text" value=cat.category class="input-inline"}}
</div>
{{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}}
<div class="pull-right">
<div class="pull-right buttons">
{{#if cat.editMode}}
<button type="submit" class="round-button-mono" {{action 'onSave' cat.id}}>
<i class="material-icons color-green">check</i>
</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>
</div>
{{else}}
<div class="">
<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>
@ -34,6 +41,7 @@
{{#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>
{{/if}}
</div>
</div>
@ -51,3 +59,18 @@
</form>
</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}}
<div class="button-gap"></div>
{{#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>
</div>
{{/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"
EventTypeSpaceJoin EventType = "joined-space"
EventTypeSpaceInvite EventType = "invited-space"
EventTypeCategoryPermission EventType = "changed-category-permissions"
EventTypeSectionAdd EventType = "added-document-section"
EventTypeSectionUpdate EventType = "updated-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.
type Permission struct {
ID uint64 `json:"id"`
OrgID string `json:"-"`
OrgID string `json:"orgId"`
Who string `json:"who"` // user, role
WhoID string `json:"whoId"` // either a user or role ID
Action Action `json:"action"` // view, edit, delete
@ -49,6 +49,9 @@ const (
DocumentCopy Action = "doc-copy"
// DocumentTemplate means you can create, edit and delete document templates and content blocks
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.
@ -178,3 +181,11 @@ func EncodeRecord(r Record, a Action) (p Permission) {
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/{categoryID}", []string{"PUT", "OPTIONS"}, nil, category.Update)
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", []string{"POST", "OPTIONS"}, nil, user.Add)