blob: 2865f0dafb640bbcb15282f7b43553404565f8ce [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 '../../../scripts/bundled-polymer.js';
import '@polymer/iron-input/iron-input.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-icons/gr-icons.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-permission/gr-permission.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.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 {htmlTemplate} from './gr-access-section_html.js';
import {AccessBehavior} from '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
/**
* Fired when the section has been modified or removed.
*
* @event access-modified
*/
/**
* Fired when a section that was previously added was removed.
*
* @event added-section-removed
*/
const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
// The name that gets automatically input when a new reference is added.
const NEW_NAME = 'refs/heads/*';
const REFS_NAME = 'refs/';
const ON_BEHALF_OF = '(On Behalf Of)';
const LABEL = 'Label';
/**
* @extends Polymer.Element
*/
class GrAccessSection extends mixinBehaviors( [
AccessBehavior,
], GestureEventListeners(
LegacyElementMixin(
PolymerElement))) {
static get template() { return htmlTemplate; }
static get is() { return 'gr-access-section'; }
static get properties() {
return {
repo: String,
capabilities: Object,
/** @type {?} */
section: {
type: Object,
notify: true,
observer: '_updateSection',
},
groups: Object,
labels: Object,
editing: {
type: Boolean,
value: false,
observer: '_handleEditingChanged',
},
canUpload: Boolean,
ownerOf: Array,
_originalId: String,
_editingRef: {
type: Boolean,
value: false,
},
_deleted: {
type: Boolean,
value: false,
},
_permissions: Array,
};
}
/** @override */
created() {
super.created();
this.addEventListener('access-saved',
() => this._handleAccessSaved());
}
_updateSection(section) {
this._permissions = this.toSortedArray(section.value.permissions);
this._originalId = section.id;
}
_handleAccessSaved() {
// Set a new 'original' value to keep track of after the value has been
// saved.
this._updateSection(this.section);
}
_handleValueChange() {
if (!this.section.value.added) {
this.section.value.modified = this.section.id !== this._originalId;
// Allows overall access page to know a change has been made.
// For a new section, this is not fired because new permissions and
// rules have to be added in order to save, modifying the ref is not
// enough.
this.dispatchEvent(new CustomEvent(
'access-modified', {bubbles: true, composed: true}));
}
this.section.value.updatedId = this.section.id;
}
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld) { return; }
// Restore original values if no longer editing.
if (!editing) {
this._editingRef = false;
this._deleted = false;
delete this.section.value.deleted;
// Restore section ref.
this.set(['section', 'id'], this._originalId);
// Remove any unsaved but added permissions.
this._permissions = this._permissions.filter(p => !p.value.added);
for (const key of Object.keys(this.section.value.permissions)) {
if (this.section.value.permissions[key].added) {
delete this.section.value.permissions[key];
}
}
}
}
_computePermissions(name, capabilities, labels) {
let allPermissions;
if (!this.section || !this.section.value) {
return [];
}
if (name === GLOBAL_NAME) {
allPermissions = this.toSortedArray(capabilities);
} else {
const labelOptions = this._computeLabelOptions(labels);
allPermissions = labelOptions.concat(
this.toSortedArray(this.permissionValues));
}
return allPermissions
.filter(permission => !this.section.value.permissions[permission.id]);
}
_computeHideEditClass(section) {
return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
}
_handleAddedPermissionRemoved(e) {
const index = e.model.index;
this._permissions = this._permissions.slice(0, index).concat(
this._permissions.slice(index + 1, this._permissions.length));
}
_computeLabelOptions(labels) {
const labelOptions = [];
if (!labels) { return []; }
for (const labelName of Object.keys(labels)) {
labelOptions.push({
id: 'label-' + labelName,
value: {
name: `${LABEL} ${labelName}`,
id: 'label-' + labelName,
},
});
labelOptions.push({
id: 'labelAs-' + labelName,
value: {
name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`,
id: 'labelAs-' + labelName,
},
});
}
return labelOptions;
}
_computePermissionName(name, permission, permissionValues, capabilities) {
if (name === GLOBAL_NAME) {
return capabilities[permission.id].name;
} else if (permissionValues[permission.id]) {
return permissionValues[permission.id].name;
} else if (permission.value.label) {
let behalfOf = '';
if (permission.id.startsWith('labelAs-')) {
behalfOf = ON_BEHALF_OF;
}
return `${LABEL} ${permission.value.label}${behalfOf}`;
}
}
_computeSectionName(name) {
// When a new section is created, it doesn't yet have a ref. Set into
// edit mode so that the user can input one.
if (!name) {
this._editingRef = true;
// Needed for the title value. This is the same default as GWT.
name = NEW_NAME;
// Needed for the input field value.
this.set('section.id', name);
}
if (name === GLOBAL_NAME) {
return 'Global Capabilities';
} else if (name.startsWith(REFS_NAME)) {
return `Reference: ${name}`;
}
return name;
}
_handleRemoveReference() {
if (this.section.value.added) {
this.dispatchEvent(new CustomEvent(
'added-section-removed', {bubbles: true, composed: true}));
}
this._deleted = true;
this.section.value.deleted = true;
this.dispatchEvent(
new CustomEvent('access-modified', {bubbles: true, composed: true}));
}
_handleUndoRemove() {
this._deleted = false;
delete this.section.value.deleted;
}
editRefInput() {
return dom(this.root).querySelector(PolymerElement ?
'iron-input.editRefInput' :
'input[is=iron-input].editRefInput');
}
editReference() {
this._editingRef = true;
this.editRefInput().focus();
}
_isEditEnabled(canUpload, ownerOf, sectionId) {
return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0);
}
_computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) {
const classList = [];
if (editing
&& this._isEditEnabled(canUpload, ownerOf, this.section.id)) {
classList.push('editing');
}
if (editingRef) {
classList.push('editingRef');
}
if (deleted) {
classList.push('deleted');
}
return classList.join(' ');
}
_computeEditBtnClass(name) {
return name === GLOBAL_NAME ? 'global' : '';
}
_handleAddPermission() {
const value = this.$.permissionSelect.value;
const permission = {
id: value,
value: {rules: {}, added: true},
};
// This is needed to update the 'label' property of the
// 'label-<label-name>' permission.
//
// The value from the add permission dropdown will either be
// label-<label-name> or labelAs-<labelName>.
// But, the format of the API response is as such:
// "permissions": {
// "label-Code-Review": {
// "label": "Code-Review",
// "rules": {...}
// }
// }
// }
// When we add a new item, we have to push the new permission in the same
// format as the ones that have been returned by the API.
if (value.startsWith('label')) {
permission.value.label =
value.replace('label-', '').replace('labelAs-', '');
}
// Add to the end of the array (used in dom-repeat) and also to the
// section object that is two way bound with its parent element.
this.push('_permissions', permission);
this.set(['section.value.permissions', permission.id],
permission.value);
}
}
customElements.define(GrAccessSection.is, GrAccessSection);