Merge changes I160d92d6,Ia70dfc06,Icd55739b,I9d52653c * changes: Convert gr-repo-list to lit Convert gr-repo-list_test.js to typescript Migrate gr-create-repo-dialog to lit Fix template problems with gr-create-repo-dialog
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD index 980abb4..47c820a 100644 --- a/polygerrit-ui/app/BUILD +++ b/polygerrit-ui/app/BUILD
@@ -95,7 +95,6 @@ # TODO: fix problems reported by template checker in these files. ignore_templates_list = [ "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-permission/gr-permission_html.ts", "elements/admin/gr-plugin-list/gr-plugin-list_html.ts",
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 63f6601..934e3fb 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
@@ -15,18 +15,11 @@ * limitations under the License. */ import '@polymer/iron-input/iron-input'; -import '../../../styles/gr-form-styles'; -import '../../../styles/shared-styles'; 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'; import {page} from '../../../utils/page-wrapper-utils'; -import {customElement, observe, property} from '@polymer/decorators'; import { BranchName, GroupId, @@ -35,63 +28,173 @@ } from '../../../types/common'; import {AutocompleteQuery} from '../../shared/gr-autocomplete/gr-autocomplete'; import {appContext} from '../../../services/app-context'; +import {convertToString} from '../../../utils/string-util'; +import {formStyles} from '../../../styles/gr-form-styles'; +import {sharedStyles} from '../../../styles/shared-styles'; +import {LitElement, css, html} from 'lit'; +import {customElement, query, property, state} from 'lit/decorators'; +import {fireEvent} from '../../../utils/event-util'; declare global { + interface HTMLElementEventMap { + 'text-changed': CustomEvent; + 'value-changed': CustomEvent; + } interface HTMLElementTagNameMap { 'gr-create-repo-dialog': GrCreateRepoDialog; } } -export interface GrCreateRepoDialog { - $: { - initialCommit: GrSelect; - parentRepo: GrSelect; - repoNameInput: HTMLInputElement; - rightsInheritFromInput: GrAutocomplete; - }; -} - @customElement('gr-create-repo-dialog') -export class GrCreateRepoDialog extends PolymerElement { - static get template() { - return htmlTemplate; - } +export class GrCreateRepoDialog extends LitElement { + /** + * Fired when repostiory name is entered. + * + * @event new-repo-name + */ - @property({type: Boolean, notify: true}) - hasNewRepoName = false; + @query('input') + input?: HTMLInputElement; - @property({type: Object}) - _repoConfig: ProjectInput & {name: RepoName} = { + @property({type: Boolean}) + nameChanged = false; + + /* private but used in test */ + @state() repoConfig: ProjectInput & {name: RepoName} = { create_empty_commit: true, permissions_only: false, name: '' as RepoName, branches: [], }; - @property({type: String}) - _defaultBranch?: BranchName; + /* private but used in test */ + @state() defaultBranch?: BranchName; - @property({type: Boolean}) - _repoCreated = false; + /* private but used in test */ + @state() repoCreated = false; - @property({type: String}) - _repoOwner?: string; + /* private but used in test */ + @state() repoOwner?: string; - @property({type: String}) - _repoOwnerId?: GroupId; + /* private but used in test */ + @state() repoOwnerId?: GroupId; - @property({type: Object}) - _query: AutocompleteQuery; + private readonly query: AutocompleteQuery; - @property({type: Object}) - _queryGroups: AutocompleteQuery; + private readonly queryGroups: AutocompleteQuery; private readonly restApiService = appContext.restApiService; constructor() { super(); - this._query = (input: string) => this._getRepoSuggestions(input); - this._queryGroups = (input: string) => this._getGroupSuggestions(input); + this.query = (input: string) => this.getRepoSuggestions(input); + this.queryGroups = (input: string) => this.getGroupSuggestions(input); + } + + static override get styles() { + return [ + formStyles, + sharedStyles, + css` + :host { + display: inline-block; + } + input { + width: 20em; + } + gr-autocomplete { + width: 20em; + } + `, + ]; + } + + override render() { + return html` + <div class="gr-form-styles"> + <div id="form"> + <section> + <span class="title">Repository name</span> + <iron-input + .bindValue=${convertToString(this.repoConfig.name)} + @bind-value-changed=${this.handleNameBindValueChanged} + > + <input id="repoNameInput" autocomplete="on" /> + </iron-input> + </section> + <section> + <span class="title">Default Branch</span> + <iron-input + .bindValue=${convertToString(this.defaultBranch)} + @bind-value-changed=${this.handleBranchNameBindValueChanged} + > + <input id="defaultBranchNameInput" autocomplete="off" /> + </iron-input> + </section> + <section> + <span class="title">Rights inherit from</span> + <span class="value"> + <gr-autocomplete + id="rightsInheritFromInput" + .text=${convertToString(this.repoConfig.parent)} + .query=${this.query} + .placeholder="Optional, defaults to 'All-Projects'" + @text-changed=${this.handleRightsTextChanged} + > + </gr-autocomplete> + </span> + </section> + <section> + <span class="title">Owner</span> + <span class="value"> + <gr-autocomplete + id="ownerInput" + .text=${convertToString(this.repoOwner)} + .value=${convertToString(this.repoOwnerId)} + .query=${this.queryGroups} + @text-changed=${this.handleOwnerTextChanged} + @value-changed=${this.handleOwnerValueChanged} + > + </gr-autocomplete> + </span> + </section> + <section> + <span class="title">Create initial empty commit</span> + <span class="value"> + <gr-select + id="initialCommit" + .bindValue=${this.repoConfig.create_empty_commit} + @bind-value-changed=${this + .handleCreateEmptyCommitBindValueChanged} + > + <select> + <option value="false">False</option> + <option value="true">True</option> + </select> + </gr-select> + </span> + </section> + <section> + <span class="title" + >Only serve as parent for other repositories</span + > + <span class="value"> + <gr-select + id="parentRepo" + .bindValue=${this.repoConfig.permissions_only} + @bind-value-changed=${this + .handlePermissionsOnlyBindValueChanged} + > + <select> + <option value="false">False</option> + <option value="true">True</option> + </select> + </gr-select> + </span> + </section> + </div> + </div> + `; } _computeRepoUrl(repoName: string) { @@ -99,44 +202,76 @@ } override focus() { - this.shadowRoot?.querySelector('input')?.focus(); + this.input?.focus(); } - @observe('_repoConfig.name') - _updateRepoName(name: string) { - this.hasNewRepoName = !!name; + async handleCreateRepo() { + if (this.defaultBranch) this.repoConfig.branches = [this.defaultBranch]; + if (this.repoOwnerId) this.repoConfig.owners = [this.repoOwnerId]; + const repoRegistered = await this.restApiService.createRepo( + this.repoConfig + ); + if (repoRegistered.status === 201) { + this.repoCreated = true; + page.show(this._computeRepoUrl(this.repoConfig.name)); + } + return repoRegistered; } - handleCreateRepo() { - if (this._defaultBranch) this._repoConfig.branches = [this._defaultBranch]; - if (this._repoOwnerId) this._repoConfig.owners = [this._repoOwnerId]; - return this.restApiService - .createRepo(this._repoConfig) - .then(repoRegistered => { - if (repoRegistered.status === 201) { - this._repoCreated = true; - page.show(this._computeRepoUrl(this._repoConfig.name)); - } - }); + private async getRepoSuggestions(input: string) { + const response = await this.restApiService.getSuggestedProjects(input); + + const repos = []; + for (const [name, project] of Object.entries(response ?? {})) { + repos.push({name, value: project.id}); + } + return repos; } - _getRepoSuggestions(input: string) { - return this.restApiService.getSuggestedProjects(input).then(response => { - const repos = []; - for (const [name, project] of Object.entries(response ?? {})) { - repos.push({name, value: project.id}); - } - return repos; - }); + private async getGroupSuggestions(input: string) { + const response = await this.restApiService.getSuggestedGroups(input); + + const groups = []; + for (const [name, group] of Object.entries(response ?? {})) { + groups.push({name, value: decodeURIComponent(group.id)}); + } + return groups; } - _getGroupSuggestions(input: string) { - return this.restApiService.getSuggestedGroups(input).then(response => { - const groups = []; - for (const [name, group] of Object.entries(response ?? {})) { - groups.push({name, value: decodeURIComponent(group.id)}); - } - return groups; - }); + private handleRightsTextChanged(e: CustomEvent) { + this.repoConfig.parent = e.detail.value as RepoName; + this.requestUpdate(); + } + + private handleOwnerTextChanged(e: CustomEvent) { + this.repoOwner = e.detail.value; + } + + private handleOwnerValueChanged(e: CustomEvent) { + this.repoOwnerId = e.detail.value as GroupId; + } + + private handleNameBindValueChanged(e: CustomEvent) { + this.repoConfig.name = e.detail.value as RepoName; + // nameChanged needs to be set before the event is fired, + // because when the event is fired, gr-repo-list gets + // the nameChanged value. + this.nameChanged = !!e.detail.value; + fireEvent(this, 'new-repo-name'); + this.requestUpdate(); + } + + private handleBranchNameBindValueChanged(e: CustomEvent) { + this.defaultBranch = e.detail.value as BranchName; + } + + private handleCreateEmptyCommitBindValueChanged(e: CustomEvent) { + this.repoConfig.create_empty_commit = e.detail.value; + this.requestUpdate(); + } + + private handlePermissionsOnlyBindValueChanged(e: CustomEvent) { + this.repoConfig.permissions_only = e.detail.value; + this.requestUpdate(); } }
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 deleted file mode 100644 index d0a6b7f..0000000 --- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts +++ /dev/null
@@ -1,103 +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"> - /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ - </style> - <style include="gr-form-styles"> - :host { - display: inline-block; - } - input { - width: 20em; - } - gr-autocomplete { - width: 20em; - } - </style> - - <div class="gr-form-styles"> - <div id="form"> - <section> - <span class="title">Repository name</span> - <iron-input bind-value="{{_repoConfig.name}}"> - <input id="repoNameInput" autocomplete="on" /> - </iron-input> - </section> - <section> - <span class="title">Default Branch</span> - <iron-input bind-value="{{_defaultBranch}}"> - <input id="defaultBranchNameInput" autocomplete="off" /> - </iron-input> - </section> - <section> - <span class="title">Rights inherit from</span> - <span class="value"> - <gr-autocomplete - id="rightsInheritFromInput" - text="{{_repoConfig.parent}}" - query="[[_query]]" - placeholder="Optional, defaults to 'All-Projects'" - > - </gr-autocomplete> - </span> - </section> - <section> - <span class="title">Owner</span> - <span class="value"> - <gr-autocomplete - id="ownerInput" - text="{{_repoOwner}}" - value="{{_repoOwnerId}}" - query="[[_queryGroups]]" - > - </gr-autocomplete> - </span> - </section> - <section> - <span class="title">Create initial empty commit</span> - <span class="value"> - <gr-select - id="initialCommit" - bind-value="{{_repoConfig.create_empty_commit}}" - > - <select> - <option value="false">False</option> - <option value="true">True</option> - </select> - </gr-select> - </span> - </section> - <section> - <span class="title">Only serve as parent for other repositories</span> - <span class="value"> - <gr-select - id="parentRepo" - bind-value="{{_repoConfig.permissions_only}}" - > - <select> - <option value="false">False</option> - <option value="true">True</option> - </select> - </gr-select> - </span> - </section> - </div> - </div> -`;
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 index 6485bae..d3e2171 100644 --- 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
@@ -18,26 +18,35 @@ 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 { + mockPromise, + queryAndAssert, + stubRestApi, +} from '../../../test/test-utils'; import {BranchName, GroupId, RepoName} from '../../../types/common'; +import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete'; +import {GrSelect} from '../../shared/gr-select/gr-select'; const basicFixture = fixtureFromElement('gr-create-repo-dialog'); suite('gr-create-repo-dialog tests', () => { let element: GrCreateRepoDialog; - setup(() => { + setup(async () => { element = basicFixture.instantiate(); + await element.updateComplete; }); test('default values are populated', () => { - assert.isTrue(element.$.initialCommit.bindValue); - assert.isFalse(element.$.parentRepo.bindValue); + assert.isTrue( + queryAndAssert<GrSelect>(element, '#initialCommit').bindValue + ); + assert.isFalse(queryAndAssert<GrSelect>(element, '#parentRepo').bindValue); }); test('repo created', async () => { const configInputObj = { - name: 'test-repo' as RepoName, + name: 'test-repo-new' as RepoName, create_empty_commit: true, parent: 'All-Project' as RepoName, permissions_only: false, @@ -47,27 +56,38 @@ Promise.resolve(new Response()) ); - assert.isFalse(element.hasNewRepoName); + const promise = mockPromise(); + element.addEventListener('new-repo-name', () => { + promise.resolve(); + }); - element._repoConfig = { + 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.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; + const repoNameInput = queryAndAssert<HTMLInputElement>( + element, + '#repoNameInput' + ); + repoNameInput.value = configInputObj.name; + repoNameInput.dispatchEvent( + new Event('input', {bubbles: true, composed: true}) + ); + queryAndAssert<GrAutocomplete>(element, '#rightsInheritFromInput').value = + configInputObj.parent; + queryAndAssert<GrSelect>(element, '#initialCommit').bindValue = + configInputObj.create_empty_commit; + queryAndAssert<GrSelect>(element, '#parentRepo').bindValue = + configInputObj.permissions_only; - assert.isTrue(element.hasNewRepoName); - - assert.deepEqual(element._repoConfig, configInputObj); + assert.deepEqual(element.repoConfig, configInputObj); await element.handleCreateRepo(); assert.isTrue( @@ -77,5 +97,10 @@ branches: ['main' as BranchName], }) ); + + await promise; + + assert.equal(element.repoConfig.name, configInputObj.name); + assert.equal(element.nameChanged, true); }); });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts index adcfb64..8e50b5f 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.ts
@@ -14,24 +14,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import '../../../styles/gr-table-styles'; -import '../../../styles/shared-styles'; import '../../shared/gr-dialog/gr-dialog'; import '../../shared/gr-list-view/gr-list-view'; import '../../shared/gr-overlay/gr-overlay'; import '../gr-create-repo-dialog/gr-create-repo-dialog'; -import {PolymerElement} from '@polymer/polymer/polymer-element'; -import {htmlTemplate} from './gr-repo-list_html'; import {GerritNav} from '../../core/gr-navigation/gr-navigation'; -import {customElement, property, observe, computed} from '@polymer/decorators'; import {AppElementAdminParams} from '../../gr-app-types'; import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; -import {RepoName, ProjectInfoWithName} from '../../../types/common'; +import { + RepoName, + ProjectInfoWithName, + WebLinkInfo, +} from '../../../types/common'; import {GrCreateRepoDialog} from '../gr-create-repo-dialog/gr-create-repo-dialog'; import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants'; import {fireTitleChange} from '../../../utils/event-util'; import {appContext} from '../../../services/app-context'; import {encodeURL, getBaseUrl} from '../../../utils/url-util'; +import {tableStyles} from '../../../styles/gr-table-styles'; +import {sharedStyles} from '../../../styles/shared-styles'; +import {LitElement, PropertyValues, css, html} from 'lit'; +import {customElement, property, query, state} from 'lit/decorators'; declare global { interface HTMLElementTagNameMap { @@ -39,151 +42,259 @@ } } -export interface GrRepoList { - $: { - createOverlay: GrOverlay; - createNewModal: GrCreateRepoDialog; - }; -} - @customElement('gr-repo-list') -export class GrRepoList extends PolymerElement { - static get template() { - return htmlTemplate; - } +export class GrRepoList extends LitElement { + @query('#createOverlay') + createOverlay?: GrOverlay; + + @query('#createNewModal') + createNewModal?: GrCreateRepoDialog; @property({type: Object}) params?: AppElementAdminParams; - @property({type: Number}) - _offset = 0; + @state() offset = 0; - @property({type: String}) - readonly _path = '/admin/repos'; + @state() newRepoName = false; - @property({type: Boolean}) - _hasNewRepoName = false; + @state() createNewCapability = false; - @property({type: Boolean}) - _createNewCapability = false; + @state() repos: ProjectInfoWithName[] = []; - @property({type: Array}) - _repos: ProjectInfoWithName[] = []; + @state() reposPerPage = 25; - @property({type: Number}) - _reposPerPage = 25; + @state() loading = true; - @property({type: Boolean}) - _loading = true; + @state() filter = ''; - @property({type: String}) - _filter = ''; - - @computed('_repos') - get _shownRepos() { - return this._repos.slice(0, SHOWN_ITEMS_COUNT); - } + @state() readonly path = '/admin/repos'; private readonly restApiService = appContext.restApiService; - override connectedCallback() { + override async connectedCallback() { super.connectedCallback(); - this._getCreateRepoCapability(); + await this.getCreateRepoCapability(); fireTitleChange(this, 'Repos'); - this._maybeOpenCreateOverlay(this.params); + this.maybeOpenCreateOverlay(this.params); } - @observe('params') - _paramsChanged(params: AppElementAdminParams) { - this._loading = true; - this._filter = params?.filter ?? ''; - this._offset = Number(params?.offset ?? 0); + static override get styles() { + return [ + tableStyles, + sharedStyles, + css` + .genericList tr td:last-of-type { + text-align: left; + } + .genericList tr th:last-of-type { + text-align: left; + } + .readOnly { + text-align: center; + } + .changesLink, + .name, + .repositoryBrowser, + .readOnly { + white-space: nowrap; + } + `, + ]; + } - return this._getRepos(this._filter, this._reposPerPage, this._offset); + override render() { + return html` + <gr-list-view + .createNew=${this.createNewCapability} + .filter=${this.filter} + .itemsPerPage=${this.reposPerPage} + .items=${this.repos} + .loading=${this.loading} + .offset=${this.offset} + .path=${this.path} + @create-clicked=${this.handleCreateClicked} + > + <table id="list" class="genericList"> + <tbody> + <tr class="headerRow"> + <th class="name topHeader">Repository Name</th> + <th class="repositoryBrowser topHeader">Repository Browser</th> + <th class="changesLink topHeader">Changes</th> + <th class="topHeader readOnly">Read only</th> + <th class="description topHeader">Repository Description</th> + </tr> + <tr + id="loading" + class="loadingMsg ${this.computeLoadingClass(this.loading)}" + > + <td>Loading...</td> + </tr> + </tbody> + <tbody class="${this.computeLoadingClass(this.loading)}"> + ${this.renderRepoList()} + </tbody> + </table> + </gr-list-view> + <gr-overlay id="createOverlay" with-backdrop> + <gr-dialog + id="createDialog" + class="confirmDialog" + ?disabled=${!this.newRepoName} + confirm-label="Create" + @confirm=${this.handleCreateRepo} + @cancel=${this.handleCloseCreate} + > + <div class="header" slot="header">Create Repository</div> + <div class="main" slot="main"> + <gr-create-repo-dialog + id="createNewModal" + @new-repo-name=${this.handleNewRepoName} + ></gr-create-repo-dialog> + </div> + </gr-dialog> + </gr-overlay> + `; + } + + private renderRepoList() { + const shownRepos = this.repos.slice(0, SHOWN_ITEMS_COUNT); + return shownRepos.map(item => this.renderRepo(item)); + } + + private renderRepo(item: ProjectInfoWithName) { + return html` + <tr class="table"> + <td class="name"> + <a href="${this.computeRepoUrl(item.name)}">${item.name}</a> + </td> + <td class="repositoryBrowser">${this.renderWebLinks(item)}</td> + <td class="changesLink"> + <a href="${this.computeChangesLink(item.name)}">view all</a> + </td> + <td class="readOnly"> + ${item.state === ProjectState.READ_ONLY ? 'Y' : ''} + </td> + <td class="description">${item.description}</td> + </tr> + `; + } + + private renderWebLinks(links: ProjectInfoWithName) { + const webLinks = links.web_links ? links.web_links : []; + return webLinks.map(link => this.renderWebLink(link)); + } + + private renderWebLink(link: WebLinkInfo) { + return html` + <a href="${link.url}" class="webLink" rel="noopener" target="_blank"> + ${link.name} + </a> + `; + } + + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('params')) { + this._paramsChanged(); + } + } + + async _paramsChanged() { + const params = this.params; + this.loading = true; + this.filter = params?.filter ?? ''; + this.offset = Number(params?.offset ?? 0); + + return await this.getRepos(); } /** - * Opens the create overlay if the route has a hash 'create' + * Opens the create overlay if the route has a hash 'create'. + * + * private but used in test */ - _maybeOpenCreateOverlay(params?: AppElementAdminParams) { + maybeOpenCreateOverlay(params?: AppElementAdminParams) { if (params?.openCreateModal) { - this.$.createOverlay.open(); + this.createOverlay?.open(); } } - _computeRepoUrl(name: string) { - return getBaseUrl() + this._path + '/' + encodeURL(name, true); + private computeRepoUrl(name: string) { + return `${getBaseUrl()}${this.path}/${encodeURL(name, true)}`; } - _computeChangesLink(name: string) { + private computeChangesLink(name: string) { return GerritNav.getUrlForProjectChanges(name as RepoName); } - _getCreateRepoCapability() { - return this.restApiService.getAccount().then(account => { - if (!account) { - return; - } - return this.restApiService - .getAccountCapabilities(['createProject']) - .then(capabilities => { - if (capabilities?.createProject) { - this._createNewCapability = true; - } - }); - }); - } + private async getCreateRepoCapability() { + const account = await this.restApiService.getAccount(); - _getRepos(filter: string, reposPerPage: number, offset?: number) { - this._repos = []; - return this.restApiService - .getRepos(filter, reposPerPage, offset) - .then(repos => { - // Late response. - if (filter !== this._filter || !repos) { - return; - } - this._repos = repos.filter(repo => - repo.name.toLowerCase().includes(filter.toLowerCase()) - ); - this._loading = false; - }); - } + if (!account) return; - _refreshReposList() { - this.restApiService.invalidateReposCache(); - return this._getRepos(this._filter, this._reposPerPage, this._offset); - } - - _handleCreateRepo() { - this.$.createNewModal.handleCreateRepo().then(() => { - this._refreshReposList(); - }); - } - - _handleCloseCreate() { - this.$.createOverlay.close(); - } - - _handleCreateClicked() { - this.$.createOverlay.open().then(() => { - this.$.createNewModal.focus(); - }); - } - - _readOnly(repo: ProjectInfoWithName) { - return repo.state === ProjectState.READ_ONLY ? 'Y' : ''; - } - - _computeWeblink(repo: ProjectInfoWithName) { - if (!repo.web_links) { - return ''; + const accountCapabilities = + await this.restApiService.getAccountCapabilities(['createProject']); + if (accountCapabilities?.createProject) { + this.createNewCapability = true; } - const webLinks = repo.web_links; - return webLinks.length ? webLinks : null; + + return account; } + /* private but used in test */ + async getRepos() { + this.repos = []; + + // We save the filter before getting the repos + // and then we check the value hasn't changed aftwards. + const filter = this.filter; + + const repos = await this.restApiService.getRepos( + this.filter, + this.reposPerPage, + this.offset + ); + + // Late response. + if (filter !== this.filter || !repos) return; + + this.repos = repos.filter(repo => + repo.name.toLowerCase().includes(filter.toLowerCase()) + ); + this.loading = false; + + return repos; + } + + private async refreshReposList() { + this.restApiService.invalidateReposCache(); + return await this.getRepos(); + } + + /* private but used in test */ + async handleCreateRepo() { + await this.createNewModal?.handleCreateRepo(); + await this.refreshReposList(); + } + + /* private but used in test */ + handleCloseCreate() { + this.createOverlay?.close(); + } + + /* private but used in test */ + handleCreateClicked() { + this.createOverlay?.open().then(() => { + this.createNewModal?.focus(); + }); + } + + /* private but used in test */ computeLoadingClass(loading: boolean) { return loading ? 'loading' : ''; } + + private handleNewRepoName() { + if (!this.createNewModal) return; + this.newRepoName = this.createNewModal.nameChanged; + } }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts deleted file mode 100644 index e1a7f48..0000000 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.ts +++ /dev/null
@@ -1,116 +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"> - /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ - </style> - <style include="gr-table-styles"> - /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ - </style> - <style> - .genericList tr td:last-of-type { - text-align: left; - } - .genericList tr th:last-of-type { - text-align: left; - } - .readOnly { - text-align: center; - } - .changesLink, - .name, - .repositoryBrowser, - .readOnly { - white-space: nowrap; - } - </style> - <gr-list-view - create-new="[[_createNewCapability]]" - filter="[[_filter]]" - items-per-page="[[_reposPerPage]]" - items="[[_repos]]" - loading="[[_loading]]" - offset="[[_offset]]" - on-create-clicked="_handleCreateClicked" - path="[[_path]]" - > - <table id="list" class="genericList"> - <tbody> - <tr class="headerRow"> - <th class="name topHeader">Repository Name</th> - <th class="repositoryBrowser topHeader">Repository Browser</th> - <th class="changesLink topHeader">Changes</th> - <th class="topHeader readOnly">Read only</th> - <th class="description topHeader">Repository Description</th> - </tr> - <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> - <td>Loading...</td> - </tr> - </tbody> - <tbody class$="[[computeLoadingClass(_loading)]]"> - <template is="dom-repeat" items="[[_shownRepos]]"> - <tr class="table"> - <td class="name"> - <a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a> - </td> - <td class="repositoryBrowser"> - <template - is="dom-repeat" - items="[[_computeWeblink(item)]]" - as="link" - > - <a - href$="[[link.url]]" - class="webLink" - rel="noopener" - target="_blank" - > - [[link.name]] - </a> - </template> - </td> - <td class="changesLink"> - <a href$="[[_computeChangesLink(item.name)]]">view all</a> - </td> - <td class="readOnly">[[_readOnly(item)]]</td> - <td class="description">[[item.description]]</td> - </tr> - </template> - </tbody> - </table> - </gr-list-view> - <gr-overlay id="createOverlay" with-backdrop=""> - <gr-dialog - id="createDialog" - class="confirmDialog" - disabled="[[!_hasNewRepoName]]" - confirm-label="Create" - on-confirm="_handleCreateRepo" - on-cancel="_handleCloseCreate" - > - <div class="header" slot="header">Create Repository</div> - <div class="main" slot="main"> - <gr-create-repo-dialog - has-new-repo-name="{{_hasNewRepoName}}" - id="createNewModal" - ></gr-create-repo-dialog> - </div> - </gr-dialog> - </gr-overlay> -`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js deleted file mode 100644 index 8fef4d0..0000000 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js +++ /dev/null
@@ -1,189 +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-repo-list.js'; -import {page} from '../../../utils/page-wrapper-utils.js'; -import 'lodash/lodash.js'; -import {stubRestApi} from '../../../test/test-utils.js'; - -const basicFixture = fixtureFromElement('gr-repo-list'); - -function createRepo(name, counter) { - return { - id: `${name}${counter}`, - name: `${name}`, - state: 'ACTIVE', - web_links: [ - { - name: 'diffusion', - url: `https://phabricator.example.org/r/project/${name}${counter}`, - }, - ], - }; -} - -let counter; -const repoGenerator = () => createRepo('test', ++counter); - -suite('gr-repo-list tests', () => { - let element; - let repos; - - let value; - - setup(() => { - sinon.stub(page, 'show'); - element = basicFixture.instantiate(); - counter = 0; - }); - - suite('list with repos', () => { - setup(async () => { - repos = _.times(26, repoGenerator); - stubRestApi('getRepos').returns(Promise.resolve(repos)); - await element._paramsChanged(value); - await flush(); - }); - - test('test for test repo in the list', async () => { - await flush(); - assert.equal(element._repos[1].id, 'test2'); - }); - - test('_shownRepos', () => { - assert.equal(element._shownRepos.length, 25); - }); - - test('_maybeOpenCreateOverlay', () => { - const overlayOpen = sinon.stub(element.$.createOverlay, 'open'); - element._maybeOpenCreateOverlay(); - assert.isFalse(overlayOpen.called); - const params = {}; - element._maybeOpenCreateOverlay(params); - assert.isFalse(overlayOpen.called); - params.openCreateModal = true; - element._maybeOpenCreateOverlay(params); - assert.isTrue(overlayOpen.called); - }); - }); - - suite('list with less then 25 repos', () => { - setup(async () => { - repos = _.times(25, repoGenerator); - stubRestApi('getRepos').returns(Promise.resolve(repos)); - await element._paramsChanged(value); - await flush(); - }); - - test('_shownRepos', () => { - assert.equal(element._shownRepos.length, 25); - }); - }); - - suite('filter', () => { - let reposFiltered; - setup(() => { - repos = _.times(25, repoGenerator); - reposFiltered = _.times(1, repoGenerator); - }); - - test('_paramsChanged', async () => { - const repoStub = stubRestApi('getRepos'); - repoStub.returns(Promise.resolve(repos)); - const value = { - filter: 'test', - offset: 25, - }; - await element._paramsChanged(value); - assert.isTrue(repoStub.lastCall.calledWithExactly('test', 25, 25)); - }); - - test('latest repos requested are always set', async () => { - const repoStub = stubRestApi('getRepos'); - repoStub.withArgs('test').returns(Promise.resolve(repos)); - repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered)); - element._filter = 'test'; - - // Repos are not set because the element._filter differs. - await element._getRepos('filter', 25, 0); - assert.deepEqual(element._repos, []); - }); - - test('filter is case insensitive', async () => { - const repoStub = stubRestApi('getRepos'); - const repos = [createRepo('aSDf', 0)]; - repoStub.withArgs('asdf').returns(Promise.resolve(repos)); - element._filter = 'asdf'; - await element._getRepos('asdf', 25, 0); - assert.equal(element._repos.length, 1); - }); - }); - - suite('loading', () => { - test('correct contents are displayed', () => { - assert.isTrue(element._loading); - assert.equal(element.computeLoadingClass(element._loading), 'loading'); - assert.equal(getComputedStyle(element.$.loading).display, 'block'); - - element._loading = false; - element._repos = _.times(25, repoGenerator); - - flush(); - assert.equal(element.computeLoadingClass(element._loading), ''); - assert.equal(getComputedStyle(element.$.loading).display, 'none'); - }); - }); - - suite('create new', () => { - test('_handleCreateClicked called when create-click fired', () => { - sinon.stub(element, '_handleCreateClicked'); - element.shadowRoot - .querySelector('gr-list-view').dispatchEvent( - new CustomEvent('create-clicked', { - composed: true, bubbles: true, - })); - assert.isTrue(element._handleCreateClicked.called); - }); - - test('_handleCreateClicked opens modal', () => { - const openStub = sinon.stub(element.$.createOverlay, 'open').returns( - Promise.resolve()); - element._handleCreateClicked(); - assert.isTrue(openStub.called); - }); - - test('_handleCreateRepo called when confirm fired', () => { - sinon.stub(element, '_handleCreateRepo'); - element.$.createDialog.dispatchEvent( - new CustomEvent('confirm', { - composed: true, bubbles: true, - })); - assert.isTrue(element._handleCreateRepo.called); - }); - - test('_handleCloseCreate called when cancel fired', () => { - sinon.stub(element, '_handleCloseCreate'); - element.$.createDialog.dispatchEvent( - new CustomEvent('cancel', { - composed: true, bubbles: true, - })); - assert.isTrue(element._handleCloseCreate.called); - }); - }); -}); -
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts new file mode 100644 index 0000000..142a838 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.ts
@@ -0,0 +1,246 @@ +/** + * @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-repo-list'; +import {GrRepoList} from './gr-repo-list'; +import {page} from '../../../utils/page-wrapper-utils'; +import { + mockPromise, + queryAndAssert, + stubRestApi, +} from '../../../test/test-utils'; +import { + UrlEncodedRepoName, + ProjectInfoWithName, + RepoName, +} from '../../../types/common'; +import {AppElementAdminParams} from '../../gr-app-types'; +import {ProjectState, SHOWN_ITEMS_COUNT} from '../../../constants/constants'; +import {GerritView} from '../../../services/router/router-model'; +import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; +import {GrDialog} from '../../shared/gr-dialog/gr-dialog'; +import {GrListView} from '../../shared/gr-list-view/gr-list-view'; + +const basicFixture = fixtureFromElement('gr-repo-list'); + +function createRepo(name: string, counter: number) { + return { + id: `${name}${counter}` as UrlEncodedRepoName, + name: `${name}` as RepoName, + state: 'ACTIVE' as ProjectState, + web_links: [ + { + name: 'diffusion', + url: `https://phabricator.example.org/r/project/${name}${counter}`, + }, + ], + }; +} + +function createRepoList(name: string, n: number) { + const repos = []; + for (let i = 0; i < n; ++i) { + repos.push(createRepo(name, i)); + } + return repos; +} + +suite('gr-repo-list tests', () => { + let element: GrRepoList; + let repos: ProjectInfoWithName[]; + + setup(async () => { + sinon.stub(page, 'show'); + element = basicFixture.instantiate(); + await element.updateComplete; + }); + + suite('list with repos', () => { + setup(async () => { + repos = createRepoList('test', 26); + stubRestApi('getRepos').returns(Promise.resolve(repos)); + await element._paramsChanged(); + await element.updateComplete; + }); + + test('test for test repo in the list', async () => { + await element.updateComplete; + assert.equal(element.repos[0].id, 'test0'); + assert.equal(element.repos[1].id, 'test1'); + assert.equal(element.repos[2].id, 'test2'); + }); + + test('shownRepos', () => { + assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25); + }); + + test('maybeOpenCreateOverlay', () => { + const overlayOpen = sinon.stub( + queryAndAssert<GrOverlay>(element, '#createOverlay'), + 'open' + ); + element.maybeOpenCreateOverlay(); + assert.isFalse(overlayOpen.called); + element.maybeOpenCreateOverlay(undefined); + assert.isFalse(overlayOpen.called); + const params: AppElementAdminParams = { + view: GerritView.ADMIN, + adminView: '', + openCreateModal: true, + }; + element.maybeOpenCreateOverlay(params); + assert.isTrue(overlayOpen.called); + }); + }); + + suite('list with less then 25 repos', () => { + setup(async () => { + repos = createRepoList('test', 25); + stubRestApi('getRepos').returns(Promise.resolve(repos)); + await element._paramsChanged(); + await element.updateComplete; + }); + + test('shownRepos', () => { + assert.equal(element.repos.slice(0, SHOWN_ITEMS_COUNT).length, 25); + }); + }); + + suite('filter', () => { + let reposFiltered: ProjectInfoWithName[]; + + setup(() => { + repos = createRepoList('test', 25); + reposFiltered = createRepoList('filter', 1); + }); + + test('_paramsChanged', async () => { + const repoStub = stubRestApi('getRepos'); + repoStub.returns(Promise.resolve(repos)); + element.params = { + view: GerritView.ADMIN, + adminView: '', + filter: 'test', + offset: 25, + } as AppElementAdminParams; + await element._paramsChanged(); + assert.isTrue(repoStub.lastCall.calledWithExactly('test', 25, 25)); + }); + + test('latest repos requested are always set', async () => { + const repoStub = stubRestApi('getRepos'); + const promise = mockPromise<ProjectInfoWithName[]>(); + repoStub.withArgs('filter', 25).returns(promise); + + element.filter = 'test'; + element.reposPerPage = 25; + element.offset = 0; + + // Repos are not set because the element.filter differs. + const p = element.getRepos(); + element.filter = 'filter'; + promise.resolve(reposFiltered); + await p; + assert.deepEqual(element.repos, []); + }); + + test('filter is case insensitive', async () => { + const repoStub = stubRestApi('getRepos'); + const repos = [createRepo('aSDf', 0)]; + repoStub.withArgs('asdf', 25).returns(Promise.resolve(repos)); + + element.filter = 'asdf'; + element.reposPerPage = 25; + element.offset = 0; + + await element.getRepos(); + assert.equal(element.repos.length, 1); + }); + }); + + suite('loading', () => { + test('correct contents are displayed', async () => { + assert.isTrue(element.loading); + assert.equal(element.computeLoadingClass(element.loading), 'loading'); + assert.equal( + getComputedStyle( + queryAndAssert<HTMLTableRowElement>(element, '#loading') + ).display, + 'block' + ); + + element.loading = false; + element.repos = createRepoList('test', 25); + + await element.updateComplete; + assert.equal(element.computeLoadingClass(element.loading), ''); + assert.equal( + getComputedStyle( + queryAndAssert<HTMLTableRowElement>(element, '#loading') + ).display, + 'none' + ); + }); + }); + + suite('create new', () => { + test('handleCreateClicked called when create-clicked fired', () => { + const handleCreateClickedStub = sinon.stub(); + element.addEventListener('create-clicked', handleCreateClickedStub); + queryAndAssert<GrListView>(element, 'gr-list-view').dispatchEvent( + new CustomEvent('create-clicked', { + composed: true, + bubbles: true, + }) + ); + assert.isTrue(handleCreateClickedStub.called); + }); + + test('handleCreateClicked opens modal', () => { + const openStub = sinon + .stub(queryAndAssert<GrOverlay>(element, '#createOverlay'), 'open') + .returns(Promise.resolve()); + element.handleCreateClicked(); + assert.isTrue(openStub.called); + }); + + test('handleCreateRepo called when confirm fired', () => { + const handleCreateRepoStub = sinon.stub(); + element.addEventListener('confirm', handleCreateRepoStub); + queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent( + new CustomEvent('confirm', { + composed: true, + bubbles: false, + }) + ); + assert.isTrue(handleCreateRepoStub.called); + }); + + test('handleCloseCreate called when cancel fired', () => { + const handleCloseCreateStub = sinon.stub(); + element.addEventListener('cancel', handleCloseCreateStub); + queryAndAssert<GrDialog>(element, '#createDialog').dispatchEvent( + new CustomEvent('cancel', { + composed: true, + bubbles: false, + }) + ); + assert.isTrue(handleCloseCreateStub.called); + }); + }); +});
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts index 3ac7c7b..4406a73 100644 --- a/polygerrit-ui/app/types/common.ts +++ b/polygerrit-ui/app/types/common.ts
@@ -206,6 +206,7 @@ UrlEncodedRepoName, UserConfigInfo, VotingRangeInfo, + WebLinkInfo, isDetailedLabelInfo, isQuickLabelInfo, };