Merge "Pre-factor for gr-dropdown migration."
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
index 9e2132b..0e43823 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow.ts
@@ -29,7 +29,7 @@
export class GrChangeListHashtagFlow extends LitElement {
@state() private selectedChanges: ChangeInfo[] = [];
- @state() private hashtagToAdd: Hashtag = '' as Hashtag;
+ @state() private hashtagToApply: Hashtag = '' as Hashtag;
@state() private existingHashtagSuggestions: Hashtag[] = [];
@@ -81,6 +81,7 @@
display: flex;
flex-wrap: wrap;
gap: 6px;
+ padding-bottom: var(--spacing-l);
}
.chip {
padding: var(--spacing-s) var(--spacing-xl);
@@ -124,6 +125,16 @@
override render() {
const isFlowDisabled = this.selectedChanges.length === 0;
+ const isCreateNewHashtagDisabled =
+ this.hashtagToApply === '' ||
+ this.existingHashtagSuggestions.includes(this.hashtagToApply) ||
+ this.selectedExistingHashtags.size !== 0 ||
+ this.overallProgress === ProgressStatus.RUNNING;
+ const isApplyHashtagDisabled =
+ (this.selectedExistingHashtags.size === 0 &&
+ (this.hashtagToApply === '' ||
+ !this.existingHashtagSuggestions.includes(this.hashtagToApply))) ||
+ this.overallProgress === ProgressStatus.RUNNING;
return html`
<gr-button
id="start-flow"
@@ -144,11 +155,38 @@
this.isDropdownOpen,
() => html`
<div slot="dropdown-content">
- ${when(
- this.selectedChanges.some(change => change.hashtags?.length),
- () => this.renderExistingHashtagsMode(),
- () => this.renderNoExistingHashtagsMode()
- )}
+ ${this.renderExistingHashtags()}
+ <!--
+ The .query function needs to be bound to this because lit's
+ autobind seems to work only for @event handlers.
+ -->
+ <gr-autocomplete
+ .text=${this.hashtagToApply}
+ .query=${(query: string) => this.getHashtagSuggestions(query)}
+ show-blue-focus-border
+ placeholder="Type hashtag name to create or filter hashtags"
+ @text-changed=${(e: ValueChangedEvent<Hashtag>) =>
+ (this.hashtagToApply = e.detail.value)}
+ ></gr-autocomplete>
+ <div class="footer">
+ <div class="loadingOrError">${this.renderLoadingOrError()}</div>
+ <div class="buttons">
+ <gr-button
+ id="create-new-hashtag-button"
+ flatten
+ @click=${() => this.applyHashtags('Creating hashtag...')}
+ .disabled=${isCreateNewHashtagDisabled}
+ >Create new hashtag</gr-button
+ >
+ <gr-button
+ id="apply-hashtag-button"
+ flatten
+ @click=${() => this.applyHashtags('Applying hashtag...')}
+ .disabled=${isApplyHashtagDisabled}
+ >Apply</gr-button
+ >
+ </div>
+ </div>
</div>
`
)}
@@ -156,38 +194,15 @@
`;
}
- private renderExistingHashtagsMode() {
+ private renderExistingHashtags() {
const hashtags = this.selectedChanges
.flatMap(change => change.hashtags ?? [])
.filter(notUndefined)
.filter(unique);
- const removeDisabled =
- this.selectedExistingHashtags.size === 0 ||
- this.overallProgress === ProgressStatus.RUNNING;
- const applyToAllDisabled = this.selectedExistingHashtags.size !== 1;
return html`
<div class="chips">
${hashtags.map(name => this.renderExistingHashtagChip(name))}
</div>
- <div class="footer">
- <div class="loadingOrError">${this.renderLoadingOrError()}</div>
- <div class="buttons">
- <gr-button
- id="apply-to-all-button"
- flatten
- ?disabled=${applyToAllDisabled}
- @click=${this.applyHashtagToAll}
- >Apply${this.selectedChanges.length > 1 ? ' to all' : nothing}
- </gr-button>
- <gr-button
- id="remove-hashtags-button"
- flatten
- ?disabled=${removeDisabled}
- @click=${this.removeHashtags}
- >Remove</gr-button
- >
- </div>
- </div>
`;
}
@@ -220,53 +235,6 @@
return nothing;
}
- private renderNoExistingHashtagsMode() {
- const isCreateNewHashtagDisabled =
- this.hashtagToAdd === '' ||
- this.existingHashtagSuggestions.includes(this.hashtagToAdd) ||
- this.overallProgress === ProgressStatus.RUNNING;
- const isApplyHashtagDisabled =
- this.hashtagToAdd === '' ||
- !this.existingHashtagSuggestions.includes(this.hashtagToAdd) ||
- this.overallProgress === ProgressStatus.RUNNING;
- return html`
- <!--
- The .query function needs to be bound to this because lit's autobind
- seems to work only for @event handlers.
- 'this.getHashtagSuggestions.bind(this)' gets in trouble with our linter
- even though the bind is necessary here, so an anonymous function is used
- instead.
- -->
- <gr-autocomplete
- .text=${this.hashtagToAdd}
- .query=${(query: string) => this.getHashtagSuggestions(query)}
- show-blue-focus-border
- placeholder="Type hashtag name to create or filter hashtags"
- @text-changed=${(e: ValueChangedEvent<Hashtag>) =>
- (this.hashtagToAdd = e.detail.value)}
- ></gr-autocomplete>
- <div class="footer">
- <div class="loadingOrError">${this.renderLoadingOrError()}</div>
- <div class="buttons">
- <gr-button
- id="create-new-hashtag-button"
- flatten
- @click=${() => this.addHashtag('Creating hashtag...')}
- .disabled=${isCreateNewHashtagDisabled}
- >Create new hashtag</gr-button
- >
- <gr-button
- id="apply-hashtag-button"
- flatten
- @click=${() => this.addHashtag('Applying hashtag...')}
- .disabled=${isApplyHashtagDisabled}
- >Apply</gr-button
- >
- </div>
- </div>
- `;
- }
-
private toggleDropdown() {
if (this.isDropdownOpen) {
this.closeDropdown();
@@ -277,7 +245,7 @@
}
private reset() {
- this.hashtagToAdd = '' as Hashtag;
+ this.hashtagToApply = '' as Hashtag;
this.selectedExistingHashtags = new Set();
this.overallProgress = ProgressStatus.NOT_STARTED;
this.errorText = undefined;
@@ -308,44 +276,16 @@
});
}
- private removeHashtags() {
- this.loadingText = `Removing hashtag${
- this.selectedExistingHashtags.size > 1 ? 's' : ''
- }...`;
- this.trackPromises(
- this.selectedChanges
- .filter(
- change =>
- change.hashtags &&
- change.hashtags.some(hashtag =>
- this.selectedExistingHashtags.has(hashtag)
- )
- )
- .map(change =>
- this.restApiService.setChangeHashtag(change._number, {
- remove: Array.from(this.selectedExistingHashtags.values()),
- })
- )
- );
- }
-
- private applyHashtagToAll() {
- this.loadingText = 'Applying hashtag to all';
- this.trackPromises(
- this.selectedChanges.map(change =>
- this.restApiService.setChangeHashtag(change._number, {
- add: Array.from(this.selectedExistingHashtags.values()),
- })
- )
- );
- }
-
- private addHashtag(loadingText: string) {
+ private applyHashtags(loadingText: string) {
+ const allHashtagsToApply = [
+ ...this.selectedExistingHashtags.values(),
+ ...(this.hashtagToApply === '' ? [] : [this.hashtagToApply]),
+ ];
this.loadingText = loadingText;
this.trackPromises(
this.selectedChanges.map(change =>
this.restApiService.setChangeHashtag(change._number, {
- add: [this.hashtagToAdd],
+ add: allHashtagsToApply,
})
)
);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
index 77517e7..f29ad39 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-hashtag-flow/gr-change-list-hashtag-flow_test.ts
@@ -133,8 +133,8 @@
});
});
- suite('changes in existing hashtags', () => {
- const changesWithHashtags: ChangeInfo[] = [
+ suite('hashtag flow', () => {
+ const changes: ChangeInfo[] = [
{
...createChange(),
_number: 1 as NumericChangeId,
@@ -147,6 +147,11 @@
subject: 'Subject 2',
hashtags: ['hashtag2' as Hashtag],
},
+ {
+ ...createChange(),
+ _number: 3 as NumericChangeId,
+ subject: 'Subject 3',
+ },
];
let setChangeHashtagPromises: MockPromise<string>[];
let setChangeHashtagStub: sinon.SinonStub;
@@ -158,20 +163,18 @@
}
setup(async () => {
- stubRestApi('getDetailedChangesWithActions').resolves(
- changesWithHashtags
- );
+ stubRestApi('getDetailedChangesWithActions').resolves(changes);
setChangeHashtagPromises = [];
setChangeHashtagStub = stubRestApi('setChangeHashtag');
- for (let i = 0; i < changesWithHashtags.length; i++) {
+ for (let i = 0; i < changes.length; i++) {
const promise = mockPromise<string>();
setChangeHashtagPromises.push(promise);
setChangeHashtagStub
- .withArgs(changesWithHashtags[i]._number, sinon.match.any)
+ .withArgs(changes[i]._number, sinon.match.any)
.returns(promise);
}
model = new BulkActionsModel(getAppContext().restApiService);
- model.sync(changesWithHashtags);
+ model.sync(changes);
element = (
await fixture(
@@ -184,9 +187,10 @@
).querySelector('gr-change-list-hashtag-flow')!;
// select changes
- await selectChange(changesWithHashtags[0]);
- await selectChange(changesWithHashtags[1]);
- await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
+ await selectChange(changes[0]);
+ await selectChange(changes[1]);
+ await selectChange(changes[2]);
+ await waitUntilObserved(model.selectedChanges$, s => s.length === 3);
await element.updateComplete;
// open flow
@@ -195,7 +199,7 @@
await flush();
});
- test('renders existing-hashtags flow', () => {
+ test('renders hashtags flow', () => {
expect(element).shadowDom.to.equal(
/* HTML */ `
<gr-button
@@ -221,214 +225,6 @@
>hashtag2</span
>
</div>
- <div class="footer">
- <div class="loadingOrError"></div>
- <div class="buttons">
- <gr-button
- id="apply-to-all-button"
- flatten=""
- aria-disabled="true"
- disabled=""
- role="button"
- tabindex="-1"
- >Apply to all</gr-button
- >
- <gr-button
- id="remove-hashtags-button"
- flatten=""
- aria-disabled="true"
- disabled=""
- role="button"
- tabindex="-1"
- >Remove</gr-button
- >
- </div>
- </div>
- </div>
- </iron-dropdown>
- `,
- {
- // iron-dropdown sizing seems to vary between local & CI
- ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
- }
- );
- });
-
- test('remove single hashtag', async () => {
- queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
- await element.updateComplete;
- queryAndAssert<GrButton>(element, '#remove-hashtags-button').click();
- await element.updateComplete;
-
- assert.equal(
- queryAndAssert(element, '.loadingText').textContent,
- 'Removing hashtag...'
- );
-
- await resolvePromises();
- await element.updateComplete;
-
- // not called for second change which as a different hashtag
- assert.isTrue(setChangeHashtagStub.calledOnce);
- assert.deepEqual(setChangeHashtagStub.firstCall.args, [
- changesWithHashtags[0]._number,
- {remove: ['hashtag1']},
- ]);
- });
-
- test('remove multiple hashtags', async () => {
- queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
- queryAll<HTMLSpanElement>(element, 'span.chip')[1].click();
- await element.updateComplete;
- queryAndAssert<GrButton>(element, '#remove-hashtags-button').click();
- await element.updateComplete;
-
- assert.equal(
- queryAndAssert(element, '.loadingText').textContent,
- 'Removing hashtags...'
- );
-
- await resolvePromises();
- await element.updateComplete;
-
- // not called for second change which as a different hashtag
- assert.isTrue(setChangeHashtagStub.calledTwice);
- assert.deepEqual(setChangeHashtagStub.firstCall.args, [
- changesWithHashtags[0]._number,
- {remove: ['hashtag1', 'hashtag2']},
- ]);
- assert.deepEqual(setChangeHashtagStub.secondCall.args, [
- changesWithHashtags[1]._number,
- {remove: ['hashtag1', 'hashtag2']},
- ]);
- });
-
- test('can only apply a single hashtag', async () => {
- assert.isTrue(
- queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
- );
-
- queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
- await element.updateComplete;
-
- assert.isFalse(
- queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
- );
-
- queryAll<HTMLSpanElement>(element, 'span.chip')[1].click();
- await element.updateComplete;
-
- assert.isTrue(
- queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
- );
- });
-
- test('applies hashtag to all changes', async () => {
- queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
- await element.updateComplete;
-
- queryAndAssert<GrButton>(element, '#apply-to-all-button').click();
- await element.updateComplete;
-
- assert.equal(
- queryAndAssert(element, '.loadingText').textContent,
- 'Applying hashtag to all'
- );
-
- await resolvePromises();
- await element.updateComplete;
-
- assert.isTrue(setChangeHashtagStub.calledTwice);
- assert.deepEqual(setChangeHashtagStub.firstCall.args, [
- changesWithHashtags[0]._number,
- {add: ['hashtag1']},
- ]);
- assert.deepEqual(setChangeHashtagStub.secondCall.args, [
- changesWithHashtags[1]._number,
- {add: ['hashtag1']},
- ]);
- });
- });
-
- suite('change have no existing hashtags', () => {
- const changesWithNoHashtags: ChangeInfo[] = [
- {
- ...createChange(),
- _number: 1 as NumericChangeId,
- subject: 'Subject 1',
- },
- {
- ...createChange(),
- _number: 2 as NumericChangeId,
- subject: 'Subject 2',
- },
- ];
- let setChangeHashtagPromises: MockPromise<string>[];
- let setChangeHashtagStub: sinon.SinonStub;
-
- async function resolvePromises() {
- setChangeHashtagPromises[0].resolve('foo');
- setChangeHashtagPromises[1].resolve('foo');
- await element.updateComplete;
- }
-
- setup(async () => {
- stubRestApi('getDetailedChangesWithActions').resolves(
- changesWithNoHashtags
- );
- setChangeHashtagPromises = [];
- setChangeHashtagStub = stubRestApi('setChangeHashtag');
- for (let i = 0; i < changesWithNoHashtags.length; i++) {
- const promise = mockPromise<string>();
- setChangeHashtagPromises.push(promise);
- setChangeHashtagStub
- .withArgs(changesWithNoHashtags[i]._number, sinon.match.any)
- .returns(promise);
- }
-
- model = new BulkActionsModel(getAppContext().restApiService);
- model.sync(changesWithNoHashtags);
-
- element = (
- await fixture(
- wrapInProvider(
- html`<gr-change-list-hashtag-flow></gr-change-list-hashtag-flow>`,
- bulkActionsModelToken,
- model
- )
- )
- ).querySelector('gr-change-list-hashtag-flow')!;
-
- // select changes
- await selectChange(changesWithNoHashtags[0]);
- await selectChange(changesWithNoHashtags[1]);
- await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
- await element.updateComplete;
-
- // open flow
- queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
- await element.updateComplete;
- await flush();
- });
-
- test('renders no-existing-hashtags flow', () => {
- expect(element).shadowDom.to.equal(
- /* HTML */ `
- <gr-button
- id="start-flow"
- flatten=""
- down-arrow=""
- aria-disabled="false"
- role="button"
- tabindex="0"
- >Hashtag</gr-button
- >
- <iron-dropdown
- aria-disabled="false"
- vertical-align="auto"
- horizontal-align="auto"
- >
- <div slot="dropdown-content">
<gr-autocomplete
placeholder="Type hashtag name to create or filter hashtags"
show-blue-focus-border=""
@@ -466,6 +262,79 @@
);
});
+ test('apply hashtag from selected change', async () => {
+ // selects "hashtag1"
+ queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
+ await element.updateComplete;
+
+ queryAndAssert<GrButton>(element, '#apply-hashtag-button').click();
+ await element.updateComplete;
+
+ assert.equal(
+ queryAndAssert(element, '.loadingText').textContent,
+ 'Applying hashtag...'
+ );
+
+ await resolvePromises();
+ await element.updateComplete;
+
+ assert.isTrue(setChangeHashtagStub.calledThrice);
+ assert.deepEqual(setChangeHashtagStub.firstCall.args, [
+ changes[0]._number,
+ {add: ['hashtag1']},
+ ]);
+ assert.deepEqual(setChangeHashtagStub.secondCall.args, [
+ changes[1]._number,
+ {add: ['hashtag1']},
+ ]);
+ assert.deepEqual(setChangeHashtagStub.thirdCall.args, [
+ changes[2]._number,
+ {add: ['hashtag1']},
+ ]);
+ });
+
+ test('apply existing hashtag not on selected changes', async () => {
+ const getHashtagsStub = stubRestApi(
+ 'getChangesWithSimilarHashtag'
+ ).resolves([{...createChange(), hashtags: ['foo' as Hashtag]}]);
+ const autocomplete = queryAndAssert<GrAutocomplete>(
+ element,
+ 'gr-autocomplete'
+ );
+
+ autocomplete.setFocus(true);
+ autocomplete.text = 'foo';
+ await element.updateComplete;
+ await waitUntilCalled(getHashtagsStub, 'getHashtagsStub');
+ assert.isTrue(
+ queryAndAssert<GrButton>(element, '#create-new-hashtag-button').disabled
+ );
+
+ queryAndAssert<GrButton>(element, '#apply-hashtag-button').click();
+ await element.updateComplete;
+
+ assert.equal(
+ queryAndAssert(element, '.loadingText').textContent,
+ 'Applying hashtag...'
+ );
+
+ await resolvePromises();
+
+ assert.isTrue(setChangeHashtagStub.calledThrice);
+ assert.deepEqual(setChangeHashtagStub.firstCall.args, [
+ changes[0]._number,
+ {add: ['foo']},
+ ]);
+ assert.deepEqual(setChangeHashtagStub.secondCall.args, [
+ changes[1]._number,
+ {add: ['foo']},
+ ]);
+ assert.deepEqual(setChangeHashtagStub.thirdCall.args, [
+ changes[2]._number,
+ {add: ['foo']},
+ ]);
+ });
+
test('create new hashtag', async () => {
const getHashtagsStub = stubRestApi(
'getChangesWithSimilarHashtag'
@@ -493,51 +362,17 @@
await resolvePromises();
await element.updateComplete;
- assert.isTrue(setChangeHashtagStub.calledTwice);
+ assert.isTrue(setChangeHashtagStub.calledThrice);
assert.deepEqual(setChangeHashtagStub.firstCall.args, [
- changesWithNoHashtags[0]._number,
+ changes[0]._number,
{add: ['foo']},
]);
assert.deepEqual(setChangeHashtagStub.secondCall.args, [
- changesWithNoHashtags[1]._number,
+ changes[1]._number,
{add: ['foo']},
]);
- });
-
- test('apply hashtag', async () => {
- const getHashtagsStub = stubRestApi(
- 'getChangesWithSimilarHashtag'
- ).resolves([{...createChange(), hashtags: ['foo' as Hashtag]}]);
- const autocomplete = queryAndAssert<GrAutocomplete>(
- element,
- 'gr-autocomplete'
- );
-
- autocomplete.setFocus(true);
- autocomplete.text = 'foo';
- await element.updateComplete;
- await waitUntilCalled(getHashtagsStub, 'getHashtagsStub');
- assert.isTrue(
- queryAndAssert<GrButton>(element, '#create-new-hashtag-button').disabled
- );
-
- queryAndAssert<GrButton>(element, '#apply-hashtag-button').click();
- await element.updateComplete;
-
- assert.equal(
- queryAndAssert(element, '.loadingText').textContent,
- 'Applying hashtag...'
- );
-
- await resolvePromises();
-
- assert.isTrue(setChangeHashtagStub.calledTwice);
- assert.deepEqual(setChangeHashtagStub.firstCall.args, [
- changesWithNoHashtags[0]._number,
- {add: ['foo']},
- ]);
- assert.deepEqual(setChangeHashtagStub.secondCall.args, [
- changesWithNoHashtags[1]._number,
+ assert.deepEqual(setChangeHashtagStub.thirdCall.args, [
+ changes[2]._number,
{add: ['foo']},
]);
});