Merge changes I38ea8a25,I5e79bba6,I9f0ef814,Iec0e7f1d
* changes:
gr-group: Use gr-textarea
Convert gr-group to lit
Convert gr-group_test.js to typescript
Fix template problems with gr-group
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 896f9ac..980abb4 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -97,7 +97,6 @@
"elements/admin/gr-admin-view/gr-admin-view_html.ts",
"elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts",
"elements/admin/gr-group-members/gr-group-members_html.ts",
- "elements/admin/gr-group/gr-group_html.ts",
"elements/admin/gr-permission/gr-permission_html.ts",
"elements/admin/gr-plugin-list/gr-plugin-list_html.ts",
"elements/admin/gr-repo-access/gr-repo-access_html.ts",
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
index cf0fdd4..6bd1ac5 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
@@ -505,7 +505,7 @@
suite('groups', () => {
let getGroupConfigStub;
setup(() => {
- stub('gr-group', '_loadGroup').callsFake(() => Promise.resolve({}));
+ stub('gr-group', 'loadGroup').callsFake(() => Promise.resolve({}));
stub('gr-group-members', '_loadGroupDetails').callsFake(() => {});
getGroupConfigStub = stubRestApi('getGroupConfig');
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 596fe5b..d7ffbaf 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -15,29 +15,27 @@
* 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/shared-styles';
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 {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-group_html';
-import {customElement, property, observe} from '@polymer/decorators';
+import '../../shared/gr-textarea/gr-textarea';
import {
AutocompleteSuggestion,
AutocompleteQuery,
} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GroupId, GroupInfo, GroupName} from '../../../types/common';
-import {
- fireEvent,
- firePageError,
- fireTitleChange,
-} from '../../../utils/event-util';
+import {firePageError, fireTitleChange} from '../../../utils/event-util';
import {appContext} from '../../../services/app-context';
import {ErrorCallback} from '../../../api/rest';
+import {convertToString} from '../../../utils/string-util';
+import {BindValueChangeEvent} 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, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
@@ -52,90 +50,267 @@
},
};
-export interface GrGroup {
- $: {
- loading: HTMLDivElement;
- };
-}
-
export interface GroupNameChangedDetail {
name: GroupName;
external: boolean;
}
declare global {
+ interface HTMLElementEventMap {
+ 'text-changed': CustomEvent;
+ 'value-changed': CustomEvent;
+ }
interface HTMLElementTagNameMap {
'gr-group': GrGroup;
}
}
@customElement('gr-group')
-export class GrGroup extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrGroup extends LitElement {
/**
* Fired when the group name changes.
*
* @event name-changed
*/
+ private readonly query: AutocompleteQuery;
+
@property({type: String})
groupId?: GroupId;
- @property({type: Boolean})
- _rename = false;
+ @state() private originalOwnerName?: string;
- @property({type: Boolean})
- _groupIsInternal = false;
+ @state() private originalDescriptionName?: string;
- @property({type: Boolean})
- _description = false;
+ @state() private originalOptionsVisibleToAll?: boolean;
- @property({type: Boolean})
- _owner = false;
+ @state() private submitTypes = Object.values(OPTIONS);
- @property({type: Boolean})
- _options = false;
+ /* private but used in test */
+ @state() isAdmin = false;
- @property({type: Boolean})
- _loading = true;
+ /* private but used in test */
+ @state() groupOwner = false;
- @property({type: Object})
- _groupConfig?: GroupInfo;
+ /* private but used in test */
+ @state() groupIsInternal = false;
- @property({type: String})
- _groupConfigOwner?: string;
+ /* private but used in test */
+ @state() loading = true;
- @property({type: Object})
- _groupName?: string;
+ /* private but used in test */
+ @state() groupConfig?: GroupInfo;
- @property({type: Boolean})
- _groupOwner = false;
+ /* private but used in test */
+ @state() groupConfigOwner?: string;
- @property({type: Array})
- _submitTypes = Object.values(OPTIONS);
-
- @property({type: Object})
- _query: AutocompleteQuery;
-
- @property({type: Boolean})
- _isAdmin = false;
+ /* private but used in test */
+ @state() originalName?: GroupName;
private readonly restApiService = appContext.restApiService;
constructor() {
super();
- this._query = (input: string) => this._getGroupSuggestions(input);
+ this.query = (input: string) => this.getGroupSuggestions(input);
}
override connectedCallback() {
super.connectedCallback();
- this._loadGroup();
+ this.loadGroup();
}
- _loadGroup() {
+ 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">
+ ${convertToString(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=${convertToString(this.groupConfig?.name)}
+ ?disabled=${this.computeGroupDisabled()}
+ @text-changed=${this.handleNameTextChanged}
+ ></gr-autocomplete>
+ </span>
+ <span class="value" ?disabled=${this.computeGroupDisabled()}>
+ <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=${convertToString(this.groupConfig?.owner)}
+ .value=${convertToString(this.groupConfigOwner)}
+ .query=${this.query}
+ ?disabled=${this.computeGroupDisabled()}
+ @text-changed=${this.handleOwnerTextChanged}
+ @value-changed=${this.handleOwnerValueChanged}
+ >
+ </gr-autocomplete>
+ </span>
+ <span class="value" ?disabled=${this.computeGroupDisabled()}>
+ <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=${convertToString(this.groupConfig?.description)}
+ @text-changed=${this.handleDescriptionTextChanged}
+ >
+ </div>
+ <span class="value" ?disabled=${this.computeGroupDisabled()}>
+ <gr-button
+ ?disabled=${!groupDescriptionEdited}
+ @click=${this.handleSaveDescription}
+ >
+ Save Description
+ </gr-button>
+ </span>
+ </fieldset>
+ `;
+ }
+
+ private renderGroupOptions() {
+ const groupOptionsEdited =
+ this.originalOptionsVisibleToAll !==
+ this.groupConfig?.options?.visible_to_all;
+ 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="${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" ?disabled=${this.computeGroupDisabled()}>
+ <gr-button
+ ?disabled=${!groupOptionsEdited}
+ @click=${this.handleSaveOptions}
+ >
+ Save Group Options
+ </gr-button>
+ </span>
+ </fieldset>
+ `;
+ }
+
+ /* private but used in test */
+ async loadGroup() {
if (!this.groupId) {
return;
}
@@ -146,154 +321,127 @@
firePageError(response);
};
- return this.restApiService
- .getGroupConfig(this.groupId, errFn)
- .then(config => {
- if (!config || !config.name) {
- return Promise.resolve();
- }
+ const config = await this.restApiService.getGroupConfig(
+ this.groupId,
+ errFn
+ );
+ if (!config || !config.name) return;
- this._groupName = config.name;
- this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
+ if (config.description === undefined) {
+ config.description = '';
+ }
- promises.push(
- this.restApiService.getIsAdmin().then(isAdmin => {
- this._isAdmin = !!isAdmin;
- })
- );
+ this.originalName = config.name;
+ this.originalOwnerName = config.owner;
+ this.originalDescriptionName = config.description;
+ this.groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
- promises.push(
- this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
- this._groupOwner = !!isOwner;
- })
- );
+ promises.push(
+ this.restApiService.getIsAdmin().then(isAdmin => {
+ this.isAdmin = !!isAdmin;
+ })
+ );
- // If visible to all is undefined, set to false. If it is defined
- // as false, setting to false is fine. If any optional values
- // are added with a default of true, then this would need to be an
- // undefined check and not a truthy/falsy check.
- if (config.options && !config.options.visible_to_all) {
- config.options.visible_to_all = false;
- }
- this._groupConfig = config;
+ promises.push(
+ this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
+ this.groupOwner = !!isOwner;
+ })
+ );
- fireTitleChange(this, config.name);
+ // If visible to all is undefined, set to false. If it is defined
+ // as false, setting to false is fine. If any optional values
+ // are added with a default of true, then this would need to be an
+ // undefined check and not a truthy/falsy check.
+ if (config.options && !config.options.visible_to_all) {
+ config.options.visible_to_all = false;
+ }
+ this.groupConfig = config;
+ this.originalOptionsVisibleToAll = config?.options?.visible_to_all;
- return Promise.all(promises).then(() => {
- this._loading = false;
- });
- });
+ fireTitleChange(this, config.name);
+
+ await Promise.all(promises);
+ this.loading = false;
}
- _computeLoadingClass(loading: boolean) {
- return loading ? 'loading' : '';
+ /* private but used in test */
+ computeLoadingClass() {
+ return this.loading ? 'loading' : '';
}
- _isLoading() {
- return this._loading || this._loading === undefined;
- }
-
- _handleSaveName() {
- const groupConfig = this._groupConfig;
+ /* 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;
- return this.restApiService
- .saveGroupName(this.groupId, groupName)
- .then(config => {
- if (config.status === 200) {
- this._groupName = groupName;
- const detail: GroupNameChangedDetail = {
- name: groupName,
- external: !this._groupIsInternal,
- };
- fireEvent(this, 'name-changed');
- this.dispatchEvent(
- new CustomEvent('name-changed', {
- detail,
- composed: true,
- bubbles: true,
- })
- );
- this._rename = false;
- }
- });
+ const config = await this.restApiService.saveGroupName(
+ this.groupId,
+ groupName
+ );
+ if (config.status === 200) {
+ this.originalName = groupName;
+ const detail: GroupNameChangedDetail = {
+ name: groupName,
+ external: !this.groupIsInternal,
+ };
+ this.dispatchEvent(
+ new CustomEvent('name-changed', {
+ detail,
+ composed: true,
+ bubbles: true,
+ })
+ );
+ this.requestUpdate();
+ }
+
+ return;
}
- _handleSaveOwner() {
- if (!this.groupId || !this._groupConfig) return;
- let owner = this._groupConfig.owner;
- if (this._groupConfigOwner) {
- owner = decodeURIComponent(this._groupConfigOwner);
+ /* 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;
- return this.restApiService.saveGroupOwner(this.groupId, owner).then(() => {
- this._owner = false;
- });
+ await this.restApiService.saveGroupOwner(this.groupId, owner);
+ this.originalOwnerName = this.groupConfig?.owner;
+ this.groupConfigOwner = undefined;
}
- _handleSaveDescription() {
- if (!this.groupId || !this._groupConfig || !this._groupConfig.description)
+ /* private but used in test */
+ async handleSaveDescription() {
+ if (
+ !this.groupId ||
+ !this.groupConfig ||
+ this.groupConfig.description === undefined
+ )
return;
- return this.restApiService
- .saveGroupDescription(this.groupId, this._groupConfig.description)
- .then(() => {
- this._description = false;
- });
+ await this.restApiService.saveGroupDescription(
+ this.groupId,
+ this.groupConfig.description
+ );
+ this.originalDescriptionName = this.groupConfig.description;
}
- _handleSaveOptions() {
- if (!this.groupId || !this._groupConfig || !this._groupConfig.options)
- return;
- const visible = this._groupConfig.options.visible_to_all;
-
+ /* 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};
-
- return this.restApiService
- .saveGroupOptions(this.groupId, options)
- .then(() => {
- this._options = false;
- });
+ await this.restApiService.saveGroupOptions(this.groupId, options);
+ this.originalOptionsVisibleToAll =
+ this.groupConfig?.options?.visible_to_all;
}
- @observe('_groupConfig.name')
- _handleConfigName() {
- if (this._isLoading()) {
- return;
- }
- this._rename = true;
- }
-
- @observe('_groupConfig.owner', '_groupConfigOwner')
- _handleConfigOwner() {
- if (this._isLoading()) {
- return;
- }
- this._owner = true;
- }
-
- @observe('_groupConfig.description')
- _handleConfigDescription() {
- if (this._isLoading()) {
- return;
- }
- this._description = true;
- }
-
- @observe('_groupConfig.options.visible_to_all')
- _handleConfigOptions() {
- if (this._isLoading()) {
- return;
- }
- this._options = true;
- }
-
- _computeHeaderClass(configChanged: boolean) {
+ private computeHeaderClass(configChanged: boolean) {
return configChanged ? 'edited' : '';
}
- _getGroupSuggestions(input: string) {
+ private getGroupSuggestions(input: string) {
return this.restApiService.getSuggestedGroups(input).then(response => {
const groups: AutocompleteSuggestion[] = [];
for (const [name, group] of Object.entries(response ?? {})) {
@@ -303,17 +451,45 @@
});
}
- _computeGroupDisabled(
- owner: boolean,
- admin: boolean,
- groupIsInternal: boolean
- ) {
- return !(groupIsInternal && (admin || owner));
+ /* private but used in test */
+ computeGroupDisabled() {
+ return !(this.groupIsInternal && (this.isAdmin || this.groupOwner));
}
- _getGroupUUID(id: GroupId) {
+ private getGroupUUID() {
+ const id = this.groupConfig?.id;
if (!id) return;
-
return id.match(INTERNAL_GROUP_REGEX) ? id : decodeURIComponent(id);
}
+
+ private handleNameTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.name = e.detail.value as GroupName;
+ this.requestUpdate();
+ }
+
+ private handleOwnerTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.owner = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleOwnerValueChanged(e: CustomEvent) {
+ if (this.loading) return;
+ this.groupConfigOwner = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleDescriptionTextChanged(e: CustomEvent) {
+ if (!this.groupConfig || this.loading) return;
+ this.groupConfig.description = e.detail.value;
+ this.requestUpdate();
+ }
+
+ private handleOptionsBindValueChanged(e: BindValueChangeEvent) {
+ if (!this.groupConfig || !this.groupConfig.options || this.loading) return;
+ this.groupConfig.options.visible_to_all = e.detail
+ .value as unknown as boolean;
+ this.requestUpdate();
+ }
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
deleted file mode 100644
index 6bc5d2a..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 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 {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
- <style include="gr-font-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="shared-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <style include="gr-subpage-styles">
- h3.edited:after {
- color: var(--deemphasized-text-color);
- content: ' *';
- }
- </style>
- <style include="gr-form-styles">
- /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
- </style>
- <div class="main gr-form-styles read-only">
- <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
- Loading...
- </div>
- <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
- <h1 id="Title" class="heading-1">[[_groupName]]</h1>
- <h2 id="configurations" class="heading-2">General</h2>
- <div id="form">
- <fieldset>
- <h3 id="groupUUID" class="heading-3">Group UUID</h3>
- <fieldset>
- <gr-copy-clipboard
- id="uuid"
- text="[[_getGroupUUID(_groupConfig.id)]]"
- ></gr-copy-clipboard>
- </fieldset>
- <h3
- id="groupName"
- class$="heading-3 [[_computeHeaderClass(_rename)]]"
- >
- Group Name
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupNameInput"
- text="{{_groupConfig.name}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- ></gr-autocomplete>
- </span>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- id="inputUpdateNameBtn"
- on-click="_handleSaveName"
- disabled="[[!_rename]]"
- >
- Rename Group</gr-button
- >
- </span>
- </fieldset>
- <h3
- id="groupOwner"
- class$="heading-3 [[_computeHeaderClass(_owner)]]"
- >
- Owners
- </h3>
- <fieldset>
- <span class="value">
- <gr-autocomplete
- id="groupOwnerInput"
- text="{{_groupConfig.owner}}"
- value="{{_groupConfigOwner}}"
- query="[[_query]]"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- </gr-autocomplete>
- </span>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- id="inputUpdateOwnerBtn"
- on-click="_handleSaveOwner"
- disabled="[[!_owner]]"
- >
- Change Owners</gr-button
- >
- </span>
- </fieldset>
- <h3 class$="heading-3 [[_computeHeaderClass(_description)]]">
- Description
- </h3>
- <fieldset>
- <div>
- <iron-autogrow-textarea
- class="description"
- autocomplete="on"
- bind-value="{{_groupConfig.description}}"
- disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- ></iron-autogrow-textarea>
- </div>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button
- on-click="_handleSaveDescription"
- disabled="[[!_description]]"
- >
- Save Description
- </gr-button>
- </span>
- </fieldset>
- <h3 id="options" class$="heading-3 [[_computeHeaderClass(_options)]]">
- Group Options
- </h3>
- <fieldset>
- <section>
- <span class="title">
- Make group visible to all registered users
- </span>
- <span class="value">
- <gr-select
- id="visibleToAll"
- bind-value="{{_groupConfig.options.visible_to_all}}"
- >
- <select
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <template is="dom-repeat" items="[[_submitTypes]]">
- <option value="[[item.value]]">[[item.label]]</option>
- </template>
- </select>
- </gr-select>
- </span>
- </section>
- <span
- class="value"
- disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
- >
- <gr-button on-click="_handleSaveOptions" disabled="[[!_options]]">
- Save Group Options
- </gr-button>
- </span>
- </fieldset>
- </fieldset>
- </div>
- </div>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
deleted file mode 100644
index e390ac5..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ /dev/null
@@ -1,234 +0,0 @@
-/**
- * @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 '../../../test/common-test-setup-karma.js';
-import './gr-group.js';
-import {
- addListenerForTest,
- mockPromise,
- stubRestApi,
-} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-group');
-
-suite('gr-group tests', () => {
- let element;
-
- let groupStub;
- const group = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
- options: {},
- description: 'Gerrit Site Administrators',
- group_id: 1,
- owner: 'Administrators',
- owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- name: 'Administrators',
- };
-
- setup(() => {
- element = basicFixture.instantiate();
- groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
- });
-
- test('loading displays before group config is loaded', () => {
- assert.isTrue(element.$.loading.classList.contains('loading'));
- assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
- assert.isTrue(element.$.loadedContent.classList.contains('loading'));
- assert.isTrue(getComputedStyle(element.$.loadedContent)
- .display === 'none');
- });
-
- test('default values are populated with internal group', async () => {
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- element.groupId = 1;
- await element._loadGroup();
- assert.isTrue(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- });
-
- test('default values with external group', async () => {
- const groupExternal = {...group};
- groupExternal.id = 'external-group-id';
- groupStub.restore();
- groupStub = stubRestApi('getGroupConfig').returns(
- Promise.resolve(groupExternal));
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- element.groupId = 1;
- await element._loadGroup();
- assert.isFalse(element._groupIsInternal);
- assert.isFalse(element.$.visibleToAll.bindValue);
- });
-
- test('rename group', async () => {
- const groupName = 'test-group';
- const groupName2 = 'test-group2';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupName = groupName;
-
- stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
- stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateNameBtn;
-
- await element._loadGroup();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupNameInput.text = groupName2;
-
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupName.classList.contains('edited'));
-
- await element._handleSaveName();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- assert.equal(element._groupName, groupName2);
- });
-
- test('rename group owner', async () => {
- const groupName = 'test-group';
- element.groupId = 1;
- element._groupConfig = {
- name: groupName,
- };
- element._groupConfigOwner = 'testId';
- element._groupOwner = true;
-
- stubRestApi('getIsGroupOwner').returns(Promise.resolve({status: 200}));
-
- const button = element.$.inputUpdateOwnerBtn;
-
- await element._loadGroup();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
-
- element.$.groupOwnerInput.text = 'testId2';
-
- await flush();
- assert.isFalse(button.hasAttribute('disabled'));
- assert.isTrue(element.$.groupOwner.classList.contains('edited'));
-
- await element._handleSaveOwner();
- assert.isTrue(button.hasAttribute('disabled'));
- assert.isFalse(element.$.Title.classList.contains('edited'));
- });
-
- test('test for undefined group name', async () => {
- groupStub.restore();
-
- stubRestApi('getGroupConfig').returns(Promise.resolve({}));
-
- assert.isUndefined(element.groupId);
-
- element.groupId = 1;
-
- assert.isDefined(element.groupId);
-
- // Test that loading shows instead of filling
- // in group details
- await element._loadGroup();
- assert.isTrue(element.$.loading.classList.contains('loading'));
-
- assert.isTrue(element._loading);
- });
-
- test('test fire event', async () => {
- element._groupConfig = {
- name: 'test-group',
- };
- element.groupId = 'gg';
- stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
-
- const showStub = sinon.stub(element, 'dispatchEvent');
- await element._handleSaveName();
- assert.isTrue(showStub.called);
- });
-
- test('_computeGroupDisabled', () => {
- let admin = true;
- let owner = false;
- let groupIsInternal = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- admin = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- owner = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), false);
-
- owner = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- groupIsInternal = false;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
-
- admin = true;
- assert.equal(element._computeGroupDisabled(owner, admin,
- groupIsInternal), true);
- });
-
- test('_computeLoadingClass', () => {
- assert.equal(element._computeLoadingClass(true), 'loading');
- assert.equal(element._computeLoadingClass(false), '');
- });
-
- test('fires page-error', async () => {
- groupStub.restore();
-
- element.groupId = 1;
-
- const response = {status: 404};
- stubRestApi('getGroupConfig').callsFake((group, errFn) => {
- errFn(response);
- return Promise.resolve(undefined);
- });
-
- const promise = mockPromise();
- addListenerForTest(document, 'page-error', e => {
- assert.deepEqual(e.detail.response, response);
- promise.resolve();
- });
-
- element._loadGroup();
- await promise;
- });
-
- test('uuid', () => {
- element._groupConfig = {
- id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
- };
-
- assert.equal(element._groupConfig.id, element.$.uuid.text);
-
- element._groupConfig = {
- id: 'user%2Fgroup',
- };
-
- assert.equal('user/group', element.$.uuid.text);
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
new file mode 100644
index 0000000..5e96e33
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
@@ -0,0 +1,314 @@
+/**
+ * @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 '../../../test/common-test-setup-karma';
+import './gr-group';
+import {GrGroup} from './gr-group';
+import {
+ addListenerForTest,
+ mockPromise,
+ queryAndAssert,
+ stubRestApi,
+} from '../../../test/test-utils';
+import {createGroupInfo} from '../../../test/test-data-generators.js';
+import {GroupId, GroupInfo, GroupName} from '../../../types/common';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {GrSelect} from '../../shared/gr-select/gr-select';
+
+const basicFixture = fixtureFromElement('gr-group');
+
+suite('gr-group tests', () => {
+ let element: GrGroup;
+ let groupStub: sinon.SinonStub;
+
+ const group: GroupInfo = {
+ ...createGroupInfo('6a1e70e1a88782771a91808c8af9bbb7a9871389'),
+ url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ options: {
+ visible_to_all: false,
+ },
+ description: 'Gerrit Site Administrators',
+ group_id: 1,
+ owner: 'Administrators',
+ owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389',
+ name: 'Administrators' as GroupName,
+ };
+
+ setup(async () => {
+ element = basicFixture.instantiate();
+ await element.updateComplete;
+ groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
+ });
+
+ test('loading displays before group config is loaded', () => {
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+ assert.isFalse(
+ getComputedStyle(queryAndAssert<HTMLDivElement>(element, '#loading'))
+ .display === 'none'
+ );
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(
+ element,
+ '#loadedContent'
+ ).classList.contains('loading')
+ );
+ assert.isTrue(
+ getComputedStyle(
+ queryAndAssert<HTMLDivElement>(element, '#loadedContent')
+ ).display === 'none'
+ );
+ });
+
+ test('default values are populated with internal group', async () => {
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ element.groupId = '1' as GroupId;
+ await element.loadGroup();
+ assert.isTrue(element.groupIsInternal);
+ assert.isFalse(
+ queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue
+ );
+ });
+
+ test('default values with external group', async () => {
+ const groupExternal = {...group};
+ groupExternal.id = 'external-group-id' as GroupId;
+ groupStub.restore();
+ groupStub = stubRestApi('getGroupConfig').returns(
+ Promise.resolve(groupExternal)
+ );
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ element.groupId = '1' as GroupId;
+ await element.loadGroup();
+ assert.isFalse(element.groupIsInternal);
+ assert.isFalse(
+ queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue
+ );
+ });
+
+ test('rename group', async () => {
+ const groupName = 'test-group';
+ const groupName2 = 'test-group2';
+ element.groupId = '1' as GroupId;
+ element.groupConfig = {
+ name: groupName as GroupName,
+ id: '1' as GroupId,
+ };
+ element.originalName = groupName as GroupName;
+
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+ stubRestApi('saveGroupName').returns(
+ Promise.resolve({...new Response(), status: 200})
+ );
+
+ const button = queryAndAssert<GrButton>(element, '#inputUpdateNameBtn');
+
+ await element.loadGroup();
+ assert.isTrue(button.hasAttribute('disabled'));
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+
+ queryAndAssert<GrAutocomplete>(element, '#groupNameInput').text =
+ groupName2;
+
+ await element.updateComplete;
+
+ assert.isFalse(button.hasAttribute('disabled'));
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#groupName'
+ ).classList.contains('edited')
+ );
+
+ await element.handleSaveName();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+ assert.equal(element.originalName, groupName2);
+ });
+
+ test('rename group owner', async () => {
+ const groupName = 'test-group';
+ element.groupId = '1' as GroupId;
+ element.groupConfig = {
+ name: groupName as GroupName,
+ id: '1' as GroupId,
+ };
+ element.groupConfigOwner = 'testId';
+ element.groupOwner = true;
+
+ stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+
+ const button = queryAndAssert<GrButton>(element, '#inputUpdateOwnerBtn');
+
+ await element.loadGroup();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+
+ queryAndAssert<GrAutocomplete>(element, '#groupOwnerInput').text =
+ 'testId2';
+
+ await element.updateComplete;
+ assert.isFalse(button.disabled);
+ assert.isTrue(
+ queryAndAssert<HTMLHeadingElement>(
+ element,
+ '#groupOwner'
+ ).classList.contains('edited')
+ );
+
+ await element.handleSaveOwner();
+ assert.isTrue(button.disabled);
+ assert.isFalse(
+ queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+ 'edited'
+ )
+ );
+ });
+
+ test('test for undefined group name', async () => {
+ groupStub.restore();
+
+ stubRestApi('getGroupConfig').returns(Promise.resolve(undefined));
+
+ assert.isUndefined(element.groupId);
+
+ element.groupId = '1' as GroupId;
+
+ assert.isDefined(element.groupId);
+
+ // Test that loading shows instead of filling
+ // in group details
+ await element.loadGroup();
+ assert.isTrue(
+ queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+ 'loading'
+ )
+ );
+
+ assert.isTrue(element.loading);
+ });
+
+ test('test fire event', async () => {
+ element.groupConfig = {
+ name: 'test-group' as GroupName,
+ id: '1' as GroupId,
+ };
+ element.groupId = 'gg' as GroupId;
+ stubRestApi('saveGroupName').returns(
+ Promise.resolve({...new Response(), status: 200})
+ );
+
+ const showStub = sinon.stub(element, 'dispatchEvent');
+ await element.handleSaveName();
+ assert.isTrue(showStub.called);
+ });
+
+ test('computeGroupDisabled', () => {
+ element.isAdmin = true;
+ element.groupOwner = false;
+ element.groupIsInternal = true;
+ assert.equal(element.computeGroupDisabled(), false);
+
+ element.isAdmin = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.groupOwner = true;
+ assert.equal(element.computeGroupDisabled(), false);
+
+ element.groupOwner = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.groupIsInternal = false;
+ assert.equal(element.computeGroupDisabled(), true);
+
+ element.isAdmin = true;
+ assert.equal(element.computeGroupDisabled(), true);
+ });
+
+ test('computeLoadingClass', () => {
+ element.loading = true;
+ assert.equal(element.computeLoadingClass(), 'loading');
+ element.loading = false;
+ assert.equal(element.computeLoadingClass(), '');
+ });
+
+ test('fires page-error', async () => {
+ groupStub.restore();
+
+ element.groupId = '1' as GroupId;
+
+ const response = {...new Response(), status: 404};
+ stubRestApi('getGroupConfig').callsFake((_, errFn) => {
+ if (errFn !== undefined) {
+ errFn(response);
+ } else {
+ assert.fail('errFn is undefined');
+ }
+ return Promise.resolve(undefined);
+ });
+
+ const promise = mockPromise();
+ addListenerForTest(document, 'page-error', e => {
+ assert.deepEqual((e as CustomEvent).detail.response, response);
+ promise.resolve();
+ });
+
+ await element.loadGroup();
+ await promise;
+ });
+
+ test('uuid', async () => {
+ element.groupConfig = {
+ id: '6a1e70e1a88782771a91808c8af9bbb7a9871389' as GroupId,
+ };
+
+ await element.updateComplete;
+
+ assert.equal(
+ element.groupConfig.id,
+ queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+ );
+
+ element.groupConfig = {
+ id: 'user%2Fgroup' as GroupId,
+ };
+
+ await element.updateComplete;
+
+ assert.equal(
+ 'user/group',
+ queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+ );
+ });
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 9e6b42a..ce1b282 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -32,6 +32,7 @@
ItemSelectedEvent,
} from '../gr-autocomplete-dropdown/gr-autocomplete-dropdown';
import {addShortcut, Key} from '../../../utils/dom-util';
+import {BindValueChangeEvent} from '../../../types/events';
const MAX_ITEMS_DROPDOWN = 10;
@@ -63,10 +64,6 @@
match: string;
}
-interface ValueChangeEvent {
- value: string;
-}
-
export interface GrTextarea {
$: {
textarea: IronAutogrowTextareaElement;
@@ -79,7 +76,6 @@
declare global {
interface HTMLElementEventMap {
'item-selected': CustomEvent<ItemSelectedEvent>;
- 'bind-value-changed': CustomEvent<ValueChangeEvent>;
}
}
@@ -316,7 +312,7 @@
* _handleKeydown used for key handling in the this.$.textarea AND all child
* autocomplete options.
*/
- _onValueChanged(e: CustomEvent<ValueChangeEvent>) {
+ _onValueChanged(e: BindValueChangeEvent) {
// Relay the event.
this.dispatchEvent(
new CustomEvent('bind-value-changed', {
diff --git a/polygerrit-ui/app/types/events.ts b/polygerrit-ui/app/types/events.ts
index b6376c4..f467cf6 100644
--- a/polygerrit-ui/app/types/events.ts
+++ b/polygerrit-ui/app/types/events.ts
@@ -22,6 +22,7 @@
import {ChangeMessage} from '../elements/change/gr-message/gr-message';
export enum EventType {
+ BIND_VALUE_CHANGED = 'bind-value-changed',
CHANGE = 'change',
CHANGED = 'changed',
CHANGE_MESSAGE_DELETED = 'change-message-deleted',
@@ -56,6 +57,8 @@
declare global {
interface HTMLElementEventMap {
/* prettier-ignore */
+ 'bind-value-changed': BindValueChangeEvent;
+ /* prettier-ignore */
'change': ChangeEvent;
/* prettier-ignore */
'changed': ChangedEvent;
@@ -102,6 +105,11 @@
}
}
+export interface BindValueChangeEventDetail {
+ value: string;
+}
+export type BindValueChangeEvent = CustomEvent<BindValueChangeEventDetail>;
+
export type ChangeEvent = InputEvent;
export type ChangedEvent = CustomEvent<string>;