Merge "Recipient types are case dependent in REST API calls"
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index cf6560b..586f685 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -61,7 +61,7 @@
Run init before starting the daemon. This will create a new site or
upgrade an existing site.
---s::
+-s::
Start link:dev-inspector.html[Gerrit Inspector] on the console, a
built-in interactive inspection environment to assist debugging and
troubleshooting of Gerrit code.
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
index df1888b..e50482d 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveConstants.java
@@ -21,7 +21,7 @@
@VisibleForTesting
public static final String ONLY_USERS_WITH_TOGGLE_WIP_STATE_PERM_CAN_MODIFY_WIP =
- "only users with Toogle-Wip-State permission can modify Work-in-Progress";
+ "only users with Toggle-Wip-State permission can modify Work-in-Progress";
static final String COMMAND_REJECTION_MESSAGE_FOOTER =
"Contact an administrator to fix the permissions";
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-create-repo-dialog/gr-create-repo-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
index a493747..63f6601 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.ts
@@ -20,6 +20,8 @@
import '../../shared/gr-autocomplete/gr-autocomplete';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-select/gr-select';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrSelect} from '../../shared/gr-select/gr-select';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-create-repo-dialog_html';
import {encodeURL, getBaseUrl} from '../../../utils/url-util';
@@ -40,6 +42,15 @@
}
}
+export interface GrCreateRepoDialog {
+ $: {
+ initialCommit: GrSelect;
+ parentRepo: GrSelect;
+ repoNameInput: HTMLInputElement;
+ rightsInheritFromInput: GrAutocomplete;
+ };
+}
+
@customElement('gr-create-repo-dialog')
export class GrCreateRepoDialog extends PolymerElement {
static get template() {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
index f529ac6..d0a6b7f 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts
@@ -36,24 +36,14 @@
<div id="form">
<section>
<span class="title">Repository name</span>
- <iron-input autocomplete="on" bind-value="{{_repoConfig.name}}">
- <input
- is="iron-input"
- id="repoNameInput"
- autocomplete="on"
- bind-value="{{_repoConfig.name}}"
- />
+ <iron-input bind-value="{{_repoConfig.name}}">
+ <input id="repoNameInput" autocomplete="on" />
</iron-input>
</section>
<section>
<span class="title">Default Branch</span>
- <iron-input autocomplete="off" bind-value="{{_defaultBranch}}">
- <input
- is="iron-input"
- id="defaultBranchNameInput"
- autocomplete="off"
- bind-value="{{_defaultBranch}}"
- />
+ <iron-input bind-value="{{_defaultBranch}}">
+ <input id="defaultBranchNameInput" autocomplete="off" />
</iron-input>
</section>
<section>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
deleted file mode 100644
index f1babee..0000000
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
+++ /dev/null
@@ -1,80 +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-create-repo-dialog.js';
-import {stubRestApi} from '../../../test/test-utils.js';
-
-const basicFixture = fixtureFromElement('gr-create-repo-dialog');
-
-suite('gr-create-repo-dialog tests', () => {
- let element;
-
- setup(() => {
- element = basicFixture.instantiate();
- });
-
- test('default values are populated', () => {
- assert.isTrue(element.$.initialCommit.bindValue);
- assert.isFalse(element.$.parentRepo.bindValue);
- });
-
- test('repo created', async () => {
- const configInputObj = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
-
- const saveStub = stubRestApi('createRepo').returns(Promise.resolve({}));
-
- assert.isFalse(element.hasNewRepoName);
-
- element._repoConfig = {
- name: 'test-repo',
- create_empty_commit: true,
- parent: 'All-Project',
- permissions_only: false,
- };
-
- element._repoOwner = 'test';
- element._repoOwnerId = 'testId';
- element._defaultBranch = 'main';
-
- element.$.repoNameInput.bindValue = configInputObj.name;
- element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
- element.$.initialCommit.bindValue =
- configInputObj.create_empty_commit;
- element.$.parentRepo.bindValue =
- configInputObj.permissions_only;
-
- assert.isTrue(element.hasNewRepoName);
-
- assert.deepEqual(element._repoConfig, configInputObj);
-
- await element.handleCreateRepo();
- assert.isTrue(saveStub.lastCall.calledWithExactly(
- {
- ...configInputObj,
- owners: ['testId'],
- branches: ['main'],
- }
- ));
- });
-});
-
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
new file mode 100644
index 0000000..6485bae
--- /dev/null
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.ts
@@ -0,0 +1,81 @@
+/**
+ * @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-create-repo-dialog';
+import {GrCreateRepoDialog} from './gr-create-repo-dialog';
+import {stubRestApi} from '../../../test/test-utils';
+import {BranchName, GroupId, RepoName} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-create-repo-dialog');
+
+suite('gr-create-repo-dialog tests', () => {
+ let element: GrCreateRepoDialog;
+
+ setup(() => {
+ element = basicFixture.instantiate();
+ });
+
+ test('default values are populated', () => {
+ assert.isTrue(element.$.initialCommit.bindValue);
+ assert.isFalse(element.$.parentRepo.bindValue);
+ });
+
+ test('repo created', async () => {
+ const configInputObj = {
+ name: 'test-repo' as RepoName,
+ create_empty_commit: true,
+ parent: 'All-Project' as RepoName,
+ permissions_only: false,
+ };
+
+ const saveStub = stubRestApi('createRepo').returns(
+ Promise.resolve(new Response())
+ );
+
+ assert.isFalse(element.hasNewRepoName);
+
+ element._repoConfig = {
+ name: 'test-repo' as RepoName,
+ create_empty_commit: true,
+ parent: 'All-Project' as RepoName,
+ permissions_only: false,
+ };
+
+ element._repoOwner = 'test';
+ element._repoOwnerId = 'testId' as GroupId;
+ element._defaultBranch = 'main' as BranchName;
+
+ element.$.repoNameInput.value = configInputObj.name;
+ element.$.rightsInheritFromInput.value = configInputObj.parent;
+ element.$.initialCommit.bindValue = configInputObj.create_empty_commit;
+ element.$.parentRepo.bindValue = configInputObj.permissions_only;
+
+ assert.isTrue(element.hasNewRepoName);
+
+ assert.deepEqual(element._repoConfig, configInputObj);
+
+ await element.handleCreateRepo();
+ assert.isTrue(
+ saveStub.lastCall.calledWithExactly({
+ ...configInputObj,
+ owners: ['testId' as GroupId],
+ branches: ['main' as BranchName],
+ })
+ );
+ });
+});
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/change-list/gr-change-list-column/gr-change-list-column.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
index 4432cc82..b5dee28 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-column/gr-change-list-column.ts
@@ -57,7 +57,9 @@
if (!submitRequirements.length) return html`n/a`;
const numOfRequirements = submitRequirements.length;
const numOfSatisfiedRequirements = submitRequirements.filter(
- req => req.status === SubmitRequirementStatus.SATISFIED
+ req =>
+ req.status === SubmitRequirementStatus.SATISFIED ||
+ req.status === SubmitRequirementStatus.OVERRIDDEN
).length;
if (numOfSatisfiedRequirements === numOfRequirements) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
index f481fd9..cd55e15 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.ts
@@ -121,7 +121,7 @@
</style>
<td aria-hidden="true" class="cell leftPadding"></td>
<td class="cell star" hidden$="[[!showStar]]" hidden="">
- <gr-change-star change="{{change}}"></gr-change-star>
+ <gr-change-star change="[[change]]"></gr-change-star>
</td>
<td class="cell number" hidden$="[[!showNumber]]" hidden="">
<a href$="[[changeURL]]">[[change._number]]</a>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 9c6a079..8d64bc0 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -43,6 +43,7 @@
import '../gr-reply-dialog/gr-reply-dialog';
import '../gr-thread-list/gr-thread-list';
import '../../checks/gr-checks-tab';
+import {ChangeStarToggleStarDetail} from '../../shared/gr-change-star/gr-change-star';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
import {PolymerElement} from '@polymer/polymer/polymer-element';
import {htmlTemplate} from './gr-change-view_html';
@@ -1195,6 +1196,24 @@
return this._changeNum !== this.params?.changeNum;
}
+ hasPatchRangeChanged(value: AppElementChangeViewParams) {
+ if (!this._patchRange) return false;
+ if (this._patchRange.basePatchNum !== value.basePatchNum) return true;
+ return this.hasPatchNumChanged(value);
+ }
+
+ hasPatchNumChanged(value: AppElementChangeViewParams) {
+ if (!this._patchRange) return false;
+ if (value.patchNum !== undefined) {
+ return this._patchRange.patchNum !== value.patchNum;
+ } else {
+ // value.patchNum === undefined specifies the latest patchset
+ return (
+ this._patchRange.patchNum !== computeLatestPatchNum(this._allPatchSets)
+ );
+ }
+ }
+
_paramsChanged(value: AppElementChangeViewParams) {
if (value.view !== GerritView.CHANGE) {
this._initialLoadComplete = false;
@@ -1218,50 +1237,40 @@
if (value.basePatchNum === undefined)
value.basePatchNum = ParentPatchSetNum;
- const patchChanged =
- this._patchRange &&
- value.patchNum !== undefined &&
- (this._patchRange.patchNum !== value.patchNum ||
- this._patchRange.basePatchNum !== value.basePatchNum);
+ const patchChanged = this.hasPatchRangeChanged(value);
+ let patchNumChanged = this.hasPatchNumChanged(value);
- let rightPatchNumChanged =
- this._patchRange &&
- value.patchNum !== undefined &&
- this._patchRange.patchNum !== value.patchNum;
-
- const patchRange: ChangeViewPatchRange = {
+ this._patchRange = {
patchNum: value.patchNum,
basePatchNum: value.basePatchNum,
};
-
- this._patchRange = patchRange;
this.scrollCommentId = value.commentId;
const patchKnown =
- !patchRange.patchNum ||
- (this._allPatchSets ?? []).some(ps => ps.num === patchRange.patchNum);
+ !this._patchRange.patchNum ||
+ (this._allPatchSets ?? []).some(
+ ps => ps.num === this._patchRange!.patchNum
+ );
// _allPatchsets does not know value.patchNum so force a reload.
const forceReload = value.forceReload || !patchKnown;
// If changeNum is defined that means the change has already been
// rendered once before so a full reload is not required.
if (this._changeNum !== undefined && !forceReload) {
- if (!patchRange.patchNum) {
+ if (!this._patchRange.patchNum) {
this._patchRange = {
...this._patchRange,
patchNum: computeLatestPatchNum(this._allPatchSets),
};
- rightPatchNumChanged = true;
+ patchNumChanged = true;
}
if (patchChanged) {
// We need to collapse all diffs when params change so that a non
// existing diff is not requested. See Issue 125270 for more details.
this.$.fileList.collapseAllDiffs();
- this._reloadPatchNumDependentResources(rightPatchNumChanged).then(
- () => {
- this._sendShowChangeEvent();
- }
- );
+ this._reloadPatchNumDependentResources(patchNumChanged).then(() => {
+ this._sendShowChangeEvent();
+ });
}
// If there is no change in patchset or changeNum, such as when user goes
@@ -2217,11 +2226,11 @@
* Kicks off requests for resources that rely on the patch range
* (`this._patchRange`) being defined.
*/
- _reloadPatchNumDependentResources(rightPatchNumChanged?: boolean) {
+ _reloadPatchNumDependentResources(patchNumChanged?: boolean) {
assertIsDefined(this._changeNum, '_changeNum');
if (!this._patchRange?.patchNum) throw new Error('missing patchNum');
const promises = [this._getCommitInfo(), this.$.fileList.reload()];
- if (rightPatchNumChanged)
+ if (patchNumChanged)
promises.push(
this.$.commentAPI.reloadPortedComments(
this._changeNum,
@@ -2471,8 +2480,8 @@
}
@observe('_patchRange.patchNum')
- _patchNumChanged(patchNumStr: PatchSetNum) {
- if (!this._selectedRevision) {
+ _patchNumChanged(patchNumStr?: PatchSetNum) {
+ if (!this._selectedRevision || !patchNumStr) {
return;
}
assertIsDefined(this._change, '_change');
@@ -2549,7 +2558,7 @@
this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
}
- _handleToggleStar(e: CustomEvent<{change: ChangeInfo; starred: boolean}>) {
+ _handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
if (e.detail.starred) {
this.reporting.reportInteraction('change-starred-from-change-view');
this.lastStarredTimestamp = Date.now();
@@ -2627,6 +2636,9 @@
}
declare global {
+ interface HTMLElementEventMap {
+ 'toggle-star': CustomEvent<ChangeStarToggleStarDetail>;
+ }
interface HTMLElementTagNameMap {
'gr-change-view': GrChangeView;
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
index 155d817..9341b18 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.ts
@@ -338,7 +338,7 @@
</div>
<gr-change-star
id="changeStar"
- change="{{_change}}"
+ change="[[_change]]"
on-toggle-star="_handleToggleStar"
hidden$="[[!_loggedIn]]"
></gr-change-star>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 591aa41..ab17f47 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -106,6 +106,7 @@
import {_testOnly_setState} from '../../../services/user/user-model';
import {FocusTarget, GrReplyDialog} from '../gr-reply-dialog/gr-reply-dialog';
import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
+import {GrChangeStar} from '../../shared/gr-change-star/gr-change-star';
const pluginApi = _testOnly_initGerritPluginApi();
const fixture = fixtureFromElement('gr-change-view');
@@ -2027,6 +2028,39 @@
});
});
+ test('patch range changed', () => {
+ element._patchRange = undefined;
+ element._change = createChangeViewChange();
+ element._change!.revisions = createRevisions(4);
+ element._change.current_revision = '1' as CommitId;
+ element._change = {...element._change};
+
+ const params = createAppElementChangeViewParams();
+
+ assert.isFalse(element.hasPatchRangeChanged(params));
+ assert.isFalse(element.hasPatchNumChanged(params));
+
+ params.basePatchNum = ParentPatchSetNum;
+ // undefined means navigate to latest patchset
+ params.patchNum = undefined;
+
+ element._patchRange = {
+ patchNum: 2 as RevisionPatchSetNum,
+ basePatchNum: ParentPatchSetNum,
+ };
+
+ assert.isTrue(element.hasPatchRangeChanged(params));
+ assert.isTrue(element.hasPatchNumChanged(params));
+
+ element._patchRange = {
+ patchNum: 4 as RevisionPatchSetNum,
+ basePatchNum: ParentPatchSetNum,
+ };
+
+ assert.isFalse(element.hasPatchRangeChanged(params));
+ assert.isFalse(element.hasPatchNumChanged(params));
+ });
+
suite('_handleEditTap', () => {
let fireEdit: () => void;
@@ -2168,17 +2202,19 @@
});
});
- test('_handleToggleStar called when star is tapped', () => {
+ test('_handleToggleStar called when star is tapped', async () => {
element._change = {
...createChangeViewChange(),
owner: {_account_id: 1 as AccountId},
starred: false,
};
element._loggedIn = true;
- const stub = sinon.stub(element, '_handleToggleStar');
- flush();
+ await flush();
- tap(element.$.changeStar.shadowRoot!.querySelector('button')!);
+ const stub = sinon.stub(element, '_handleToggleStar');
+
+ const changeStar = queryAndAssert<GrChangeStar>(element, '#changeStar');
+ tap(queryAndAssert<HTMLButtonElement>(changeStar, 'button')!);
assert.isTrue(stub.called);
});
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
index e4703df..744db3b 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-change.ts
@@ -35,6 +35,9 @@
href?: string;
@property()
+ label?: string;
+
+ @property()
showSubmittableCheck = false;
@property()
@@ -110,7 +113,12 @@
const linkClass = this._computeLinkClass(change);
return html`
<div class="changeContainer">
- <a href="${ifDefined(this.href)}" class="${linkClass}"><slot></slot></a>
+ <a
+ href="${ifDefined(this.href)}"
+ aria-label="${ifDefined(this.label)}"
+ class="${linkClass}"
+ ><slot></slot
+ ></a>
${this.showSubmittableCheck
? html`<span
tabindex="-1"
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
index 0e98ddc..963c009 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.ts
@@ -33,6 +33,7 @@
import {appContext} from '../../../services/app-context';
import {ParsedChangeInfo} from '../../../types/types';
import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {truncatePath} from '../../../utils/path-list-util';
import {pluralize} from '../../../utils/string-util';
import {
changeIsOpen,
@@ -274,6 +275,7 @@
${this.renderMarkers(
submittedTogetherMarkersPredicate(index)
)}<gr-related-change
+ .label="${this.renderChangeTitle(change)}"
.change="${change}"
.href="${GerritNav.getUrlForChangeById(
change._number,
@@ -324,6 +326,7 @@
sameTopicMarkersPredicate(index)
)}<gr-related-change
.change="${change}"
+ .label="${this.renderChangeTitle(change)}"
.href="${GerritNav.getUrlForChangeById(
change._number,
change.project
@@ -424,10 +427,17 @@
</section>`;
}
- private renderChangeLine(change: ChangeInfo) {
+ private renderChangeTitle(change: ChangeInfo) {
return `${change.project}: ${change.branch}: ${change.subject}`;
}
+ private renderChangeLine(change: ChangeInfo) {
+ const truncatedRepo = truncatePath(change.project, 2);
+ return html`<span class="truncatedRepo" .title="${change.project}"
+ >${truncatedRepo}</span
+ >: ${change.branch}: ${change.subject}`;
+ }
+
sectionSizeFactory(
relatedChangesLen: number,
submittedTogetherLen: number,
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
index 45a55c1..4a8b996 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.ts
@@ -341,7 +341,6 @@
class="message newReplyDialog"
autocomplete="on"
placeholder="[[_messagePlaceholder]]"
- fixed-position-dropdown=""
monospace="true"
disabled="{{disabled}}"
rows="4"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index e83f948..0ffe61f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -1027,6 +1027,13 @@
return;
}
+ // The diff view is kept in the background once created. If the user
+ // scrolls in the change page, the scrolling is reflected in the diff view
+ // as well, which means the diff is scrolled to a random position based
+ // on how much the change view was scrolled.
+ // Hence, reset the scroll position here.
+ document.documentElement.scrollTop = 0;
+
// Everything in the diff view is tied to the change. It seems better to
// force the re-creation of the diff view when the change number changes.
const changeChanged = this._changeNum !== value.changeNum;
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
index ad671da..dddd6a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
@@ -54,6 +54,7 @@
['text/x-erlang', 'erlang'],
['text/x-fortran', 'fortran'],
['text/x-fsharp', 'fsharp'],
+ ['text/x-gherkin', 'gherkin'],
['text/x-go', 'go'],
['text/x-groovy', 'groovy'],
['text/x-haml', 'haml'],
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
index a23621e..c6fd01c 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.ts
@@ -15,10 +15,6 @@
* limitations under the License.
*/
import '../gr-icons/gr-icons';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-change-star_html';
-import {customElement, property} from '@polymer/decorators';
import {ChangeInfo} from '../../../types/common';
import {fireAlert} from '../../../utils/event-util';
import {
@@ -26,6 +22,9 @@
ShortcutSection,
} from '../../../services/shortcuts/shortcuts-config';
import {appContext} from '../../../services/app-context';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
declare global {
interface HTMLElementTagNameMap {
@@ -39,44 +38,78 @@
}
@customElement('gr-change-star')
-export class GrChangeStar extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrChangeStar extends LitElement {
/**
* Fired when star state is toggled.
*
* @event toggle-star
*/
- @property({type: Object, notify: true})
+ @property({type: Object})
change?: ChangeInfo;
private readonly shortcuts = appContext.shortcutsService;
- _computeStarClass(starred?: boolean) {
- return starred ? 'active' : '';
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ button {
+ background-color: transparent;
+ cursor: pointer;
+ }
+ iron-icon.active {
+ fill: var(--link-color);
+ }
+ iron-icon {
+ vertical-align: top;
+ --iron-icon-height: var(
+ --gr-change-star-size,
+ var(--line-height-normal, 20px)
+ );
+ --iron-icon-width: var(
+ --gr-change-star-size,
+ var(--line-height-normal, 20px)
+ );
+ }
+ :host([hidden]) {
+ visibility: hidden;
+ display: block !important;
+ }
+ `,
+ ];
}
- _computeStarIcon(starred?: boolean) {
- // Hollow star is used to indicate inactive state.
- return `gr-icons:star${starred ? '' : '-border'}`;
- }
-
- _computeAriaLabel(starred?: boolean) {
- return starred ? 'Unstar this change' : 'Star this change';
+ override render() {
+ return html`
+ <button
+ role="checkbox"
+ title=${this.shortcuts.createTitle(
+ Shortcut.TOGGLE_CHANGE_STAR,
+ ShortcutSection.ACTIONS
+ )}
+ aria-label=${this.change?.starred
+ ? 'Unstar this change'
+ : 'Star this change'}
+ @click=${this.toggleStar}
+ >
+ <iron-icon
+ class=${this.change?.starred ? 'active' : ''}
+ .icon=${`gr-icons:star${this.change?.starred ? '' : '-border'}`}
+ ></iron-icon>
+ </button>
+ `;
}
toggleStar() {
// Note: change should always be defined when use gr-change-star
// but since we don't have a good way to enforce usage to always
// set the change, we still check it here.
- if (!this.change) {
- return;
- }
+ if (!this.change) return;
+
const newVal = !this.change.starred;
- this.set('change.starred', newVal);
+ this.change.starred = newVal;
+ this.requestUpdate('change');
const detail: ChangeStarToggleStarDetail = {
change: this.change,
starred: newVal,
@@ -90,8 +123,4 @@
})
);
}
-
- createTitle(shortcutName: Shortcut, section: ShortcutSection) {
- return this.shortcuts.createTitle(shortcutName, section);
- }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
deleted file mode 100644
index d404795..0000000
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.ts
+++ /dev/null
@@ -1,56 +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="shared-styles">
- button {
- background-color: transparent;
- cursor: pointer;
- }
- iron-icon.active {
- fill: var(--link-color);
- }
- iron-icon {
- vertical-align: top;
- --iron-icon-height: var(
- --gr-change-star-size,
- var(--line-height-normal, 20px)
- );
- --iron-icon-width: var(
- --gr-change-star-size,
- var(--line-height-normal, 20px)
- );
- }
- :host([hidden]) {
- visibility: hidden;
- display: block !important;
- }
- </style>
- <button
- role="checkbox"
- title="[[createTitle(Shortcut.TOGGLE_CHANGE_STAR,
- ShortcutSection.ACTIONS)]]"
- aria-label="[[_computeAriaLabel(change.starred)]]"
- on-click="toggleStar"
- >
- <iron-icon
- class$="[[_computeStarClass(change.starred)]]"
- icon$="[[_computeStarIcon(change.starred)]]"
- ></iron-icon>
- </button>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
index 8f411ae..2c5d7a2 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.ts
@@ -27,45 +27,47 @@
suite('gr-change-star tests', () => {
let element: GrChangeStar;
- setup(() => {
+ setup(async () => {
element = basicFixture.instantiate();
element.change = {
...createChange(),
starred: true,
};
+ await element.updateComplete;
});
test('star visibility states', async () => {
- element.set('change.starred', true);
- await flush();
+ element.change!.starred = true;
+ await element.updateComplete;
let icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
assert.isTrue(icon.classList.contains('active'));
assert.equal(icon.icon, 'gr-icons:star');
- element.set('change.starred', false);
- await flush();
+ element.change!.starred = false;
+ element.requestUpdate('change');
+ await element.updateComplete;
icon = queryAndAssert<IronIconElement>(element, 'iron-icon');
assert.isFalse(icon.classList.contains('active'));
assert.equal(icon.icon, 'gr-icons:star-border');
});
test('starring', async () => {
- element.set('change.starred', false);
- await flush();
+ element.change!.starred = false;
+ await element.updateComplete;
assert.equal(element.change!.starred, false);
- MockInteractions.tap(queryAndAssert(element, 'button'));
- await flush();
+ MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+ await element.updateComplete;
assert.equal(element.change!.starred, true);
});
test('unstarring', async () => {
- element.set('change.starred', true);
- await flush();
+ element.change!.starred = true;
+ await element.updateComplete;
assert.equal(element.change!.starred, true);
- MockInteractions.tap(queryAndAssert(element, 'button'));
- await flush();
+ MockInteractions.tap(queryAndAssert<HTMLButtonElement>(element, 'button'));
+ await element.updateComplete;
assert.equal(element.change!.starred, false);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
index 571272d..47295ab 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.ts
@@ -34,7 +34,7 @@
}
@property({type: String, notify: true})
- bindValue?: string | number;
+ bindValue?: string | number | boolean;
get nativeSelect() {
// gr-select is not a shadow component
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/elements/shared/gr-tooltip-content/gr-tooltip-content.ts b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
index 0585aec8..4315071 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.ts
@@ -210,13 +210,9 @@
const left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2;
const right = parentRect.width - left - boxRect.width;
if (left < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${left}px`,
- });
+ tooltip.arrowCenterOffset = `${left}px`;
} else if (right < 0) {
- tooltip.updateStyles({
- '--gr-tooltip-arrow-center-offset': `${-0.5 * right}px`,
- });
+ tooltip.arrowCenterOffset = `${-0.5 * right}px`;
}
tooltip.style.left = `${Math.max(0, left)}px`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
index 8d3bbb0..3b81f46 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.js
@@ -25,11 +25,15 @@
function makeTooltip(tooltipRect, parentRect) {
return {
- getBoundingClientRect() { return tooltipRect; },
- updateStyles: sinon.stub(),
+ arrowCenterOffset: '0',
+ getBoundingClientRect() {
+ return tooltipRect;
+ },
style: {left: 0, top: 0},
parentElement: {
- getBoundingClientRect() { return parentRect; },
+ getBoundingClientRect() {
+ return parentRect;
+ },
},
};
}
@@ -66,12 +70,12 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isFalse(tooltip.updateStyles.called);
+ assert.equal(tooltip.arrowCenterOffset, '0');
assert.equal(tooltip.style.left, '175px');
assert.equal(tooltip.style.top, '100px');
});
- test('left side position', () => {
+ test('left side position', async () => {
sinon.stub(element, 'getBoundingClientRect').callsFake(() => {
return {top: 100, left: 10, width: 50};
});
@@ -80,10 +84,8 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0);
+ await element.updateComplete;
+ assert.isBelow(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '0px');
assert.equal(tooltip.style.top, '100px');
});
@@ -97,10 +99,7 @@
{top: 0, left: 0, width: 1000});
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '100px');
});
@@ -115,19 +114,16 @@
element.positionBelow = true;
element._positionTooltip(tooltip);
- assert.isTrue(tooltip.updateStyles.called);
- const offset = tooltip.updateStyles
- .lastCall.args[0]['--gr-tooltip-arrow-center-offset'];
- assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0);
+ assert.isAbove(parseFloat(tooltip.arrowCenterOffset.replace(/px$/, '')), 0);
assert.equal(tooltip.style.left, '915px');
assert.equal(tooltip.style.top, '157.2px');
});
test('hides tooltip when detached', async () => {
- sinon.stub(element, '_handleHideTooltip');
+ const handleHideTooltipStub = sinon.stub(element, '_handleHideTooltip');
element.remove();
await element.updateComplete;
- assert.isTrue(element._handleHideTooltip.called);
+ assert.isTrue(handleHideTooltipStub.called);
});
test('sets up listeners when has-tooltip is changed', async () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
index cab05b4..0e41891 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.ts
@@ -14,14 +14,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-tooltip_html';
-import {customElement, property, observe} from '@polymer/decorators';
-export interface GrTooltip {
- $: {};
-}
+import {sharedStyles} from '../../../styles/shared-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property} from 'lit/decorators';
+import {styleMap} from 'lit/directives/style-map';
declare global {
interface HTMLElementTagNameMap {
@@ -30,22 +27,78 @@
}
@customElement('gr-tooltip')
-export class GrTooltip extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrTooltip extends LitElement {
@property({type: String})
text = '';
@property({type: String})
maxWidth = '';
- @property({type: Boolean, reflectToAttribute: true})
+ @property({type: String})
+ arrowCenterOffset = '0';
+
+ @property({type: Boolean, reflect: true, attribute: 'position-below'})
positionBelow = false;
- @observe('maxWidth')
- _updateWidth(maxWidth: string) {
- this.updateStyles({'--tooltip-max-width': maxWidth});
+ static override get styles() {
+ return [
+ sharedStyles,
+ css`
+ :host {
+ --gr-tooltip-arrow-size: 0.5em;
+
+ background-color: var(--tooltip-background-color);
+ box-shadow: var(--elevation-level-2);
+ color: var(--tooltip-text-color);
+ font-size: var(--font-size-small);
+ position: absolute;
+ z-index: 1000;
+ }
+ :host .tooltip {
+ padding: var(--spacing-m) var(--spacing-l);
+ }
+ :host .arrowPositionBelow,
+ :host([position-below]) .arrowPositionAbove {
+ display: none;
+ }
+ :host([position-below]) .arrowPositionBelow {
+ display: initial;
+ }
+ .arrow {
+ border-left: var(--gr-tooltip-arrow-size) solid transparent;
+ border-right: var(--gr-tooltip-arrow-size) solid transparent;
+ height: 0;
+ position: absolute;
+ left: calc(50% - var(--gr-tooltip-arrow-size));
+ width: 0;
+ }
+ .arrowPositionAbove {
+ border-top: var(--gr-tooltip-arrow-size) solid
+ var(--tooltip-background-color);
+ bottom: calc(-1 * var(--gr-tooltip-arrow-size));
+ }
+ .arrowPositionBelow {
+ border-bottom: var(--gr-tooltip-arrow-size) solid
+ var(--tooltip-background-color);
+ top: calc(-1 * var(--gr-tooltip-arrow-size));
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ this.style.maxWidth = this.maxWidth;
+
+ return html` <div class="tooltip">
+ <i
+ class="arrowPositionBelow arrow"
+ style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ ></i>
+ ${this.text}
+ <i
+ class="arrowPositionAbove arrow"
+ style="${styleMap({marginLeft: this.arrowCenterOffset})}"
+ ></i>
+ </div>`;
}
}
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
deleted file mode 100644
index d59a6c3..0000000
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.ts
+++ /dev/null
@@ -1,68 +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="shared-styles">
- :host {
- --gr-tooltip-arrow-size: 0.5em;
- --gr-tooltip-arrow-center-offset: 0;
-
- background-color: var(--tooltip-background-color);
- box-shadow: var(--elevation-level-2);
- color: var(--tooltip-text-color);
- font-size: var(--font-size-small);
- position: absolute;
- z-index: 1000;
- max-width: var(--tooltip-max-width);
- }
- :host .tooltip {
- padding: var(--spacing-m) var(--spacing-l);
- }
- :host .arrowPositionBelow,
- :host([position-below]) .arrowPositionAbove {
- display: none;
- }
- :host([position-below]) .arrowPositionBelow {
- display: initial;
- }
- .arrow {
- border-left: var(--gr-tooltip-arrow-size) solid transparent;
- border-right: var(--gr-tooltip-arrow-size) solid transparent;
- height: 0;
- position: absolute;
- left: calc(50% - var(--gr-tooltip-arrow-size));
- margin-left: var(--gr-tooltip-arrow-center-offset);
- width: 0;
- }
- .arrowPositionAbove {
- border-top: var(--gr-tooltip-arrow-size) solid
- var(--tooltip-background-color);
- bottom: calc(-1 * var(--gr-tooltip-arrow-size));
- }
- .arrowPositionBelow {
- border-bottom: var(--gr-tooltip-arrow-size) solid
- var(--tooltip-background-color);
- top: calc(-1 * var(--gr-tooltip-arrow-size));
- }
- </style>
- <div class="tooltip">
- <i class="arrowPositionBelow arrow"></i>
- [[text]]
- <i class="arrowPositionAbove arrow"></i>
- </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
index 8b44047..b693a9e 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.ts
@@ -17,49 +17,45 @@
import '../../../test/common-test-setup-karma';
import './gr-tooltip';
-import {html} from '@polymer/polymer/lib/utils/html-tag';
import {GrTooltip} from './gr-tooltip';
+import {queryAndAssert} from '../../../test/test-utils';
-const basicFixture = fixtureFromTemplate(html` <gr-tooltip> </gr-tooltip> `);
+const basicFixture = fixtureFromElement('gr-tooltip');
suite('gr-tooltip tests', () => {
let element: GrTooltip;
- setup(() => {
+
+ setup(async () => {
element = basicFixture.instantiate() as GrTooltip;
+ await element.updateComplete;
});
- test('max-width is respected if set', () => {
+ test('max-width is respected if set', async () => {
element.text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit' +
', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua';
element.maxWidth = '50px';
+ await element.updateComplete;
assert.equal(getComputedStyle(element).width, '50px');
});
- test('the correct arrow is displayed', () => {
+ test('the correct arrow is displayed', async () => {
assert.equal(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionBelow')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
'none'
);
assert.notEqual(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionAbove')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
'none'
);
element.positionBelow = true;
+ await element.updateComplete;
assert.notEqual(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionBelow')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionBelow')!).display,
'none'
);
assert.equal(
- getComputedStyle(
- element.shadowRoot!.querySelector('.arrowPositionAbove')!
- ).display,
+ getComputedStyle(queryAndAssert(element, '.arrowPositionAbove')!).display,
'none'
);
});
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>;
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index fd2280c..01ca71c 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -67,6 +67,7 @@
f = text/x-fortran
factor = text/x-factor
feathre = text/x-feature
+feature = text/x-gherkin
fcl = text/x-fcl
for = text/x-fortran
formula = text/x-spreadsheet