|  | /** | 
|  | * @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'; | 
|  | import '../../../styles/gr-form-styles'; | 
|  | import '../../../styles/gr-menu-page-styles'; | 
|  | import '../../../styles/shared-styles'; | 
|  | import '../../shared/gr-autocomplete/gr-autocomplete'; | 
|  | import '../../shared/gr-button/gr-button'; | 
|  | import '../../shared/gr-rest-api-interface/gr-rest-api-interface'; | 
|  | import '../gr-rule-editor/gr-rule-editor'; | 
|  | import {flush} from '@polymer/polymer/lib/legacy/polymer.dom'; | 
|  | import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners'; | 
|  | import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin'; | 
|  | import {PolymerElement} from '@polymer/polymer/polymer-element'; | 
|  | import {htmlTemplate} from './gr-permission_html'; | 
|  | import { | 
|  | toSortedPermissionsArray, | 
|  | PermissionArrayItem, | 
|  | PermissionArray, | 
|  | } from '../../../utils/access-util'; | 
|  | import {customElement, property, observe} from '@polymer/decorators'; | 
|  | import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api'; | 
|  | import {hasOwnProperty} from '../../../utils/common-util'; | 
|  | import { | 
|  | LabelNameToLabelTypeInfoMap, | 
|  | LabelTypeInfoValues, | 
|  | GroupInfo, | 
|  | ProjectAccessGroups, | 
|  | GroupId, | 
|  | GitRef, | 
|  | } from '../../../types/common'; | 
|  | import { | 
|  | AutocompleteQuery, | 
|  | GrAutocomplete, | 
|  | AutocompleteSuggestion, | 
|  | AutocompleteCommitEvent, | 
|  | } from '../../shared/gr-autocomplete/gr-autocomplete'; | 
|  | import { | 
|  | EditablePermissionInfo, | 
|  | EditablePermissionRuleInfo, | 
|  | EditableProjectAccessGroups, | 
|  | } from '../gr-repo-access/gr-repo-access-interfaces'; | 
|  | import {PolymerDomRepeatEvent} from '../../../types/types'; | 
|  |  | 
|  | const MAX_AUTOCOMPLETE_RESULTS = 20; | 
|  |  | 
|  | const RANGE_NAMES = ['QUERY LIMIT', 'BATCH CHANGES LIMIT']; | 
|  |  | 
|  | type GroupsWithRulesMap = {[ruleId: string]: boolean}; | 
|  |  | 
|  | export interface GrPermission { | 
|  | $: { | 
|  | restAPI: RestApiService & Element; | 
|  | groupAutocomplete: GrAutocomplete; | 
|  | }; | 
|  | } | 
|  |  | 
|  | interface ComputedLabelValue { | 
|  | value: number; | 
|  | text: string; | 
|  | } | 
|  |  | 
|  | interface ComputedLabel { | 
|  | name: string; | 
|  | values: ComputedLabelValue[]; | 
|  | } | 
|  |  | 
|  | interface GroupSuggestion { | 
|  | name: string; | 
|  | value: GroupInfo; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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 | 
|  | */ | 
|  | @customElement('gr-permission') | 
|  | export class GrPermission extends GestureEventListeners( | 
|  | LegacyElementMixin(PolymerElement) | 
|  | ) { | 
|  | static get template() { | 
|  | return htmlTemplate; | 
|  | } | 
|  |  | 
|  | @property({type: Object}) | 
|  | labels?: LabelNameToLabelTypeInfoMap; | 
|  |  | 
|  | @property({type: String}) | 
|  | name?: string; | 
|  |  | 
|  | @property({type: Object, observer: '_sortPermission', notify: true}) | 
|  | permission?: PermissionArrayItem<EditablePermissionInfo>; | 
|  |  | 
|  | @property({type: Object}) | 
|  | groups?: EditableProjectAccessGroups; | 
|  |  | 
|  | @property({type: String}) | 
|  | section?: GitRef; | 
|  |  | 
|  | @property({type: Boolean, observer: '_handleEditingChanged'}) | 
|  | editing = false; | 
|  |  | 
|  | @property({type: Object, computed: '_computeLabel(permission, labels)'}) | 
|  | _label?: ComputedLabel; | 
|  |  | 
|  | @property({type: String}) | 
|  | _groupFilter?: string; | 
|  |  | 
|  | @property({type: Object}) | 
|  | _query: AutocompleteQuery; | 
|  |  | 
|  | @property({type: Array}) | 
|  | _rules?: PermissionArray<EditablePermissionRuleInfo>; | 
|  |  | 
|  | @property({type: Object}) | 
|  | _groupsWithRules?: GroupsWithRulesMap; | 
|  |  | 
|  | @property({type: Boolean}) | 
|  | _deleted = false; | 
|  |  | 
|  | @property({type: Boolean}) | 
|  | _originalExclusiveValue?: boolean; | 
|  |  | 
|  | constructor() { | 
|  | super(); | 
|  | this._query = () => this._getGroupSuggestions(); | 
|  | } | 
|  |  | 
|  | /** @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: string, section: string) { | 
|  | return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES'; | 
|  | } | 
|  |  | 
|  | _handleEditingChanged(editing: boolean, editingOld: boolean) { | 
|  | // Ignore when editing gets set initially. | 
|  | if (!editingOld) { | 
|  | return; | 
|  | } | 
|  | if (!this.permission || !this._rules) { | 
|  | 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: PolymerDomRepeatEvent) { | 
|  | if (!this._rules) { | 
|  | return; | 
|  | } | 
|  | const index = e.model.index; | 
|  | this._rules = this._rules | 
|  | .slice(0, index) | 
|  | .concat(this._rules.slice(index + 1, this._rules.length)); | 
|  | } | 
|  |  | 
|  | _handleValueChange() { | 
|  | if (!this.permission) { | 
|  | return; | 
|  | } | 
|  | 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) { | 
|  | return; | 
|  | } | 
|  | 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}) | 
|  | ); | 
|  | } | 
|  |  | 
|  | @observe('_rules.splices') | 
|  | _handleRulesChanged() { | 
|  | if (!this._rules) { | 
|  | return; | 
|  | } | 
|  | // Update the groups to exclude in the autocomplete. | 
|  | this._groupsWithRules = this._computeGroupsWithRules(this._rules); | 
|  | } | 
|  |  | 
|  | _sortPermission(permission: PermissionArrayItem<EditablePermissionInfo>) { | 
|  | this._rules = toSortedPermissionsArray(permission.value.rules); | 
|  | } | 
|  |  | 
|  | _computeSectionClass(editing: boolean, deleted: boolean) { | 
|  | const classList = []; | 
|  | if (editing) { | 
|  | classList.push('editing'); | 
|  | } | 
|  | if (deleted) { | 
|  | classList.push('deleted'); | 
|  | } | 
|  | return classList.join(' '); | 
|  | } | 
|  |  | 
|  | _handleUndoRemove() { | 
|  | if (!this.permission) { | 
|  | return; | 
|  | } | 
|  | this._deleted = false; | 
|  | delete this.permission.value.deleted; | 
|  | } | 
|  |  | 
|  | _computeLabel( | 
|  | permission?: PermissionArrayItem<EditablePermissionInfo>, | 
|  | labels?: LabelNameToLabelTypeInfoMap | 
|  | ): ComputedLabel | undefined { | 
|  | 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: LabelTypeInfoValues): ComputedLabelValue[] { | 
|  | const valuesArr: ComputedLabelValue[] = []; | 
|  | const keys = Object.keys(values).sort((a, b) => Number(a) - Number(b)); | 
|  |  | 
|  | 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: Number(key), text}); | 
|  | } | 
|  | return valuesArr; | 
|  | } | 
|  |  | 
|  | _computeGroupsWithRules( | 
|  | rules: PermissionArray<EditablePermissionRuleInfo> | 
|  | ): GroupsWithRulesMap { | 
|  | const groups: GroupsWithRulesMap = {}; | 
|  | for (const rule of rules) { | 
|  | groups[rule.id] = true; | 
|  | } | 
|  | return groups; | 
|  | } | 
|  |  | 
|  | _computeGroupName(groups: ProjectAccessGroups, groupId: GroupId) { | 
|  | return groups && groups[groupId] && groups[groupId].name | 
|  | ? groups[groupId].name | 
|  | : groupId; | 
|  | } | 
|  |  | 
|  | _getGroupSuggestions(): Promise<AutocompleteSuggestion[]> { | 
|  | return this.$.restAPI | 
|  | .getSuggestedGroups(this._groupFilter || '', MAX_AUTOCOMPLETE_RESULTS) | 
|  | .then(response => { | 
|  | const groups: GroupSuggestion[] = []; | 
|  | for (const key in response) { | 
|  | if (!hasOwnProperty(response, 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 && !this._groupsWithRules[group.value.id] | 
|  | ) | 
|  | .map((group: GroupSuggestion) => { | 
|  | const autocompleteSuggestion: AutocompleteSuggestion = { | 
|  | name: group.name, | 
|  | value: group.value.id, | 
|  | }; | 
|  | return autocompleteSuggestion; | 
|  | }); | 
|  | }); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Handles adding a skeleton item to the dom-repeat. | 
|  | * gr-rule-editor handles setting the default values. | 
|  | */ | 
|  | _handleAddRuleItem(e: AutocompleteCommitEvent) { | 
|  | if (!this.permission || !this._rules) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // 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).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 | 
|  | // Actual value assigned below, after the flush | 
|  | this.permission.value.rules[groupId] = {} as EditablePermissionRuleInfo; | 
|  |  | 
|  | // 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: string) { | 
|  | if (!name) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | return RANGE_NAMES.includes(name.toUpperCase()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Work around a issue on iOS when clicking turns into double tap | 
|  | */ | 
|  | _onTapExclusiveToggle(e: Event) { | 
|  | e.preventDefault(); | 
|  | } | 
|  | } | 
|  |  | 
|  | declare global { | 
|  | interface HTMLElementTagNameMap { | 
|  | 'gr-permission': GrPermission; | 
|  | } | 
|  | } |