blob: 1aa3e4bb04029146db24df5ceac3e427306a58a4 [file] [log] [blame]
/**
* @license
* Copyright (C) 2017 The Android Open Source Project
*
* 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 '@polymer/paper-toggle-button/paper-toggle-button.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-menu-page-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-autocomplete/gr-autocomplete.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-rule-editor/gr-rule-editor.js';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-permission_html.js';
import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
const MAX_AUTOCOMPLETE_RESULTS = 20;
const RANGE_NAMES = [
'QUERY LIMIT',
'BATCH CHANGES LIMIT',
];
/**
* Fired when the permission has been modified or removed.
*
* @event access-modified
*/
/**
* Fired when a permission that was previously added was removed.
*
* @event added-permission-removed
* @extends PolymerElement
*/
class GrPermission extends mixinBehaviors( [
AccessBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-permission'; }
static get properties() {
return {
labels: Object,
name: String,
/** @type {?} */
permission: {
type: Object,
observer: '_sortPermission',
notify: true,
},
groups: Object,
section: String,
editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
_label: {
type: Object,
computed: '_computeLabel(permission, labels)',
},
_groupFilter: String,
_query: {
type: Function,
value() {
return this._getGroupSuggestions.bind(this);
},
},
_rules: Array,
_groupsWithRules: Object,
_deleted: {
type: Boolean,
value: false,
},
_originalExclusiveValue: Boolean,
};
}
static get observers() {
return [
'_handleRulesChanged(_rules.splices)',
];
}
/** @override */
created() {
super.created();
this.addEventListener('access-saved',
() => this._handleAccessSaved());
}
/** @override */
ready() {
super.ready();
this._setupValues();
}
_setupValues() {
if (!this.permission) { return; }
this._originalExclusiveValue = !!this.permission.value.exclusive;
flush();
}
_handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
this._setupValues();
}
_permissionIsOwnerOrGlobal(permissionId, section) {
return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
}
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld) { return; }
// Restore original values if no longer editing.
if (!editing) {
this._deleted = false;
delete this.permission.value.deleted;
this._groupFilter = '';
this._rules = this._rules.filter(rule => !rule.value.added);
for (const key of Object.keys(this.permission.value.rules)) {
if (this.permission.value.rules[key].added) {
delete this.permission.value.rules[key];
}
}
// Restore exclusive bit to original.
this.set(['permission', 'value', 'exclusive'],
this._originalExclusiveValue);
}
}
_handleAddedRuleRemoved(e) {
const index = e.model.index;
this._rules = this._rules.slice(0, index)
.concat(this._rules.slice(index + 1, this._rules.length));
}
_handleValueChange() {
this.permission.value.modified = true;
// Allows overall access page to know a change has been made.
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleRemovePermission() {
if (this.permission.value.added) {
this.dispatchEvent(new CustomEvent(
'added-permission-removed', {bubbles: true, composed: true}));
}
this._deleted = true;
this.permission.value.deleted = true;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleRulesChanged(changeRecord) {
// Update the groups to exclude in the autocomplete.
this._groupsWithRules = this._computeGroupsWithRules(this._rules);
}
_sortPermission(permission) {
this._rules = this.toSortedArray(permission.value.rules);
}
_computeSectionClass(editing, deleted) {
const classList = [];
if (editing) {
classList.push('editing');
}
if (deleted) {
classList.push('deleted');
}
return classList.join(' ');
}
_handleUndoRemove() {
this._deleted = false;
delete this.permission.value.deleted;
}
_computeLabel(permission, labels) {
if (!labels || !permission ||
!permission.value || !permission.value.label) { return; }
const labelName = permission.value.label;
// It is possible to have a label name that is not included in the
// 'labels' object. In this case, treat it like anything else.
if (!labels[labelName]) { return; }
return {
name: labelName,
values: this._computeLabelValues(labels[labelName].values),
};
}
_computeLabelValues(values) {
const valuesArr = [];
const keys = Object.keys(values)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
for (const key of keys) {
let text = values[key];
if (!text) { text = ''; }
// The value from the server being used to choose which item is
// selected is in integer form, so this must be converted.
valuesArr.push({value: parseInt(key, 10), text});
}
return valuesArr;
}
/**
* @param {!Array} rules
* @return {!Object} Object with groups with rues as keys, and true as
* value.
*/
_computeGroupsWithRules(rules) {
const groups = {};
for (const rule of rules) {
groups[rule.id] = true;
}
return groups;
}
_computeGroupName(groups, groupId) {
return groups && groups[groupId] && groups[groupId].name ?
groups[groupId].name : groupId;
}
_getGroupSuggestions() {
return this.$.restAPI.getSuggestedGroups(
this._groupFilter,
MAX_AUTOCOMPLETE_RESULTS)
.then(response => {
const groups = [];
for (const key in response) {
if (!response.hasOwnProperty(key)) { continue; }
groups.push({
name: key,
value: response[key],
});
}
// Does not return groups in which we already have rules for.
return groups
.filter(group => !this._groupsWithRules[group.value.id]);
});
}
/**
* Handles adding a skeleton item to the dom-repeat.
* gr-rule-editor handles setting the default values.
*/
_handleAddRuleItem(e) {
// The group id is encoded, but have to decode in order for the access
// API to work as expected.
const groupId = decodeURIComponent(e.detail.value.id)
.replace(/\+/g, ' ');
// We cannot use "this.set(...)" here, because groupId may contain dots,
// and dots in property path names are totally unsupported by Polymer.
// Apparently Polymer picks up this change anyway, otherwise we should
// have looked at using MutableData:
// https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
this.permission.value.rules[groupId] = {};
// Purposely don't recompute sorted array so that the newly added rule
// is the last item of the array.
this.push('_rules', {
id: groupId,
});
// Add the new group name to the groups object so the name renders
// correctly.
if (this.groups && !this.groups[groupId]) {
this.groups[groupId] = {name: this.$.groupAutocomplete.text};
}
// Wait for new rule to get value populated via gr-rule-editor, and then
// add to permission values as well, so that the change gets propagated
// back to the section. Since the rule is inside a dom-repeat, a flush
// is needed.
flush();
const value = this._rules[this._rules.length - 1].value;
value.added = true;
// See comment above for why we cannot use "this.set(...)" here.
this.permission.value.rules[groupId] = value;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_computeHasRange(name) {
if (!name) { return false; }
return RANGE_NAMES.includes(name.toUpperCase());
}
/**
* Work around a issue on iOS when clicking turns into double tap
*/
_onTapExclusiveToggle(e) {
e.preventDefault();
}
}
customElements.define(GrPermission.is, GrPermission);