| /** |
| * @license |
| * Copyright 2017 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '@polymer/iron-input/iron-input'; |
| import '../../shared/gr-button/gr-button'; |
| import '../../shared/gr-icon/gr-icon'; |
| import '../gr-permission/gr-permission'; |
| import { |
| AccessPermissions, |
| PermissionArray, |
| PermissionArrayItem, |
| toSortedPermissionsArray, |
| } from '../../../utils/access-util'; |
| import { |
| EditablePermissionInfo, |
| PermissionAccessSection, |
| EditableRepoAccessGroups, |
| } from '../gr-repo-access/gr-repo-access-interfaces'; |
| import { |
| CapabilityInfoMap, |
| GitRef, |
| LabelNameToLabelTypeInfoMap, |
| RepoName, |
| } from '../../../types/common'; |
| import {fire} from '../../../utils/event-util'; |
| import {IronInputElement} from '@polymer/iron-input/iron-input'; |
| import {fontStyles} from '../../../styles/gr-font-styles'; |
| import {formStyles} from '../../../styles/gr-form-styles'; |
| import {sharedStyles} from '../../../styles/shared-styles'; |
| import {LitElement, PropertyValues, html, css} from 'lit'; |
| import {customElement, property, query, state} from 'lit/decorators.js'; |
| import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events'; |
| import {assertIsDefined, queryAndAssert} from '../../../utils/common-util'; |
| |
| 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/'; |
| |
| @customElement('gr-access-section') |
| export class GrAccessSection extends LitElement { |
| @query('#permissionSelect') private permissionSelect?: HTMLSelectElement; |
| |
| @property({type: String}) |
| repo?: RepoName; |
| |
| @property({type: Object}) |
| capabilities?: CapabilityInfoMap; |
| |
| @property({type: Object}) |
| section?: PermissionAccessSection; |
| |
| @property({type: Object}) |
| groups?: EditableRepoAccessGroups; |
| |
| @property({type: Object}) |
| labels?: LabelNameToLabelTypeInfoMap; |
| |
| @property({type: Boolean}) |
| editing = false; |
| |
| @property({type: Boolean}) |
| canUpload?: boolean; |
| |
| @property({type: Array}) |
| ownerOf?: GitRef[]; |
| |
| // private but used in test |
| @state() originalId?: GitRef; |
| |
| // private but used in test |
| @state() editingRef = false; |
| |
| // private but used in test |
| @state() deleted = false; |
| |
| // private but used in test |
| @state() permissions?: PermissionArray<EditablePermissionInfo>; |
| |
| constructor() { |
| super(); |
| this.addEventListener('access-saved', () => this.handleAccessSaved()); |
| } |
| |
| static override get styles() { |
| return [ |
| formStyles, |
| fontStyles, |
| sharedStyles, |
| css` |
| :host { |
| display: block; |
| margin-bottom: var(--spacing-l); |
| } |
| fieldset { |
| border: 1px solid var(--border-color); |
| } |
| .name { |
| align-items: center; |
| display: flex; |
| } |
| .header, |
| #deletedContainer { |
| align-items: center; |
| background: var(--table-header-background-color); |
| border-bottom: 1px dotted var(--border-color); |
| display: flex; |
| justify-content: space-between; |
| min-height: 3em; |
| padding: 0 var(--spacing-m); |
| } |
| #deletedContainer { |
| border-bottom: 0; |
| } |
| .sectionContent { |
| padding: var(--spacing-m); |
| } |
| #editBtn, |
| .editing #editBtn.global, |
| #deletedContainer, |
| .deleted #mainContainer, |
| #addPermission, |
| #deleteBtn, |
| .editingRef .name, |
| .editRefInput { |
| display: none; |
| } |
| .editing #editBtn, |
| .editingRef .editRefInput { |
| display: flex; |
| } |
| .deleted #deletedContainer { |
| display: flex; |
| } |
| .editing #addPermission, |
| #mainContainer, |
| .editing #deleteBtn { |
| display: block; |
| } |
| .editing #deleteBtn, |
| #undoRemoveBtn { |
| padding-right: var(--spacing-m); |
| } |
| `, |
| ]; |
| } |
| |
| override render() { |
| if (!this.section) return; |
| return html` |
| <fieldset |
| id="section" |
| class="gr-form-styles ${this.computeSectionClass()}" |
| > |
| <div id="mainContainer"> |
| <div class="header"> |
| <div class="name"> |
| <h3 class="heading-3">${this.computeSectionName()}</h3> |
| <gr-button |
| id="editBtn" |
| link |
| class=${this.section?.id === GLOBAL_NAME ? 'global' : ''} |
| @click=${this.editReference} |
| > |
| <gr-icon id="icon" icon="edit" filled small></gr-icon> |
| </gr-button> |
| </div> |
| <iron-input |
| class="editRefInput" |
| .bindValue=${this.section?.id} |
| @input=${this.handleValueChange} |
| @bind-value-changed=${this.handleIdBindValueChanged} |
| > |
| <input |
| class="editRefInput" |
| type="text" |
| @input=${this.handleValueChange} |
| /> |
| </iron-input> |
| <gr-button link id="deleteBtn" @click=${this.handleRemoveReference} |
| >Remove</gr-button |
| > |
| </div> |
| <!-- end header --> |
| <div class="sectionContent"> |
| ${this.permissions?.map((permission, index) => |
| this.renderPermission(permission, index) |
| )} |
| <div id="addPermission"> |
| Add permission: |
| <select id="permissionSelect"> |
| ${this.computePermissions().map(item => |
| this.renderPermissionOptions(item) |
| )} |
| </select> |
| <gr-button link id="addBtn" @click=${this.handleAddPermission} |
| >Add</gr-button |
| > |
| </div> |
| <!-- end addPermission --> |
| </div> |
| <!-- end sectionContent --> |
| </div> |
| <!-- end mainContainer --> |
| <div id="deletedContainer"> |
| <span>${this.computeSectionName()} was deleted</span> |
| <gr-button link="" id="undoRemoveBtn" @click=${this._handleUndoRemove} |
| >Undo</gr-button |
| > |
| </div> |
| <!-- end deletedContainer --> |
| </fieldset> |
| `; |
| } |
| |
| private renderPermission( |
| permission: PermissionArrayItem<EditablePermissionInfo>, |
| index: number |
| ) { |
| return html` |
| <gr-permission |
| .name=${this.computePermissionName(permission)} |
| .permission=${permission} |
| .labels=${this.labels} |
| .section=${this.section?.id} |
| .editing=${this.editing} |
| .groups=${this.groups} |
| .repo=${this.repo} |
| @added-permission-removed=${() => { |
| this.handleAddedPermissionRemoved(index); |
| }} |
| @permission-changed=${( |
| e: ValueChangedEvent<PermissionArrayItem<EditablePermissionInfo>> |
| ) => { |
| this.handlePermissionChanged(e, index); |
| }} |
| > |
| </gr-permission> |
| `; |
| } |
| |
| private renderPermissionOptions(item: { |
| id: string; |
| value: {name: string; id: string}; |
| }) { |
| return html`<option value=${item.value.id}>${item.value.name}</option>`; |
| } |
| |
| override willUpdate(changedProperties: PropertyValues) { |
| if (changedProperties.has('section')) { |
| this.updateSection(); |
| } |
| if (changedProperties.has('editing')) { |
| this.handleEditingChanged(changedProperties.get('editing') as boolean); |
| } |
| } |
| |
| // private but used in test |
| updateSection() { |
| this.permissions = toSortedPermissionsArray( |
| this.section!.value.permissions |
| ); |
| this.originalId = this.section!.id; |
| } |
| |
| // private but used in test |
| handleAccessSaved() { |
| if (!this.section) return; |
| // Set a new 'original' value to keep track of after the value has been |
| // saved. |
| this.updateSection(); |
| } |
| |
| // private but used in test |
| handleValueChange() { |
| if (!this.section) { |
| return; |
| } |
| if (!this.section.value.added) { |
| this.section.value.modified = this.section.id !== this.originalId; |
| this.requestUpdate(); |
| // 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. |
| fire(this, 'access-modified', {}); |
| } |
| this.section.value.updatedId = this.section.id; |
| this.requestUpdate(); |
| } |
| |
| private handleEditingChanged(editingOld: boolean) { |
| // Ignore when editing gets set initially. |
| if (!editingOld) { |
| return; |
| } |
| if (!this.section || !this.permissions) { |
| return; |
| } |
| // Restore original values if no longer editing. |
| if (!this.editing) { |
| this.editingRef = false; |
| this.deleted = false; |
| delete this.section.value.deleted; |
| // Restore section ref. |
| this.section.id = this.originalId as GitRef; |
| this.requestUpdate(); |
| fire(this, 'section-changed', {value: this.section}); |
| // 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]; |
| } |
| } |
| } |
| } |
| |
| // private but used in test |
| computePermissions() { |
| let allPermissions; |
| const section = this.section; |
| if (!section || !section.value) { |
| return []; |
| } |
| if (section.id === GLOBAL_NAME) { |
| allPermissions = toSortedPermissionsArray(this.capabilities); |
| } else { |
| const labelOptions = this.computeLabelOptions(); |
| allPermissions = labelOptions.concat( |
| toSortedPermissionsArray(AccessPermissions) |
| ); |
| } |
| return allPermissions.filter( |
| permission => !section.value.permissions[permission.id] |
| ); |
| } |
| |
| private handleAddedPermissionRemoved(index: number) { |
| if (!this.permissions) { |
| return; |
| } |
| delete this.section?.value.permissions[this.permissions[index].id]; |
| this.permissions = this.permissions |
| .slice(0, index) |
| .concat(this.permissions.slice(index + 1, this.permissions.length)); |
| } |
| |
| computeLabelOptions() { |
| const labelOptions = []; |
| if (!this.labels) { |
| return []; |
| } |
| for (const labelName of Object.keys(this.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; |
| } |
| |
| // private but used in test |
| computePermissionName( |
| permission: PermissionArrayItem<EditablePermissionInfo> |
| ): string | undefined { |
| if (this.section?.id === GLOBAL_NAME) { |
| return this.capabilities?.[permission.id]?.name; |
| } else if (AccessPermissions[permission.id]) { |
| return AccessPermissions[permission.id]?.name; |
| } else if (permission.value.label) { |
| if (permission.id.startsWith('labelAs-')) { |
| return `Label ${permission.value.label} (On Behalf Of)`; |
| } else if (permission.id.startsWith('removeLabel-')) { |
| return `Remove Label ${permission.value.label}`; |
| } else { |
| return `Label ${permission.value.label}`; |
| } |
| } |
| return undefined; |
| } |
| |
| // private but used in test |
| computeSectionName() { |
| let name = this.section?.id; |
| // 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 as GitRef; |
| // Needed for the input field value. |
| this.section!.id = name; |
| fire(this, 'section-changed', {value: this.section!}); |
| this.requestUpdate(); |
| } |
| if (name === GLOBAL_NAME) { |
| return 'Global Capabilities'; |
| } else if (name.startsWith(REFS_NAME)) { |
| return `Reference: ${name}`; |
| } |
| return name; |
| } |
| |
| private handleRemoveReference() { |
| if (!this.section) { |
| return; |
| } |
| if (this.section.value.added) { |
| fire(this, 'added-section-removed', {}); |
| } |
| this.deleted = true; |
| this.section.value.deleted = true; |
| fire(this, 'access-modified', {}); |
| } |
| |
| _handleUndoRemove() { |
| if (!this.section) { |
| return; |
| } |
| this.deleted = false; |
| delete this.section.value.deleted; |
| this.requestUpdate(); |
| } |
| |
| editRefInput() { |
| return queryAndAssert<IronInputElement>(this, 'iron-input.editRefInput'); |
| } |
| |
| editReference() { |
| this.editingRef = true; |
| this.editRefInput().focus(); |
| } |
| |
| private isEditEnabled() { |
| return ( |
| this.canUpload || |
| (this.ownerOf && this.ownerOf.indexOf(this.section!.id) >= 0) |
| ); |
| } |
| |
| // private but used in test |
| computeSectionClass() { |
| const classList = []; |
| if (this.editing && this.section && this.isEditEnabled()) { |
| classList.push('editing'); |
| } |
| if (this.editingRef) { |
| classList.push('editingRef'); |
| } |
| if (this.deleted) { |
| classList.push('deleted'); |
| } |
| return classList.join(' '); |
| } |
| |
| // private but used in test |
| handleAddPermission() { |
| assertIsDefined(this.permissionSelect, 'permissionSelect'); |
| const value = this.permissionSelect.value as GitRef; |
| const permission: PermissionArrayItem<EditablePermissionInfo> = { |
| 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.permissions!.push(permission); |
| this.section!.value.permissions[permission.id] = permission.value; |
| this.requestUpdate(); |
| fire(this, 'section-changed', {value: this.section!}); |
| } |
| |
| private handleIdBindValueChanged = (e: BindValueChangeEvent) => { |
| this.section!.id = e.detail.value as GitRef; |
| this.requestUpdate(); |
| fire(this, 'section-changed', {value: this.section!}); |
| }; |
| |
| private handlePermissionChanged = ( |
| e: ValueChangedEvent<PermissionArrayItem<EditablePermissionInfo>>, |
| index: number |
| ) => { |
| this.permissions![index] = e.detail.value; |
| this.requestUpdate(); |
| }; |
| } |
| |
| declare global { |
| interface HTMLElementEventMap { |
| /** Fired when the section has been modified or removed. */ |
| 'access-modified': CustomEvent<{}>; |
| /** Fired when a section that was previously added was removed. */ |
| 'added-section-removed': CustomEvent<{}>; |
| 'section-changed': ValueChangedEvent<PermissionAccessSection>; |
| } |
| interface HTMLElementTagNameMap { |
| 'gr-access-section': GrAccessSection; |
| } |
| } |