blob: 223a7007108bcc95fb3b948afff3ec27bfc2dc03 [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import '../../shared/gr-select/gr-select';
import '../../shared/gr-textarea/gr-textarea';
import {
AutocompleteSuggestion,
AutocompleteQuery,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GroupId, GroupInfo, GroupName} from '../../../types/common';
import {fire, firePageError, fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
import {convertToString} from '../../../utils/string-util';
import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {fontStyles} from '../../../styles/gr-font-styles';
import {formStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {subpageStyles} from '../../../styles/gr-subpage-styles';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
const OPTIONS = {
submitFalse: {
value: false,
label: 'False',
},
submitTrue: {
value: true,
label: 'True',
},
};
export interface GroupNameChangedDetail {
name: GroupName;
external: boolean;
}
declare global {
interface HTMLElementTagNameMap {
'gr-group': GrGroup;
}
interface HTMLElementEventMap {
'name-changed': CustomEvent<GroupNameChangedDetail>;
}
}
@customElement('gr-group')
export class GrGroup extends LitElement {
private readonly query: AutocompleteQuery;
@property({type: String})
groupId?: GroupId;
@state() private originalOwnerName?: string;
@state() private originalDescriptionName?: string;
@state() private originalOptionsVisibleToAll?: boolean;
@state() private submitTypes = Object.values(OPTIONS);
// private but used in test
@state() isAdmin = false;
// private but used in test
@state() groupOwner = false;
// private but used in test
@state() groupIsInternal = false;
// private but used in test
@state() loading = true;
// private but used in test
@state() groupConfig?: GroupInfo;
// private but used in test
@state() groupConfigOwner?: string;
// private but used in test
@state() originalName?: GroupName;
private readonly restApiService = getAppContext().restApiService;
constructor() {
super();
this.query = (input: string) => this.getGroupSuggestions(input);
}
override connectedCallback() {
super.connectedCallback();
}
static override get styles() {
return [
fontStyles,
formStyles,
sharedStyles,
subpageStyles,
css`
h3.edited:after {
color: var(--deemphasized-text-color);
content: ' *';
}
`,
];
}
override render() {
return html`
<div class="main gr-form-styles read-only">
<div id="loading" class=${this.computeLoadingClass()}>Loading...</div>
<div id="loadedContent" class=${this.computeLoadingClass()}>
<h1 id="Title" class="heading-1">${this.originalName}</h1>
<h2 id="configurations" class="heading-2">General</h2>
<div id="form">
<fieldset>
${this.renderGroupUUID()} ${this.renderGroupName()}
${this.renderGroupOwner()} ${this.renderGroupDescription()}
${this.renderGroupOptions()}
</fieldset>
</div>
</div>
</div>
`;
}
private renderGroupUUID() {
return html`
<h3 id="groupUUID" class="heading-3">Group UUID</h3>
<fieldset>
<gr-copy-clipboard
id="uuid"
.text=${this.getGroupUUID()}
></gr-copy-clipboard>
</fieldset>
`;
}
private renderGroupName() {
const groupNameEdited = this.originalName !== this.groupConfig?.name;
return html`
<h3
id="groupName"
class="heading-3 ${this.computeHeaderClass(groupNameEdited)}"
>
Group Name
</h3>
<fieldset>
<span class="value">
<gr-autocomplete
id="groupNameInput"
.text=${this.groupConfig?.name}
?disabled=${this.computeGroupDisabled()}
@text-changed=${this.handleNameTextChanged}
></gr-autocomplete>
</span>
<span class="value">
<gr-button
id="inputUpdateNameBtn"
?disabled=${!groupNameEdited}
@click=${this.handleSaveName}
>
Rename Group</gr-button
>
</span>
</fieldset>
`;
}
private renderGroupOwner() {
const groupOwnerNameEdited =
this.originalOwnerName !== this.groupConfig?.owner;
return html`
<h3
id="groupOwner"
class="heading-3 ${this.computeHeaderClass(groupOwnerNameEdited)}"
>
Owners
</h3>
<fieldset>
<span class="value">
<gr-autocomplete
id="groupOwnerInput"
.text=${this.groupConfig?.owner}
.value=${this.groupConfigOwner}
.query=${this.query}
?disabled=${this.computeGroupDisabled()}
@text-changed=${this.handleOwnerTextChanged}
@value-changed=${this.handleOwnerValueChanged}
>
</gr-autocomplete>
</span>
<span class="value">
<gr-button
id="inputUpdateOwnerBtn"
?disabled=${!groupOwnerNameEdited}
@click=${this.handleSaveOwner}
>
Change Owners</gr-button
>
</span>
</fieldset>
`;
}
private renderGroupDescription() {
const groupDescriptionEdited =
this.originalDescriptionName !== this.groupConfig?.description;
return html`
<h3 class="heading-3 ${this.computeHeaderClass(groupDescriptionEdited)}">
Description
</h3>
<fieldset>
<div>
<gr-textarea
class="description"
autocomplete="on"
rows="4"
monospace
?disabled=${this.computeGroupDisabled()}
.text=${this.groupConfig?.description ?? ''}
@text-changed=${this.handleDescriptionTextChanged}
></gr-textarea>
</div>
<span class="value">
<gr-button
?disabled=${!groupDescriptionEdited}
@click=${this.handleSaveDescription}
>
Save Description
</gr-button>
</span>
</fieldset>
`;
}
private renderGroupOptions() {
// We make sure the value is a boolean
// this is done so undefined is converted to false.
const groupOptionsEdited =
Boolean(this.originalOptionsVisibleToAll) !==
Boolean(this.groupConfig?.options?.visible_to_all);
// We have to convert boolean to string in order
// for the selection to work correctly.
// We also convert undefined to false using boolean.
return html`
<h3
id="options"
class="heading-3 ${this.computeHeaderClass(groupOptionsEdited)}"
>
Group Options
</h3>
<fieldset>
<section>
<span class="title">
Make group visible to all registered users
</span>
<span class="value">
<gr-select
id="visibleToAll"
.bindValue=${convertToString(
Boolean(this.groupConfig?.options?.visible_to_all)
)}
@bind-value-changed=${this.handleOptionsBindValueChanged}
>
<select ?disabled=${this.computeGroupDisabled()}>
${this.submitTypes.map(
item => html`
<option value=${item.value}>${item.label}</option>
`
)}
</select>
</gr-select>
</span>
</section>
<span class="value">
<gr-button
?disabled=${!groupOptionsEdited}
@click=${this.handleSaveOptions}
>
Save Group Options
</gr-button>
</span>
</fieldset>
`;
}
override willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has('groupId')) {
this.loadGroup();
}
}
// private but used in test
async loadGroup() {
if (!this.groupId) return;
const promises: Promise<unknown>[] = [];
const errFn: ErrorCallback = response => {
firePageError(response);
};
const config = await this.restApiService.getGroupConfig(
this.groupId,
errFn
);
if (!config || !config.name) return;
if (config.description === undefined) {
config.description = '';
}
this.originalName = config.name;
this.originalOwnerName = config.owner;
this.originalDescriptionName = config.description;
this.groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
promises.push(
this.restApiService.getIsAdmin().then(isAdmin => {
this.isAdmin = !!isAdmin;
})
);
promises.push(
this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
this.groupOwner = !!isOwner;
})
);
this.groupConfig = config;
this.originalOptionsVisibleToAll = config?.options?.visible_to_all;
fireTitleChange(config.name);
await Promise.all(promises);
this.loading = false;
}
// private but used in test
computeLoadingClass() {
return this.loading ? 'loading' : '';
}
// private but used in test
async handleSaveName() {
const groupConfig = this.groupConfig;
if (!this.groupId || !groupConfig || !groupConfig.name) {
return Promise.reject(new Error('invalid groupId or config name'));
}
const groupName = groupConfig.name;
const config = await this.restApiService.saveGroupName(
this.groupId,
groupName
);
if (config.status === 200) {
this.originalName = groupName;
const detail: GroupNameChangedDetail = {
name: groupName,
external: !this.groupIsInternal,
};
fire(this, 'name-changed', detail);
this.requestUpdate();
}
return;
}
// private but used in test
async handleSaveOwner() {
if (!this.groupId || !this.groupConfig) return;
let owner = this.groupConfig.owner;
if (this.groupConfigOwner) {
owner = decodeURIComponent(this.groupConfigOwner);
}
if (!owner) return;
await this.restApiService.saveGroupOwner(this.groupId, owner);
this.originalOwnerName = this.groupConfig?.owner;
this.groupConfigOwner = undefined;
}
// private but used in test
async handleSaveDescription() {
if (
!this.groupId ||
!this.groupConfig ||
this.groupConfig.description === undefined
)
return;
await this.restApiService.saveGroupDescription(
this.groupId,
this.groupConfig.description
);
this.originalDescriptionName = this.groupConfig.description;
}
// private but used in test
async handleSaveOptions() {
if (!this.groupId || !this.groupConfig || !this.groupConfig.options) return;
const visible = this.groupConfig.options.visible_to_all;
const options = {visible_to_all: visible};
await this.restApiService.saveGroupOptions(this.groupId, options);
this.originalOptionsVisibleToAll = visible;
}
private computeHeaderClass(configChanged: boolean) {
return configChanged ? 'edited' : '';
}
private getGroupSuggestions(input: string) {
return this.restApiService
.getSuggestedGroups(
input,
/* project=*/ undefined,
/* n=*/ undefined,
throwingErrorCallback
)
.then(response => {
const groups: AutocompleteSuggestion[] = [];
for (const [name, group] of Object.entries(response ?? {})) {
groups.push({name, value: decodeURIComponent(group.id)});
}
return groups;
});
}
// private but used in test
computeGroupDisabled() {
return !(this.groupIsInternal && (this.isAdmin || this.groupOwner));
}
private getGroupUUID() {
const id = this.groupConfig?.id;
if (!id) return;
return id.match(INTERNAL_GROUP_REGEX) ? id : decodeURIComponent(id);
}
private handleNameTextChanged(e: ValueChangedEvent) {
if (!this.groupConfig || this.loading) return;
this.groupConfig.name = e.detail.value as GroupName;
this.requestUpdate();
}
private handleOwnerTextChanged(e: ValueChangedEvent) {
if (!this.groupConfig || this.loading) return;
this.groupConfig.owner = e.detail.value;
this.requestUpdate();
}
private handleOwnerValueChanged(e: ValueChangedEvent) {
if (this.loading) return;
this.groupConfigOwner = e.detail.value;
this.requestUpdate();
}
private handleDescriptionTextChanged(e: ValueChangedEvent) {
if (!this.groupConfig || this.loading) return;
this.groupConfig.description = e.detail.value;
this.requestUpdate();
}
private handleOptionsBindValueChanged(e: BindValueChangeEvent) {
if (!this.groupConfig || this.loading) return;
// Because the value for e.detail.value is a string
// we convert the value to a boolean.
const value = e.detail.value === 'true' ? true : false;
this.groupConfig.options!.visible_to_all = value;
this.requestUpdate();
}
}