| /** |
| * @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); |