UNPKG

@nuxeo/nuxeo-ui-elements

Version:
833 lines (736 loc) 26.3 kB
/** @license (C) Copyright Nuxeo Corp. (http://nuxeo.com/) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { html } from '@polymer/polymer/lib/utils/html-tag.js'; import { mixinBehaviors } from '@polymer/polymer/lib/legacy/class.js'; import '@polymer/iron-form/iron-form.js'; import '@polymer/iron-icon/iron-icon.js'; import '@nuxeo/nuxeo-elements/nuxeo-connection.js'; import '@nuxeo/nuxeo-elements/nuxeo-element.js'; import '@nuxeo/nuxeo-elements/nuxeo-resource.js'; import '@polymer/paper-button/paper-button.js'; import '@polymer/paper-icon-button/paper-icon-button.js'; import '@polymer/paper-toast/paper-toast.js'; import '@polymer/polymer/lib/elements/dom-if.js'; import '@polymer/polymer/lib/elements/dom-repeat.js'; import { FiltersBehavior } from '../nuxeo-filters-behavior.js'; import { I18nBehavior } from '../nuxeo-i18n-behavior.js'; import '../nuxeo-pagination-controls.js'; import '../widgets/nuxeo-card.js'; import '../widgets/nuxeo-dialog.js'; import '../widgets/nuxeo-group-tag.js'; import '../widgets/nuxeo-input.js'; import '../widgets/nuxeo-tag.js'; import '../widgets/nuxeo-user-suggestion.js'; import '../widgets/nuxeo-selectivity.js'; import '../widgets/nuxeo-user-tag.js'; import './nuxeo-user-group-permissions-table.js'; import '../nuxeo-button-styles.js'; { /** * Used by `nuxeo-user-group-management` * An element for managing a group. * * Example: * * <nuxeo-group-management group="administrators"></nuxeo-group-management> * * @appliesMixin Nuxeo.I18nBehavior * @appliesMixin Nuxeo.FiltersBehavior * @memberof Nuxeo */ class GroupManagement extends mixinBehaviors([I18nBehavior, FiltersBehavior], Nuxeo.Element) { static get template() { return html` <style include="iron-flex iron-flex-alignment iron-flex-factors nuxeo-button-styles"> :host { display: block; } [hidden] { display: none !important; } h2 { color: var(--nuxeo-title-color, #213f7d); } nuxeo-user-group-permissions-table { margin-top: 1em; } .header-actions paper-button { margin-left: 1em; } .header-actions iron-icon { width: 1.3rem; } .card { background: none var(--nuxeo-box, #fff); box-shadow: 0 3px 5px rgba(0, 0, 0, 0.04); margin: 2em 0; padding: 2em 1.5em; } .header { margin-bottom: 2rem; @apply --layout-start; } h3 iron-icon { width: 1.3em; margin-right: 0.2rem; } h3 { font-weight: 400; font-size: 1.25rem; letter-spacing: 0; line-height: 1.625rem; } .groupname { margin: 0.5rem 0 0; } .grouplabel, .counter { font-weight: normal; margin: 0.5rem 0 0; } .avatar { margin: 0.5rem 0.5rem 0 0; width: 2rem; } .header-actions { height: 2.8rem; margin-top: 0.5em; } .activity-entry { margin-top: 15px; } .remove { @apply --nuxeo-link; text-decoration: underline; padding-left: 0.5em; font-size: 0.8rem; } .remove:hover { @apply --nuxeo-link-hover; } /* Table */ .title { margin: 0 0 0.8em 0; padding: 0; } .table { border: 1px solid var(--nuxeo-border, #e3e3e3); } .table-header { @apply --layout-horizontal; @apply --layout-center; background-color: var(--nuxeo-table-header-background, #fafafa); color: var(--nuxeo-table-header-titles, rgba(0, 0, 0, 0.54)); font-weight: 400; min-height: 48px; padding: 0 0 0 12px; border-bottom: 2px solid var(--nuxeo-border, #eee); box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2) inset; } .table-row { @apply --layout-horizontal; @apply --layout-center; padding: 12px 0 12px 12px; border-bottom: 1px solid var(--nuxeo-border, #eee); cursor: pointer; } .table-row:hover { background: var(--nuxeo-container-hover, #fafafa); } .table-row:last-of-type { border-bottom: none; } .table-actions { width: 3em; } .filter-wrapper { margin-top: 1em; } nuxeo-dialog[id='editGroupDialog'] { width: 40%; } .buttons { @apply --buttons-bar; } #editForm { padding: 1em 2em 2em; } .emptyResult { opacity: 0.5; display: block; font-weight: 300; padding: 1.5em 0.7em; text-align: center; } .preserve-white-space { white-space: pre; } </style> <nuxeo-connection user="{{_currentUser}}"></nuxeo-connection> <nuxeo-resource id="request" path="group/[[groupname]]" response="{{group}}" headers='{"fetch-group": "memberUsers,memberGroups"}' > </nuxeo-resource> <nuxeo-resource id="users" path="[[_usersPath(groupname)]]" response="{{memberUsers}}" auto></nuxeo-resource> <nuxeo-resource id="groups" path="[[_groupsPath(groupname)]]" response="{{memberGroups}}" auto></nuxeo-resource> <nuxeo-resource id="editRequest" path="group/[[groupname]]" response="{{group}}" headers='{"fetch-group": "memberUsers,memberGroups"}' > </nuxeo-resource> <paper-toast id="toast"></paper-toast> <nuxeo-dialog id="deleteGroupDialog" with-backdrop> <h2>[[i18n('groupManagement.delete.confirm')]]</h2> <div class="buttons horizontal end-justified layout"> <div class="flex start-justified"> <paper-button noink dialog-dismiss class="secondary">[[i18n('label.no')]]</paper-button> </div> <paper-button noink class="primary" on-click="_deleteGroup">[[i18n('label.yes')]]</paper-button> </div> </nuxeo-dialog> <nuxeo-dialog id="rmFromGroupDialog" with-backdrop class="vertical layout"> <h2>[[i18n('groupManagement.removeUserFromGroup.confirm', _removedMember.id)]]</h2> <div class="buttons horizontal end-justified layout"> <div class="flex start-justified"> <paper-button noink dialog-dismiss class="secondary">[[i18n('label.no')]]</paper-button> </div> <paper-button noink class="primary" dialog-confirm on-click="_removeMember"> [[i18n('label.yes')]] </paper-button> </div> </nuxeo-dialog> <nuxeo-dialog id="editGroupDialog" with-backdrop> <h2>[[i18n('groupManagement.editGroup.heading')]]</h2> <iron-form id="editForm"> <form> <nuxeo-input label="[[i18n('groupManagement.group.label')]]" value="{{_editableGroup.grouplabel}}"> </nuxeo-input> </form> </iron-form> <div class="buttons horizontal end-justified layout"> <div class="flex start-justified"> <paper-button dialog-dismiss>[[i18n('command.cancel')]]</paper-button> </div> <paper-button noink class="primary" on-click="_submitEditForm"> [[i18n('command.save.changes')]] </paper-button> </div> </nuxeo-dialog> <nuxeo-card> <div class="header horizontal layout"> <iron-icon icon="nuxeo:user" class="avatar"></iron-icon> <div class="layout vertical flex"> <h3 class="groupname preserve-white-space">[[group.groupname]]</h3> <h4 class="grouplabel preserve-white-space">[[group.grouplabel]]</h4> <h5 class="counter">[[_countUsers(group.memberUsers)]] + [[_countGroups(group.memberGroups)]]</h5> </div> <dom-if if="[[_canEditGroup(readonly, _currentUser, groupname)]]"> <template> <div class="layout horizontal header-actions"> <paper-button id="deleteGroupButton" noink class="flex-end horizontal-button" on-click="_toggleDeleteGroup" aria-labelledby="deleteGroupButtonLabel" > <iron-icon icon="nuxeo:delete"></iron-icon> <span id="deleteGroupButtonLabel">[[i18n('command.delete')]]</span> </paper-button> <paper-button id="addMembersButton" noink class="flex-end primary horizontal-button" on-click="_toggleEditMembers" aria-labelledby="addMembersButtonLabel" > <iron-icon icon="nuxeo:add"></iron-icon> <span id="addMembersButtonLabel">[[i18n('groupManagement.addMembers')]]</span> </paper-button> <paper-button id="editGroupButton" noink on-click="_toggleEditGroup" class="primary horizontal-button" aria-labelledby="editGroupButtonLabel" > <iron-icon icon="nuxeo:edit"></iron-icon> <span id="editGroupButtonLabel">[[i18n('groupManagement.editGroup')]]</span> </paper-button> </div> </template> </dom-if> </div> </nuxeo-card> <div class="card layout vertical" hidden$="[[!showEditMembers]]"> <nuxeo-user-suggestion id="picker" search-type="USER_GROUP_TYPE" placeholder="[[i18n('groupManagement.addEntity')]]" selected-item="{{selectedMember}}" result-formatter="[[resultFormatter]]" query-results-filter="[[resultsFilter]]" > </nuxeo-user-suggestion> <dom-repeat items="[[activity]]"> <template> <div class="activity-entry"> <nuxeo-tag icon="[[_icon(item)]]"> <span class="preserve-white-space">[[item.displayLabel]]</span> </nuxeo-tag> <span>[[i18n('groupManagement.addedToGroup')]]</span> <span class="remove" on-click="_toggleDeleteDialog">[[i18n('groupManagement.remove')]]</span> </div> </template> </dom-repeat> </div> <!-- users table --> <nuxeo-card icon="nuxeo:user" heading="[[i18n('groupManagement.users.heading')]]"> <div class="filter-wrapper"> <nuxeo-input autofocus value="{{usersFilter}}" type="search" placeholder="[[i18n('groupManagement.filterUsers.placeholder')]]" > <iron-icon icon="nuxeo:search" prefix></iron-icon> </nuxeo-input> </div> <div class="table"> <div class="table-header"> <div class="flex-4">[[i18n('groupManagement.name')]]</div> <div class="flex-4">[[i18n('groupManagement.identifier')]]</div> <div class="table-actions"></div> </div> <div class="table-rows"> <dom-if if="[[!_empty(memberUsers.entries)]]"> <template> <dom-repeat items="[[memberUsers.entries]]"> <template> <div class="table-row"> <div class="flex-4"> <dom-if if="[[_userHasName(item)]]"> <template> <nuxeo-user-tag user="[[item]]"></nuxeo-user-tag> </template> </dom-if> </div> <div class="flex-4 preserve-white-space">[[item.id]]</div> <div class="table-actions"> <dom-if if="[[_canEditGroup(readonly, _currentUser, groupname)]]"> <template> <paper-icon-button icon="nuxeo:clear" noink title="[[i18n('groupManagement.removeFrom', groupname)]]" on-click="_toggleDeleteDialog" > </paper-icon-button> </template> </dom-if> </div> </div> </template> </dom-repeat> </template> </dom-if> <dom-if if="[[_empty(memberUsers.entries)]]"> <template> <div class="table-row"> <div class="emptyResult">[[i18n('groupManagement.noSearchResults')]]</div> </div> </template> </dom-if> </div> </div> <nuxeo-pagination-controls page="{{usersCurrentPage}}" number-of-pages="[[memberUsers.numberOfPages]]"> </nuxeo-pagination-controls> </nuxeo-card> <!-- nested groups --> <nuxeo-card icon="nuxeo:group" heading="[[i18n('groupManagement.nestedGroups.heading')]]"> <div class="filter-wrapper"> <nuxeo-input autofocus value="{{groupsFilter}}" type="search" placeholder="[[i18n('groupManagement.filterGroups.placeholder')]]" > <iron-icon icon="nuxeo:search" prefix></iron-icon> </nuxeo-input> </div> <div class="table"> <div class="table-header"> <div class="flex-4">[[i18n('groupManagement.name')]]</div> <div class="flex-4">[[i18n('groupManagement.identifier')]]</div> <div class="table-actions"></div> </div> <div class="table-rows"> <dom-if if="[[!_empty(memberGroups.entries)]]"> <template> <dom-repeat items="[[memberGroups.entries]]"> <template> <div class="table-row"> <div class="flex-4"> <nuxeo-group-tag group="[[item]]"></nuxeo-group-tag> </div> <div class="flex-4 preserve-white-space">[[item.grouplabel]]</div> <div class="table-actions"> <dom-if if="[[_canEditGroup(readonly, _currentUser, groupname)]]"> <template> <paper-icon-button icon="nuxeo:clear" noink title="[[i18n('groupManagement.removeFrom', groupname)]]" on-click="_toggleDeleteDialog" > </paper-icon-button> </template> </dom-if> </div> </div> </template> </dom-repeat> </template> </dom-if> <dom-if if="[[_empty(memberGroups.entries)]]"> <template> <div class="table-row"> <div>[[i18n('groupManagement.noSearchResults')]]</div> </div> </template> </dom-if> </div> </div> <nuxeo-pagination-controls page="{{groupsCurrentPage}}" number-of-pages="[[memberGroups.numberOfPages]]"> </nuxeo-pagination-controls> </nuxeo-card> <!-- permissions --> <nuxeo-card heading="[[i18n('groupManagement.permissions.heading')]]"> <nuxeo-user-group-permissions-table entity="[[groupname]]"></nuxeo-user-group-permissions-table> </nuxeo-card> `; } static get is() { return 'nuxeo-group-management'; } static get properties() { return { groupname: { type: String, observer: '_fetch', }, group: Object, selectedMember: { type: Object, observer: '_memberSelected', }, memberUsers: Object, memberGroups: Object, activity: { type: Array, value: [], }, resultsFilter: { type: Function, value() { return this._resultsFilter.bind(this); }, }, resultFormatter: { type: Function, }, showEditMembers: { type: Boolean, value: false, }, usersCurrentPage: Number, usersFilter: String, groupsCurrentPage: Number, groupsFilter: String, _editableGroup: Object, _removedMember: Object, _fromDelete: { type: Boolean, value: false, }, readonly: { type: Boolean, value: false, reflectToAttribute: true, }, _currentUser: { type: Object, }, }; } static get observers() { return [ '_fetchUsers(usersCurrentPage)', '_filterUsers(usersFilter)', '_fetchGroups(groupsCurrentPage)', '_filterGroups(groupsFilter)', ]; } /** * Fired when a group is deleted. * * @event nuxeo-group-deleted */ ready() { super.ready(); this.$.editForm.addEventListener('iron-form-presubmit', (event) => { event.preventDefault(); this._saveGroup(); }); } _hasAdministrationPermissions(user) { return ( user && (user.isAdministrator || (this.isMember(user, 'powerusers') && this.groupname !== 'administrators')) ); } _canEditGroup(readonly, currentUser, groupname) { return !readonly && this._hasAdministrationPermissions(currentUser, groupname); } _userHasName(user) { return user.properties.firstName || user.properties.lastName; } _fetch() { if (this.groupname) { this.$.request.get().then(() => { this.activity = []; this.showEditMembers = false; this.selectedMember = null; this._fetchGroups(); this._fetchUsers(); }); } } _saveGroup() { this.$.editRequest.data = this._editableGroup; this.$.editRequest.put().then(() => { this._toast(this.i18n('groupManagement.group.updated')); this.$.editGroupDialog.toggle(); }); } _fetchGroups() { if (this.group) { // if there's only one entry left in the current page and we delete it, we should go to prev page if (this._fromDelete && this.memberGroups.currentPageSize === 1) { this._fromDelete = false; this.groupsCurrentPage--; return; } const params = { q: this.groupsFilter, currentPageIndex: this.groupsCurrentPage - 1, }; this.$.groups.params = params; } } _fetchUsers() { if (this.group) { // if there's only one entry left in the current page and we delete it, we should go to prev page if (this._fromDelete && this.memberUsers.currentPageSize === 1) { this._fromDelete = false; this.usersCurrentPage--; return; } const params = { q: this.usersFilter, currentPageIndex: this.usersCurrentPage - 1, }; this.$.users.params = params; } } _memberSelected() { if (this.selectedMember) { const member = this.selectedMember; switch (member.type) { case 'USER_TYPE': { const users = this.group.memberUsers || []; users.push(member.id); member['entity-type'] = 'user'; this.group.memberUsers = users; break; } case 'GROUP_TYPE': { const groups = this.group.memberGroups || []; groups.push(member.id); member['entity-type'] = 'group'; this.group.memberGroups = groups; break; } default: // do nothing } this.push('activity', member); this.$.editRequest.data = this.group; this.$.editRequest.put().then(() => { if (member['entity-type'] === 'user') { this._fetchUsers(); } else { this._fetchGroups(); } this._toast(this.i18n('groupManagement.addedUserToGroup', member.displayLabel, this.group.groupname)); }); } this.selectedMember = null; } _removeMember() { const member = this._removedMember; let idx; switch (member['entity-type']) { case 'user': if (this.group.memberUsers) { idx = this.group.memberUsers.indexOf(this._removedMember.id); this.group.memberUsers.splice(idx, 1); } break; case 'group': if (this.group.memberGroups) { idx = this.group.memberGroups.indexOf(this._removedMember.id); this.group.memberGroups.splice(idx, 1); } break; default: // do nothing } this.$.editRequest.data = this.group; this.$.editRequest.put().then(() => { this._fromDelete = true; if (member['entity-type'] === 'user') { this._fetchUsers(); } else { this._fetchGroups(); } this._removeRecent(this._removedMember.id); this._toast(this.i18n('groupManagement.removedUserFromGroup', this._removedMember.id)); }); } _removeRecent(group) { // remove from 'recent', if it exists for (let i = 0; i < this.activity.length; i++) { if (this.activity[i].id === group) { this.splice('activity', i, 1); return; } } } _submitEditForm() { this.$.editForm.submit(); } _filterUsers() { if (this.group) { this.usersCurrentPage = 1; this._fetchUsers(); } } _filterGroups() { if (this.group) { this.groupsCurrentPage = 1; this._fetchGroups(); } } _deleteGroup() { const deletedGroup = this.group; this.$.deleteGroupDialog.toggle(); this.$.editRequest.data = deletedGroup; this.$.editRequest.remove().then(() => { this.dispatchEvent( new CustomEvent('nuxeo-group-deleted', { composed: true, bubbles: true, detail: deletedGroup, }), ); this._goHome(); }); } _toggleEditMembers() { this.showEditMembers = !this.showEditMembers; } _toggleDeleteDialog(e) { const type = e.model.item['entity-type']; this._removedMember = e.model.item; this._removedMember.id = type === 'user' ? e.model.item.id : e.model.item.groupname; this.$.rmFromGroupDialog.toggle(); } _toggleEditGroup() { this._clone(); this.$.editGroupDialog.toggle(); } _toggleDeleteGroup() { this.$.deleteGroupDialog.toggle(); } _empty(entries) { return entries && entries.length === 0; } _goHome() { this.dispatchEvent( new CustomEvent('goHome', { composed: true, bubbles: true, }), ); } _resultsFilter(entry) { const userInGroup = this.group.memberUsers && this.group.memberUsers.indexOf(entry.id) >= 0; const groupInGroup = this.group.memberGroups && this.group.memberGroups.indexOf(entry.id) >= 0; return !userInGroup && !groupInGroup && entry.id !== this.group.groupname; } _icon(entry) { return entry.type === 'GROUP_TYPE' ? 'nuxeo:group' : 'nuxeo:user'; } _countUsers(users) { if (users) { const label = ` ${ users.length === 1 ? this.i18n('groupManagement.member') : this.i18n('groupManagement.members') }`; return users.length + label; } } _countGroups(groups) { if (groups) { const label = ` ${ groups.length === 1 ? this.i18n('groupManagement.nestedGroup') : this.i18n('groupManagement.nestedGroups') }`; return groups.length + label; } } _toast(msg) { this.$.toast.text = msg; this.$.toast.open(); } _clone() { this._editableGroup = JSON.parse(JSON.stringify(this.group)); } _usersPath() { if (this.groupname) { return `group/${this.groupname}/@users`; } } _groupsPath() { if (this.groupname) { return `group/${this.groupname}/@groups`; } } } customElements.define(GroupManagement.is, GroupManagement); Nuxeo.GroupManagement = GroupManagement; }