| /** |
| * @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/iron-autogrow-textarea/iron-autogrow-textarea'; |
| import '../../../styles/gr-font-styles'; |
| import '../../../styles/gr-form-styles'; |
| import '../../../styles/gr-subpage-styles'; |
| import '../../../styles/gr-table-styles'; |
| import '../../../styles/shared-styles'; |
| import '../../shared/gr-account-link/gr-account-link'; |
| import '../../shared/gr-autocomplete/gr-autocomplete'; |
| import '../../shared/gr-button/gr-button'; |
| import '../../shared/gr-overlay/gr-overlay'; |
| import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog'; |
| import {PolymerElement} from '@polymer/polymer/polymer-element'; |
| import {htmlTemplate} from './gr-group-members_html'; |
| import {getBaseUrl} from '../../../utils/url-util'; |
| import {customElement, property} from '@polymer/decorators'; |
| import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; |
| import { |
| GroupId, |
| AccountId, |
| AccountInfo, |
| GroupInfo, |
| GroupName, |
| } from '../../../types/common'; |
| import { |
| AutocompleteQuery, |
| AutocompleteSuggestion, |
| } from '../../shared/gr-autocomplete/gr-autocomplete'; |
| import {PolymerDomRepeatEvent} from '../../../types/types'; |
| import { |
| fireAlert, |
| firePageError, |
| fireTitleChange, |
| } from '../../../utils/event-util'; |
| import {appContext} from '../../../services/app-context'; |
| import {ErrorCallback} from '../../../api/rest'; |
| import {assertNever} from '../../../utils/common-util'; |
| |
| const SUGGESTIONS_LIMIT = 15; |
| const SAVING_ERROR_TEXT = |
| 'Group may not exist, or you may not have ' + 'permission to add it'; |
| |
| const URL_REGEX = '^(?:[a-z]+:)?//'; |
| |
| export enum ItemType { |
| MEMBER = 'member', |
| INCLUDED_GROUP = 'includedGroup', |
| } |
| |
| export interface GrGroupMembers { |
| $: { |
| overlay: GrOverlay; |
| }; |
| } |
| @customElement('gr-group-members') |
| export class GrGroupMembers extends PolymerElement { |
| static get template() { |
| return htmlTemplate; |
| } |
| |
| @property({type: Number}) |
| groupId?: GroupId; |
| |
| @property({type: Number}) |
| _groupMemberSearchId?: number; |
| |
| @property({type: String}) |
| _groupMemberSearchName?: string; |
| |
| @property({type: String}) |
| _includedGroupSearchId?: string; |
| |
| @property({type: String}) |
| _includedGroupSearchName?: string; |
| |
| @property({type: Boolean}) |
| _loading = true; |
| |
| @property({type: String}) |
| _groupName?: GroupName; |
| |
| @property({type: Object}) |
| _groupMembers?: AccountInfo[]; |
| |
| @property({type: Object}) |
| _includedGroups?: GroupInfo[]; |
| |
| @property({type: String}) |
| _itemName?: string; |
| |
| @property({type: String}) |
| _itemType?: ItemType; |
| |
| @property({type: Object}) |
| _queryMembers: AutocompleteQuery; |
| |
| @property({type: Object}) |
| _queryIncludedGroup: AutocompleteQuery; |
| |
| @property({type: Boolean}) |
| _groupOwner = false; |
| |
| @property({type: Boolean}) |
| _isAdmin = false; |
| |
| _itemId?: AccountId | GroupId; |
| |
| private readonly restApiService = appContext.restApiService; |
| |
| constructor() { |
| super(); |
| this._queryMembers = input => this._getAccountSuggestions(input); |
| this._queryIncludedGroup = input => this._getGroupSuggestions(input); |
| } |
| |
| override connectedCallback() { |
| super.connectedCallback(); |
| this._loadGroupDetails(); |
| |
| fireTitleChange(this, 'Members'); |
| } |
| |
| _loadGroupDetails() { |
| if (!this.groupId) { |
| return; |
| } |
| |
| const promises: Promise<void>[] = []; |
| |
| const errFn: ErrorCallback = response => { |
| firePageError(response); |
| }; |
| |
| return this.restApiService |
| .getGroupConfig(this.groupId, errFn) |
| .then(config => { |
| if (!config || !config.name) { |
| return Promise.resolve(); |
| } |
| |
| this._groupName = config.name; |
| |
| promises.push( |
| this.restApiService.getIsAdmin().then(isAdmin => { |
| this._isAdmin = !!isAdmin; |
| }) |
| ); |
| |
| promises.push( |
| this.restApiService.getIsGroupOwner(this._groupName).then(isOwner => { |
| this._groupOwner = !!isOwner; |
| }) |
| ); |
| |
| promises.push( |
| this.restApiService.getGroupMembers(this._groupName).then(members => { |
| this._groupMembers = members; |
| }) |
| ); |
| |
| promises.push( |
| this.restApiService |
| .getIncludedGroup(this._groupName) |
| .then(includedGroup => { |
| this._includedGroups = includedGroup; |
| }) |
| ); |
| |
| return Promise.all(promises).then(() => { |
| this._loading = false; |
| }); |
| }); |
| } |
| |
| _computeLoadingClass(loading: boolean) { |
| return loading ? 'loading' : ''; |
| } |
| |
| _isLoading() { |
| return this._loading || this._loading === undefined; |
| } |
| |
| _computeGroupUrl(url: string) { |
| if (!url) { |
| return; |
| } |
| |
| const r = new RegExp(URL_REGEX, 'i'); |
| if (r.test(url)) { |
| return url; |
| } |
| |
| // For GWT compatibility |
| if (url.startsWith('#')) { |
| return getBaseUrl() + url.slice(1); |
| } |
| return getBaseUrl() + url; |
| } |
| |
| _handleSavingGroupMember() { |
| if (!this._groupName) { |
| return Promise.reject(new Error('group name undefined')); |
| } |
| return this.restApiService |
| .saveGroupMember(this._groupName, this._groupMemberSearchId as AccountId) |
| .then(config => { |
| if (!config || !this._groupName) { |
| return; |
| } |
| this.restApiService.getGroupMembers(this._groupName).then(members => { |
| this._groupMembers = members; |
| }); |
| this._groupMemberSearchName = ''; |
| this._groupMemberSearchId = undefined; |
| }); |
| } |
| |
| _handleDeleteConfirm() { |
| if (!this._groupName) { |
| return Promise.reject(new Error('group name undefined')); |
| } |
| this.$.overlay.close(); |
| if (this._itemType === ItemType.MEMBER) { |
| return this.restApiService |
| .deleteGroupMember(this._groupName, this._itemId! as AccountId) |
| .then(itemDeleted => { |
| if (itemDeleted.status === 204 && this._groupName) { |
| this.restApiService |
| .getGroupMembers(this._groupName) |
| .then(members => { |
| this._groupMembers = members; |
| }); |
| } |
| }); |
| } else if (this._itemType === ItemType.INCLUDED_GROUP) { |
| return this.restApiService |
| .deleteIncludedGroup(this._groupName, this._itemId! as GroupId) |
| .then(itemDeleted => { |
| if ( |
| (itemDeleted.status === 204 || itemDeleted.status === 205) && |
| this._groupName |
| ) { |
| this.restApiService |
| .getIncludedGroup(this._groupName) |
| .then(includedGroup => { |
| this._includedGroups = includedGroup; |
| }); |
| } |
| }); |
| } |
| return Promise.reject(new Error('Unrecognized item type')); |
| } |
| |
| _computeItemTypeName(itemType?: ItemType): string { |
| if (itemType === undefined) return ''; |
| switch (itemType) { |
| case ItemType.INCLUDED_GROUP: |
| return 'Included Group'; |
| case ItemType.MEMBER: |
| return 'Member'; |
| default: |
| assertNever(itemType, 'unknown item type: ${itemType}'); |
| } |
| } |
| |
| _handleConfirmDialogCancel() { |
| this.$.overlay.close(); |
| } |
| |
| _handleDeleteMember(e: PolymerDomRepeatEvent<AccountInfo>) { |
| const id = e.model.get('item._account_id'); |
| const name = e.model.get('item.name'); |
| const username = e.model.get('item.username'); |
| const email = e.model.get('item.email'); |
| const item = username || name || email || id?.toString(); |
| if (!item) { |
| return; |
| } |
| this._itemName = item; |
| this._itemId = id; |
| this._itemType = ItemType.MEMBER; |
| this.$.overlay.open(); |
| } |
| |
| _handleSavingIncludedGroups() { |
| if (!this._groupName || !this._includedGroupSearchId) { |
| return Promise.reject( |
| new Error('group name or includedGroupSearchId undefined') |
| ); |
| } |
| return this.restApiService |
| .saveIncludedGroup( |
| this._groupName, |
| this._includedGroupSearchId.replace(/\+/g, ' ') as GroupId, |
| (errResponse, err) => { |
| if (errResponse) { |
| if (errResponse.status === 404) { |
| fireAlert(this, SAVING_ERROR_TEXT); |
| return errResponse; |
| } |
| throw Error(errResponse.statusText); |
| } |
| throw err; |
| } |
| ) |
| .then(config => { |
| if (!config || !this._groupName) { |
| return; |
| } |
| this.restApiService |
| .getIncludedGroup(this._groupName) |
| .then(includedGroup => { |
| this._includedGroups = includedGroup; |
| }); |
| this._includedGroupSearchName = ''; |
| this._includedGroupSearchId = ''; |
| }); |
| } |
| |
| _handleDeleteIncludedGroup(e: PolymerDomRepeatEvent<GroupInfo>) { |
| const id = decodeURIComponent(`${e.model.get('item.id')}`).replace( |
| /\+/g, |
| ' ' |
| ) as GroupId; |
| const name = e.model.get('item.name'); |
| const item = name || id; |
| if (!item) { |
| return; |
| } |
| this._itemName = item; |
| this._itemId = id; |
| this._itemType = ItemType.INCLUDED_GROUP; |
| this.$.overlay.open(); |
| } |
| |
| _getAccountSuggestions(input: string) { |
| if (input.length === 0) { |
| return Promise.resolve([]); |
| } |
| return this.restApiService |
| .getSuggestedAccounts(input, SUGGESTIONS_LIMIT) |
| .then(accounts => { |
| if (!accounts) return []; |
| const accountSuggestions = []; |
| for (const account of accounts) { |
| let nameAndEmail; |
| if (account.email !== undefined) { |
| nameAndEmail = `${account.name} <${account.email}>`; |
| } else { |
| nameAndEmail = account.name; |
| } |
| accountSuggestions.push({ |
| name: nameAndEmail, |
| value: account._account_id?.toString(), |
| }); |
| } |
| return accountSuggestions; |
| }); |
| } |
| |
| _getGroupSuggestions(input: string) { |
| return this.restApiService.getSuggestedGroups(input).then(response => { |
| const groups: AutocompleteSuggestion[] = []; |
| for (const [name, group] of Object.entries(response ?? {})) { |
| groups.push({name, value: decodeURIComponent(group.id)}); |
| } |
| return groups; |
| }); |
| } |
| |
| _computeHideItemClass(owner: boolean, admin: boolean) { |
| return admin || owner ? '' : 'canModify'; |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-group-members': GrGroupMembers; |
| } |
| } |