blob: 0a862ebd7ee94373f27e49e02903bbda8bae7ba6 [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 '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
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;
}
}