gr-account-list to lit
Release-Notes: skip
Change-Id: I16dc2fe017b921320e20682427eebbb1365fee71
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 2dc401f..e8448b7 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
@@ -2555,8 +2555,9 @@
_resetReplyOverlayFocusStops() {
const dialog = query<GrReplyDialog>(this, '#replyDialog');
- if (!dialog) return;
- this.$.replyOverlay.setFocusStops(dialog.getFocusStops());
+ const focusStops = dialog?.getFocusStops();
+ if (!focusStops) return;
+ this.$.replyOverlay.setFocusStops(focusStops);
}
_handleToggleStar(e: CustomEvent<ChangeStarToggleStarDetail>) {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
index d7b4ef3..2972d24 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
@@ -101,12 +101,12 @@
test('submit blocked when invalid email is supplied to ccs', () => {
const sendStub = sinon.stub(element, 'send').returns(Promise.resolve());
- element.$.ccs.$.entry.setText('test');
+ element.$.ccs.entry!.setText('test');
MockInteractions.tap(queryAndAssert(element, 'gr-button.send'));
assert.isFalse(sendStub.called);
flush();
- element.$.ccs.$.entry.setText('test@test.test');
+ element.$.ccs.entry!.setText('test@test.test');
MockInteractions.tap(queryAndAssert(element, 'gr-button.send'));
assert.isTrue(sendStub.called);
});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index de03d81..c7b1982 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -82,10 +82,7 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrLabelScores} from '../gr-label-scores/gr-label-scores';
import {GrLabelScoreRow} from '../gr-label-score-row/gr-label-score-row';
-import {
- PolymerDeepPropertyChange,
- PolymerSpliceChange,
-} from '@polymer/polymer/interfaces';
+import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {
areSetsEqual,
assertIsDefined,
@@ -119,6 +116,7 @@
import {resolve, DIPolymerElement} from '../../../models/dependency';
import {changeModelToken} from '../../../models/change/change-model';
import {LabelNameToValuesMap} from '../../../api/rest-api';
+import {ValueChangedEvent} from '../../../types/events';
const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
@@ -480,6 +478,7 @@
getFocusStops() {
const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
+ if (!this.$.reviewers.focusStart) return undefined;
return {
start: this.$.reviewers.focusStart,
end,
@@ -502,16 +501,6 @@
return selectorEl?.selectedValue;
}
- @observe('_reviewers.splices', '_ccs.splices')
- reviewerOrCCChanged(
- reviewerSplices?: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>,
- ccsSplices?: PolymerSpliceChange<AccountInfo[] | GroupInfo[]>
- ) {
- if (reviewerSplices?.indexSplices || ccsSplices?.indexSplices) {
- this._reviewersMutated = true;
- }
- }
-
accountAdded(e: CustomEvent<AccountInputDetail>) {
const account = e.detail.account;
const key = accountOrGroupKey(account);
@@ -679,10 +668,10 @@
setTimeout(() => textarea.getNativeTextarea().focus());
} else if (section === FocusTarget.REVIEWERS) {
const reviewerEntry = this.$.reviewers.focusStart;
- setTimeout(() => reviewerEntry.focus());
+ setTimeout(() => reviewerEntry?.focus());
} else if (section === FocusTarget.CCS) {
const ccEntry = this.$.ccs.focusStart;
- setTimeout(() => ccEntry.focus());
+ setTimeout(() => ccEntry?.focus());
}
}
@@ -1278,6 +1267,33 @@
Object.keys(this.getLabelScores().getLabelValues(false)).length !== 0;
}
+ // To decouple account-list and reply dialog
+ _getAccountListCopy(list: (AccountInfo | GroupInfo)[]) {
+ return list.slice();
+ }
+
+ _handleReviewersChanged(e: ValueChangedEvent<(AccountInfo | GroupInfo)[]>) {
+ this._reviewers = e.detail.value.slice();
+ this._reviewersMutated = true;
+ }
+
+ _handleCcsChanged(e: ValueChangedEvent<(AccountInfo | GroupInfo)[]>) {
+ this._ccs = e.detail.value.slice();
+ this._reviewersMutated = true;
+ }
+
+ _handleReviewersConfirmationChanged(
+ e: ValueChangedEvent<SuggestedReviewerGroupInfo | null>
+ ) {
+ this._reviewerPendingConfirmation = e.detail.value;
+ }
+
+ _handleCcsConfirmationChanged(
+ e: ValueChangedEvent<SuggestedReviewerGroupInfo | null>
+ ) {
+ this._ccPendingConfirmation = e.detail.value;
+ }
+
_isState(knownLatestState?: LatestPatchState, value?: LatestPatchState) {
return knownLatestState === value;
}
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 c1d7cb1..c4b3578 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
@@ -266,11 +266,13 @@
<div class="peopleListLabel">Reviewers</div>
<gr-account-list
id="reviewers"
- accounts="{{_reviewers}}"
+ accounts="[[_getAccountListCopy(_reviewers)]]"
on-account-added="accountAdded"
+ on-accounts-changed="_handleReviewersChanged"
removable-values="[[change.removable_reviewers]]"
filter="[[filterReviewerSuggestion]]"
- pending-confirmation="{{_reviewerPendingConfirmation}}"
+ pending-confirmation="[[_reviewerPendingConfirmation]]"
+ on-pending-confirmation-changed="_handleReviewersConfirmationChanged"
placeholder="Add reviewer..."
on-account-text-changed="_handleAccountTextEntry"
suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"
@@ -284,10 +286,12 @@
<div class="peopleListLabel">CC</div>
<gr-account-list
id="ccs"
- accounts="{{_ccs}}"
+ accounts="[[_getAccountListCopy(_ccs)]]"
on-account-added="accountAdded"
+ on-accounts-changed="_handleCcsChanged"
filter="[[filterCCSuggestion]]"
- pending-confirmation="{{_ccPendingConfirmation}}"
+ pending-confirmation="[[_ccPendingConfirmation]]"
+ pending-confirmation-changed="_handleCcsConfirmationChanged"
allow-any-input=""
placeholder="Add CC..."
on-account-text-changed="_handleAccountTextEntry"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
index 1c0768e..883fbfa 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.ts
@@ -1184,7 +1184,7 @@
// We should be focused on account entry input.
assert.isTrue(
isFocusInsideElement(
- queryAndAssert<GrAccountList>(element, '#reviewers').$.entry.$.input.$
+ queryAndAssert<GrAccountList>(element, '#reviewers').entry!.$.input.$
.input
)
);
@@ -1245,13 +1245,13 @@
if (cc) {
assert.isTrue(
isFocusInsideElement(
- queryAndAssert<GrAccountList>(element, '#ccs').$.entry.$.input.$.input
+ queryAndAssert<GrAccountList>(element, '#ccs').entry!.$.input.$.input
)
);
} else {
assert.isTrue(
isFocusInsideElement(
- queryAndAssert<GrAccountList>(element, '#reviewers').$.entry.$.input.$
+ queryAndAssert<GrAccountList>(element, '#reviewers').entry!.$.input.$
.input
)
);
@@ -1683,13 +1683,14 @@
const cc1 = makeAccount();
const cc2 = makeAccount();
const cc3 = makeAccount();
- element._reviewers = [reviewer1, reviewer2];
- element._ccs = [cc1, cc2, cc3];
-
element.change!.reviewers = {
[ReviewerState.CC]: [],
[ReviewerState.REVIEWER]: [{_account_id: 33 as AccountId}],
};
+ await flush();
+
+ element._reviewers = [reviewer1, reviewer2];
+ element._ccs = [cc1, cc2, cc3];
const mutations: ReviewerInput[] = [];
@@ -1708,9 +1709,9 @@
})
);
+ await flush();
assert.isTrue(element._reviewersMutated);
-
- ccs.$.entry.dispatchEvent(
+ ccs.entry!.dispatchEvent(
new CustomEvent('add', {
detail: {value: {account: reviewer1}},
composed: true,
@@ -1731,7 +1732,7 @@
bubbles: true,
})
);
- reviewers.$.entry.dispatchEvent(
+ reviewers.entry!.dispatchEvent(
new CustomEvent('add', {
detail: {value: {account: cc1}},
composed: true,
@@ -1741,14 +1742,14 @@
// Add to other field without removing from former field.
// (Currently not possible in UI, but this is a good consistency check).
- reviewers.$.entry.dispatchEvent(
+ reviewers.entry!.dispatchEvent(
new CustomEvent('add', {
detail: {value: {account: cc2}},
composed: true,
bubbles: true,
})
);
- ccs.$.entry.dispatchEvent(
+ ccs.entry!.dispatchEvent(
new CustomEvent('add', {
detail: {value: {account: reviewer2}},
composed: true,
@@ -1771,6 +1772,7 @@
// Send and purge and verify moves, delete cc3.
await element.send(false, false);
+ await flush();
expect(mutations).to.have.lengthOf(5);
expect(mutations[0]).to.deep.equal(
mapReviewer(cc1, ReviewerState.REVIEWER)
@@ -1819,7 +1821,7 @@
bubbles: true,
})
);
- ccs.$.entry.dispatchEvent(
+ ccs.entry!.dispatchEvent(
new CustomEvent('add', {
detail: {value: {account: reviewer1}},
composed: true,
@@ -2221,7 +2223,7 @@
await flush();
assert.equal(
- element.getFocusStops().end,
+ element.getFocusStops()!.end,
queryAndAssert(element, '#cancelButton')
);
element.draftCommentThreads = [
@@ -2239,7 +2241,7 @@
await flush();
assert.equal(
- element.getFocusStops().end,
+ element.getFocusStops()!.end,
queryAndAssert(element, '#sendButton')
);
});
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
index 1553eef..cf24bcd 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.ts
@@ -16,11 +16,7 @@
*/
import '../gr-account-chip/gr-account-chip';
import '../gr-account-entry/gr-account-entry';
-import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-account-list_html';
import {getAppContext} from '../../../services/app-context';
-import {customElement, property} from '@polymer/decorators';
import {
ChangeInfo,
Suggestion,
@@ -30,20 +26,29 @@
SuggestedReviewerGroupInfo,
SuggestedReviewerAccountInfo,
} from '../../../types/common';
-import {
- ReviewerSuggestionsProvider,
- SuggestionItem,
-} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
+import {ReviewerSuggestionsProvider} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider';
import {GrAccountEntry} from '../gr-account-entry/gr-account-entry';
import {GrAccountChip} from '../gr-account-chip/gr-account-chip';
-import {PolymerDeepPropertyChange} from '@polymer/polymer/interfaces';
import {PaperInputElementExt} from '../../../types/types';
-import {fireAlert, fire} from '../../../utils/event-util';
+import {fire, fireAlert} from '../../../utils/event-util';
import {accountOrGroupKey} from '../../../utils/account-util';
+import {LitElement, css, html, PropertyValues} from 'lit';
+import {customElement, property, query, state} from 'lit/decorators';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {classMap} from 'lit/directives/class-map';
+import {
+ AutocompleteQuery,
+ AutocompleteSuggestion,
+} from '../gr-autocomplete/gr-autocomplete';
+import {ValueChangedEvent} from '../../../types/events';
const VALID_EMAIL_ALERT = 'Please input a valid email.';
declare global {
+ interface HTMLElementEventMap {
+ 'accounts-changed': ValueChangedEvent<(AccountInfo | GroupInfo)[]>;
+ 'pending-confirmation-changed': ValueChangedEvent<SuggestedReviewerGroupInfo | null>;
+ }
interface HTMLElementTagNameMap {
'gr-account-list': GrAccountList;
}
@@ -51,13 +56,6 @@
'account-added': CustomEvent<AccountInputDetail>;
}
}
-
-export interface GrAccountList {
- $: {
- entry: GrAccountEntry;
- };
-}
-
export interface AccountInputDetail {
account: AccountInput;
}
@@ -115,18 +113,15 @@
}
@customElement('gr-account-list')
-export class GrAccountList extends PolymerElement {
- static get template() {
- return htmlTemplate;
- }
-
+export class GrAccountList extends LitElement {
/**
* Fired when user inputs an invalid email address.
*
* @event show-alert
*/
+ @query('#entry') entry?: GrAccountEntry;
- @property({type: Array, notify: true})
+ @property({type: Array})
accounts: AccountInput[] = [];
@property({type: Object})
@@ -135,7 +130,7 @@
@property({type: Object})
filter?: (input: Suggestion) => boolean;
- @property({type: String})
+ @property()
placeholder = '';
@property({type: Boolean})
@@ -150,7 +145,7 @@
/**
* Needed for template checking since value is initially set to null.
*/
- @property({type: Object, notify: true})
+ @property({type: Object})
pendingConfirmation: SuggestedReviewerGroupInfo | null = null;
@property({type: Boolean})
@@ -159,7 +154,7 @@
/**
* When true, allows for non-suggested inputs to be added.
*/
- @property({type: Boolean})
+ @property({type: Boolean, attribute: 'allow-any-input'})
allowAnyInput = false;
/**
@@ -175,8 +170,7 @@
/**
* Returns suggestion items
*/
- @property({type: Object})
- _querySuggestions: (input: string) => Promise<SuggestionItem[]>;
+ @state() private querySuggestions: AutocompleteQuery;
private readonly reporting = getAppContext().reportingService;
@@ -184,21 +178,92 @@
constructor() {
super();
- this._querySuggestions = input => this._getSuggestions(input);
+ this.querySuggestions = input => this.getSuggestions(input);
this.addEventListener('remove', e =>
- this._handleRemove(e as CustomEvent<{account: AccountInput}>)
+ this.handleRemove(e as CustomEvent<{account: AccountInput}>)
);
}
- get accountChips() {
- return Array.from(this.root?.querySelectorAll('gr-account-chip') || []);
+ static override styles = [
+ sharedStyles,
+ css`
+ gr-account-chip {
+ display: inline-block;
+ margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
+ }
+ gr-account-entry {
+ display: flex;
+ flex: 1;
+ min-width: 10em;
+ margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
+ }
+ .group {
+ --account-label-suffix: ' (group)';
+ }
+ .pending-add {
+ font-style: italic;
+ }
+ .list {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ }
+ `,
+ ];
+
+ override render() {
+ return html`<div class="list">
+ ${this.accounts.map(
+ account => html`
+ <gr-account-chip
+ .account=${account}
+ class=${classMap({
+ group: !!account._group,
+ pendingAdd: !!account._pendingAdd,
+ })}
+ ?removable=${this.computeRemovable(account)}
+ @keydown=${this.handleChipKeydown}
+ tabindex="-1"
+ >
+ </gr-account-chip>
+ `
+ )}
+ </div>
+ <gr-account-entry
+ borderless=""
+ ?hidden=${(this.maxCount && this.maxCount <= this.accounts.length) ||
+ this.readonly}
+ id="entry"
+ .placeholder=${this.placeholder}
+ @add=${this.handleAdd}
+ @keydown=${this.handleInputKeydown}
+ .allowAnyInput=${this.allowAnyInput}
+ .querySuggestions=${this.querySuggestions}
+ >
+ </gr-account-entry>
+ <slot></slot>`;
+ }
+
+ override willUpdate(changedProperties: PropertyValues) {
+ if (changedProperties.has('pendingConfirmation')) {
+ fire(this, 'pending-confirmation-changed', {
+ value: this.pendingConfirmation,
+ });
+ }
+ }
+
+ get accountChips(): GrAccountChip[] {
+ return Array.from(
+ this.shadowRoot?.querySelectorAll('gr-account-chip') || []
+ );
}
get focusStart() {
- return this.$.entry.focusStart;
+ // Entry is always defined and we cannot return undefined.
+ return this.entry?.focusStart;
}
- _getSuggestions(input: string) {
+ getSuggestions(input: string): Promise<AutocompleteSuggestion[]> {
const provider = this.suggestionsProvider;
if (!provider) return Promise.resolve([]);
return provider.getSuggestions(input).then(suggestions => {
@@ -212,8 +277,11 @@
});
}
- _handleAdd(e: CustomEvent<{value: RawAccountInput}>) {
- this.addAccountItem(e.detail.value);
+ // private but used in test
+ handleAdd(e: ValueChangedEvent<string>) {
+ // TODO(TS) this is temporary hack to avoid cascade of ts issues
+ const item = e.detail.value as RawAccountInput;
+ this.addAccountItem(item);
}
addAccountItem(item: RawAccountInput) {
@@ -226,7 +294,7 @@
if (isAccountObject(item)) {
account = {...item.account, _pendingAdd: true};
this.removeFromPendingRemoval(account);
- this.push('accounts', account);
+ this.accounts.push(account);
itemTypeAdded = 'account';
} else if (isSuggestedReviewerGroupInfo(item)) {
if (item.confirm) {
@@ -234,41 +302,45 @@
return;
}
group = {...item.group, _pendingAdd: true, _group: true};
- this.push('accounts', group);
+ this.accounts.push(group);
this.removeFromPendingRemoval(group);
itemTypeAdded = 'group';
} else if (this.allowAnyInput) {
if (!item.includes('@')) {
// Repopulate the input with what the user tried to enter and have
// a toast tell them why they can't enter it.
- this.$.entry.setText(item);
+ this.entry?.setText(item);
fireAlert(this, VALID_EMAIL_ALERT);
return false;
} else {
account = {email: item as EmailAddress, _pendingAdd: true};
- this.push('accounts', account);
+ this.accounts.push(account);
this.removeFromPendingRemoval(account);
itemTypeAdded = 'email';
}
}
-
+ fire(this, 'accounts-changed', {value: this.accounts.slice()});
fire(this, 'account-added', {account: (account ?? group)! as AccountInput});
this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
this.pendingConfirmation = null;
+ this.requestUpdate();
return true;
}
confirmGroup(group: GroupInfo) {
- this.push('accounts', {
+ this.accounts.push({
...group,
confirmed: true,
_pendingAdd: true,
_group: true,
});
this.pendingConfirmation = null;
+ fire(this, 'accounts-changed', {value: this.accounts});
+ this.requestUpdate();
}
- _computeChipClass(account: AccountInput) {
+ // private but used in test
+ computeChipClass(account: AccountInput) {
const classes = [];
if (account._group) {
classes.push('group');
@@ -279,8 +351,9 @@
return classes.join(' ');
}
- _computeRemovable(account: AccountInput, readonly: boolean) {
- if (readonly) {
+ // private but used in test
+ computeRemovable(account: AccountInput) {
+ if (this.readonly) {
return false;
}
if (this.removableValues) {
@@ -297,21 +370,23 @@
return true;
}
- _handleRemove(e: CustomEvent<{account: AccountInput}>) {
+ private handleRemove(e: CustomEvent<{account: AccountInput}>) {
const toRemove = e.detail.account;
this.removeAccount(toRemove);
- this.$.entry.focus();
+ this.entry?.focus();
}
removeAccount(toRemove?: AccountInput) {
- if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) {
+ if (!toRemove || !this.computeRemovable(toRemove)) {
return;
}
for (let i = 0; i < this.accounts.length; i++) {
if (accountOrGroupKey(toRemove) === accountOrGroupKey(this.accounts[i])) {
- this.splice('accounts', i, 1);
+ this.accounts.splice(i, 1);
this.pendingRemoval.add(toRemove);
this.reporting.reportInteraction(`Remove from ${this.id}`);
+ this.requestUpdate();
+ fire(this, 'accounts-changed', {value: this.accounts.slice()});
return;
}
}
@@ -320,23 +395,23 @@
);
}
- _getNativeInput(paperInput: PaperInputElementExt) {
+ // private but used in test
+ getOwnNativeInput(paperInput: PaperInputElementExt) {
// In Polymer 2 inputElement isn't nativeInput anymore
return (paperInput.$.nativeInput ||
paperInput.inputElement) as HTMLTextAreaElement;
}
- _handleInputKeydown(
- e: CustomEvent<{input: PaperInputElementExt; keyCode: number}>
- ) {
- const input = this._getNativeInput(e.detail.input);
+ private handleInputKeydown(e: KeyboardEvent) {
+ const target = e.target as GrAccountEntry;
+ const input = this.getOwnNativeInput(target.$.input.$.input);
if (
input.selectionStart !== input.selectionEnd ||
input.selectionStart !== 0
) {
return;
}
- switch (e.detail.keyCode) {
+ switch (e.keyCode) {
case 8: // Backspace
this.removeAccount(this.accounts[this.accounts.length - 1]);
break;
@@ -348,7 +423,7 @@
}
}
- _handleChipKeydown(e: KeyboardEvent) {
+ private handleChipKeydown(e: KeyboardEvent) {
const chip = e.target as GrAccountChip;
const chips = this.accountChips;
const index = chips.indexOf(chip);
@@ -366,7 +441,7 @@
} else if (index > 0) {
chips[index - 1].focus();
} else {
- this.$.entry.focus();
+ this.entry?.focus();
}
break;
case 37: // Left arrow
@@ -380,7 +455,7 @@
if (index < chips.length - 1) {
chips[index + 1].focus();
} else {
- this.$.entry.focus();
+ this.entry?.focus();
}
break;
}
@@ -395,13 +470,13 @@
* return true.
*/
submitEntryText() {
- const text = this.$.entry.getText();
- if (!text.length) {
+ const text = this.entry?.getText();
+ if (!text?.length) {
return true;
}
const wasSubmitted = this.addAccountItem(text);
if (wasSubmitted) {
- this.$.entry.clear();
+ this.entry?.clear();
}
return wasSubmitted;
}
@@ -432,19 +507,11 @@
});
}
- removeFromPendingRemoval(account: AccountInput) {
+ private removeFromPendingRemoval(account: AccountInput) {
this.pendingRemoval.delete(account);
}
clearPendingRemovals() {
this.pendingRemoval.clear();
}
-
- _computeEntryHidden(
- maxCount: number,
- accountsRecord: PolymerDeepPropertyChange<AccountInput[], AccountInput[]>,
- readonly: boolean
- ) {
- return (maxCount && maxCount <= accountsRecord.base.length) || readonly;
- }
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts
deleted file mode 100644
index 7a47e29..0000000
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.ts
+++ /dev/null
@@ -1,72 +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">
- gr-account-chip {
- display: inline-block;
- margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
- }
- gr-account-entry {
- display: flex;
- flex: 1;
- min-width: 10em;
- margin: var(--spacing-xs) var(--spacing-xs) var(--spacing-xs) 0;
- }
- .group {
- --account-label-suffix: ' (group)';
- }
- .pending-add {
- font-style: italic;
- }
- .list {
- align-items: center;
- display: flex;
- flex-wrap: wrap;
- }
- </style>
- <!--
- NOTE(Issue 6419): Nest the inner dom-repeat template in a div rather than
- as a direct child of the dom-module's template.
- -->
- <div class="list">
- <template id="chips" is="dom-repeat" items="[[accounts]]" as="account">
- <gr-account-chip
- account="[[account]]"
- class$="[[_computeChipClass(account)]]"
- data-account-id$="[[account._account_id]]"
- removable="[[_computeRemovable(account, readonly)]]"
- on-keydown="_handleChipKeydown"
- tabindex="-1"
- >
- </gr-account-chip>
- </template>
- </div>
- <gr-account-entry
- borderless=""
- hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]"
- id="entry"
- placeholder="[[placeholder]]"
- on-add="_handleAdd"
- on-input-keydown="_handleInputKeydown"
- allow-any-input="[[allowAnyInput]]"
- query-suggestions="[[_querySuggestions]]"
- >
- </gr-account-entry>
- <slot></slot>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
index d77dec3..7509023 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.ts
@@ -28,7 +28,6 @@
GroupBaseInfo,
GroupId,
GroupName,
- SuggestedReviewerAccountInfo,
Suggestion,
} from '../../../types/common';
import {queryAll} from '../../../test/test-utils';
@@ -52,7 +51,7 @@
_account_id: 1 as AccountId,
} as AccountInfo,
count: 1,
- } as SuggestedReviewerAccountInfo,
+ } as unknown as string,
};
}
}
@@ -85,12 +84,14 @@
}
function handleAdd(value: RawAccountInput) {
- element._handleAdd(
- new CustomEvent<{value: RawAccountInput}>('add', {detail: {value}})
+ element.handleAdd(
+ new CustomEvent<{value: string}>('add', {
+ detail: {value: value as unknown as string},
+ })
);
}
- setup(() => {
+ setup(async () => {
existingAccount1 = makeAccount();
existingAccount2 = makeAccount();
@@ -98,18 +99,33 @@
element.accounts = [existingAccount1, existingAccount2];
suggestionsProvider = new MockSuggestionsProvider();
element.suggestionsProvider = suggestionsProvider;
+ await element.updateComplete;
});
- test('account entry only appears when editable', () => {
+ test('renders', () => {
+ expect(element).shadowDom.to.equal(
+ /* HTML */
+ `<div class="list">
+ <gr-account-chip removable="" tabindex="-1"> </gr-account-chip>
+ <gr-account-chip removable="" tabindex="-1"> </gr-account-chip>
+ </div>
+ <gr-account-entry borderless="" id="entry"></gr-account-entry>
+ <slot></slot>`
+ );
+ });
+
+ test('account entry only appears when editable', async () => {
element.readonly = false;
- assert.isFalse(element.$.entry.hasAttribute('hidden'));
+ await element.updateComplete;
+ assert.isFalse(element.entry!.hasAttribute('hidden'));
element.readonly = true;
- assert.isTrue(element.$.entry.hasAttribute('hidden'));
+ await element.updateComplete;
+ assert.isTrue(element.entry!.hasAttribute('hidden'));
});
- test('addition and removal of account/group chips', () => {
- flush();
- sinon.stub(element, '_computeRemovable').returns(true);
+ test('addition and removal of account/group chips', async () => {
+ await element.updateComplete;
+ sinon.stub(element, 'computeRemovable').returns(true);
// Existing accounts are listed.
let chips = getChips();
assert.equal(chips.length, 2);
@@ -119,7 +135,7 @@
// New accounts are added to end with pendingAdd class.
const newAccount = makeAccount();
handleAdd({account: newAccount, count: 1});
- flush();
+ await element.updateComplete;
chips = getChips();
assert.equal(chips.length, 3);
assert.isFalse(chips[0].classList.contains('pendingAdd'));
@@ -134,7 +150,7 @@
bubbles: true,
})
);
- flush();
+ await element.updateComplete;
chips = getChips();
assert.equal(chips.length, 2);
assert.isFalse(chips[0].classList.contains('pendingAdd'));
@@ -155,7 +171,7 @@
bubbles: true,
})
);
- flush();
+ await element.updateComplete;
chips = getChips();
assert.equal(chips.length, 1);
assert.isFalse(chips[0].classList.contains('pendingAdd'));
@@ -163,7 +179,7 @@
// New groups are added to end with pendingAdd and group classes.
const newGroup = makeGroup();
handleAdd({group: newGroup, confirm: false, count: 1});
- flush();
+ await element.updateComplete;
chips = getChips();
assert.equal(chips.length, 2);
assert.isTrue(chips[1].classList.contains('group'));
@@ -177,13 +193,13 @@
bubbles: true,
})
);
- flush();
+ await element.updateComplete;
chips = getChips();
assert.equal(chips.length, 1);
assert.isFalse(chips[0].classList.contains('pendingAdd'));
});
- test('_getSuggestions uses filter correctly', () => {
+ test('getSuggestions uses filter correctly', () => {
const originalSuggestions: Suggestion[] = [
{
email: 'abc@example.com' as EmailAddress,
@@ -212,12 +228,12 @@
value: {
account: suggestion as AccountInfo,
count: 1,
- },
+ } as unknown as string,
};
});
return element
- ._getSuggestions('')
+ .getSuggestions('')
.then(suggestions => {
// Default is no filtering.
assert.equal(suggestions.length, 3);
@@ -228,7 +244,7 @@
return (suggestion as AccountInfo)._account_id === accountId;
};
- return element._getSuggestions('');
+ return element.getSuggestions('');
})
.then(suggestions => {
assert.deepEqual(suggestions, [
@@ -237,52 +253,55 @@
value: {
account: originalSuggestions[0] as AccountInfo,
count: 1,
- },
+ } as unknown as string,
},
]);
});
});
- test('_computeChipClass', () => {
+ test('computeChipClass', () => {
const account = makeAccount() as AccountInfoInput;
- assert.equal(element._computeChipClass(account), '');
+ assert.equal(element.computeChipClass(account), '');
account._pendingAdd = true;
- assert.equal(element._computeChipClass(account), 'pendingAdd');
+ assert.equal(element.computeChipClass(account), 'pendingAdd');
account._group = true;
- assert.equal(element._computeChipClass(account), 'group pendingAdd');
+ assert.equal(element.computeChipClass(account), 'group pendingAdd');
account._pendingAdd = false;
- assert.equal(element._computeChipClass(account), 'group');
+ assert.equal(element.computeChipClass(account), 'group');
});
- test('_computeRemovable', () => {
+ test('computeRemovable', async () => {
const newAccount = makeAccount() as AccountInfoInput;
newAccount._pendingAdd = true;
element.readonly = false;
element.removableValues = [];
- assert.isFalse(element._computeRemovable(existingAccount1, false));
- assert.isTrue(element._computeRemovable(newAccount, false));
+ element.updateComplete;
+ assert.isFalse(element.computeRemovable(existingAccount1));
+ assert.isTrue(element.computeRemovable(newAccount));
element.removableValues = [existingAccount1];
- assert.isTrue(element._computeRemovable(existingAccount1, false));
- assert.isTrue(element._computeRemovable(newAccount, false));
- assert.isFalse(element._computeRemovable(existingAccount2, false));
+ element.updateComplete;
+ assert.isTrue(element.computeRemovable(existingAccount1));
+ assert.isTrue(element.computeRemovable(newAccount));
+ assert.isFalse(element.computeRemovable(existingAccount2));
element.readonly = true;
- assert.isFalse(element._computeRemovable(existingAccount1, true));
- assert.isFalse(element._computeRemovable(newAccount, true));
+ element.updateComplete;
+ assert.isFalse(element.computeRemovable(existingAccount1));
+ assert.isFalse(element.computeRemovable(newAccount));
});
- test('submitEntryText', () => {
+ test('submitEntryText', async () => {
element.allowAnyInput = true;
- flush();
+ await element.updateComplete;
- const getTextStub = sinon.stub(element.$.entry, 'getText');
+ const getTextStub = sinon.stub(element.entry!, 'getText');
getTextStub.onFirstCall().returns('');
getTextStub.onSecondCall().returns('test');
getTextStub.onThirdCall().returns('test@test');
// When entry is empty, return true.
- const clearStub = sinon.stub(element.$.entry, 'clear');
+ const clearStub = sinon.stub(element.entry!, 'clear');
assert.isTrue(element.submitEntryText());
assert.isFalse(clearStub.called);
@@ -363,12 +382,12 @@
assert.equal(element.accounts.length, 1);
});
- test('max-count', () => {
+ test('max-count', async () => {
element.maxCount = 1;
const acct = makeAccount();
handleAdd({account: acct, count: 1});
- flush();
- assert.isTrue(element.$.entry.hasAttribute('hidden'));
+ await element.updateComplete;
+ assert.isTrue(element.entry!.hasAttribute('hidden'));
});
test('enter text calls suggestions provider', async () => {
@@ -391,12 +410,12 @@
'makeSuggestionItem'
);
- const input = element.$.entry.$.input;
+ const input = element.entry!.$.input;
input.text = 'newTest';
MockInteractions.focus(input.$.input);
input.noDebounce = true;
- await flush();
+ await element.updateComplete;
assert.isTrue(getSuggestionsStub.calledOnce);
assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest');
assert.equal(makeSuggestionItemSpy.getCalls().length, 2);
@@ -427,38 +446,38 @@
suite('keyboard interactions', () => {
test('backspace at text input start removes last account', async () => {
- const input = element.$.entry.$.input;
+ const input = element.entry!.$.input;
sinon.stub(input, '_updateSuggestions');
- sinon.stub(element, '_computeRemovable').returns(true);
- await flush();
+ sinon.stub(element, 'computeRemovable').returns(true);
+ await await element.updateComplete;
// Next line is a workaround for Firefox not moving cursor
// on input field update
- assert.equal(element._getNativeInput(input.$.input).selectionStart, 0);
+ assert.equal(element.getOwnNativeInput(input.$.input).selectionStart, 0);
input.text = 'test';
MockInteractions.focus(input.$.input);
- flush();
+ await element.updateComplete;
assert.equal(element.accounts.length, 2);
MockInteractions.pressAndReleaseKeyOn(
- element._getNativeInput(input.$.input),
+ element.getOwnNativeInput(input.$.input),
8
); // Backspace
assert.equal(element.accounts.length, 2);
input.text = '';
MockInteractions.pressAndReleaseKeyOn(
- element._getNativeInput(input.$.input),
+ element.getOwnNativeInput(input.$.input),
8
); // Backspace
- flush();
+ await element.updateComplete;
assert.equal(element.accounts.length, 1);
});
test('arrow key navigation', async () => {
- const input = element.$.entry.$.input;
+ const input = element.entry!.$.input;
input.text = '';
element.accounts = [makeAccount(), makeAccount()];
- flush();
+ await element.updateComplete;
MockInteractions.focus(input.$.input);
- await flush();
+ await await element.updateComplete;
const chips = element.accountChips;
const chipsOneSpy = sinon.spy(chips[1], 'focus');
MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left
@@ -472,9 +491,9 @@
assert.isTrue(chipsOneSpy.calledTwice);
});
- test('delete', () => {
+ test('delete', async () => {
element.accounts = [makeAccount(), makeAccount()];
- flush();
+ await element.updateComplete;
const focusSpy = sinon.spy(element.accountChips[1], 'focus');
const removeSpy = sinon.spy(element, 'removeAccount');
MockInteractions.pressAndReleaseKeyOn(element.accountChips[0], 8); // Backspace
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
index a685f32..36baf87 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.ts
@@ -54,7 +54,7 @@
name?: string;
label?: string;
value?: T;
- text?: T;
+ text?: string;
}
export interface AutocompleteCommitEventDetail {
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
index a74adf6..78cff25 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.ts
@@ -25,10 +25,10 @@
isReviewerGroupSuggestion,
NumericChangeId,
ServerInfo,
- SuggestedReviewerInfo,
Suggestion,
} from '../../types/common';
import {assertNever} from '../../utils/common-util';
+import {AutocompleteSuggestion} from '../../elements/shared/gr-autocomplete/gr-autocomplete';
// TODO(TS): enum name doesn't follow typescript style guid rules
// Rename it
@@ -44,15 +44,10 @@
type ApiCallCallback = (input: string) => Promise<Suggestion[] | void>;
-export interface SuggestionItem {
- name: string;
- value: SuggestedReviewerInfo;
-}
-
export interface ReviewerSuggestionsProvider {
init(): void;
getSuggestions(input: string): Promise<Suggestion[]>;
- makeSuggestionItem(suggestion: Suggestion): SuggestionItem;
+ makeSuggestionItem(suggestion: Suggestion): AutocompleteSuggestion;
}
export class GrReviewerSuggestionsProvider
@@ -120,12 +115,15 @@
return this._apiCall(input).then(reviewers => reviewers || []);
}
- makeSuggestionItem(suggestion: Suggestion): SuggestionItem {
+ // this can be retyped to AutocompleteSuggestion<SuggestedReviewerInfo> but
+ // this would need to change generics of gr-autocomplete.
+ makeSuggestionItem(suggestion: Suggestion): AutocompleteSuggestion {
if (isReviewerAccountSuggestion(suggestion)) {
// Reviewer is an account suggestion from getChangeSuggestedReviewers.
return {
name: getAccountDisplayName(this.config, suggestion.account),
- value: suggestion,
+ // TODO(TS) this is temporary hack to avoid cascade of ts issues
+ value: suggestion as unknown as string,
};
}
@@ -133,7 +131,8 @@
// Reviewer is a group suggestion from getChangeSuggestedReviewers.
return {
name: getGroupDisplayName(suggestion.group),
- value: suggestion,
+ // TODO(TS) this is temporary hack to avoid cascade of ts issues
+ value: suggestion as unknown as string,
};
}
@@ -141,7 +140,8 @@
// Reviewer is an account suggestion from getSuggestedAccounts.
return {
name: getAccountDisplayName(this.config, suggestion),
- value: {account: suggestion, count: 1},
+ // TODO(TS) this is temporary hack to avoid cascade of ts issues
+ value: {account: suggestion, count: 1} as unknown as string,
};
}
assertNever(suggestion, 'Received an incorrect suggestion');