diff options
Diffstat (limited to 'polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts')
-rw-r--r-- | polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts | 635 |
1 files changed, 430 insertions, 205 deletions
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts index 052e07a5e2..d72f916034 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts +++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.ts @@ -16,10 +16,7 @@ */ import '@polymer/iron-input/iron-input'; -import '../../../styles/gr-form-styles'; -import '../../../styles/gr-table-styles'; -import '../../../styles/shared-styles'; -import '../../shared/gr-account-link/gr-account-link'; +import '../../shared/gr-account-label/gr-account-label'; import '../../shared/gr-button/gr-button'; import '../../shared/gr-date-formatter/gr-date-formatter'; import '../../shared/gr-dialog/gr-dialog'; @@ -27,106 +24,356 @@ import '../../shared/gr-list-view/gr-list-view'; import '../../shared/gr-overlay/gr-overlay'; import '../gr-create-pointer-dialog/gr-create-pointer-dialog'; import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog'; -import {flush} from '@polymer/polymer/lib/legacy/polymer.dom'; -import {PolymerElement} from '@polymer/polymer/polymer-element'; -import {htmlTemplate} from './gr-repo-detail-list_html'; import {encodeURL} from '../../../utils/url-util'; -import {customElement, property} from '@polymer/decorators'; import {GrOverlay} from '../../shared/gr-overlay/gr-overlay'; import {GrCreatePointerDialog} from '../gr-create-pointer-dialog/gr-create-pointer-dialog'; import { - RepoName, - ProjectInfo, BranchInfo, + GitPersonInfo, GitRef, + ProjectInfo, + RepoName, TagInfo, - GitPersonInfo, + WebLinkInfo, } from '../../../types/common'; import {AppElementRepoParams} from '../../gr-app-types'; -import {PolymerDomRepeatEvent} from '../../../types/types'; import {RepoDetailView} from '../../core/gr-navigation/gr-navigation'; import {firePageError} from '../../../utils/event-util'; -import {appContext} from '../../../services/app-context'; +import {getAppContext} from '../../../services/app-context'; import {ErrorCallback} from '../../../api/rest'; import {SHOWN_ITEMS_COUNT} from '../../../constants/constants'; +import {formStyles} from '../../../styles/gr-form-styles'; +import {tableStyles} from '../../../styles/gr-table-styles'; +import {sharedStyles} from '../../../styles/shared-styles'; +import {LitElement, PropertyValues, css, html} from 'lit'; +import {customElement, query, property, state} from 'lit/decorators'; +import {BindValueChangeEvent} from '../../../types/events'; +import {assertIsDefined} from '../../../utils/common-util'; +import {ifDefined} from 'lit/directives/if-defined'; const PGP_START = '-----BEGIN PGP SIGNATURE-----'; -export interface GrRepoDetailList { - $: { - overlay: GrOverlay; - createOverlay: GrOverlay; - createNewModal: GrCreatePointerDialog; - }; -} - @customElement('gr-repo-detail-list') -export class GrRepoDetailList extends PolymerElement { - static get template() { - return htmlTemplate; - } - - @property({type: Object, observer: '_paramsChanged'}) - params?: AppElementRepoParams; - - @property({type: String}) - detailType?: RepoDetailView.BRANCHES | RepoDetailView.TAGS; - - @property({type: Boolean}) - _editing = false; +export class GrRepoDetailList extends LitElement { + @query('#overlay') private readonly overlay?: GrOverlay; - @property({type: Boolean}) - _isOwner = false; + @query('#createOverlay') private readonly createOverlay?: GrOverlay; - @property({type: Boolean}) - _loggedIn = false; + @query('#createNewModal') + private readonly createNewModal?: GrCreatePointerDialog; - @property({type: Number}) - _offset = 0; - - @property({type: String}) - _repo?: RepoName; - - @property({type: Array}) - _items?: BranchInfo[] | TagInfo[]; - - // _shownItems should be BranchInfo[] | TagInfo[], - // but TS incorrectly assumes that in the loop for(const item of _shownItems) - // item has type BranchInfo, not BranchInfo | TagInfo. - @property({type: Array, computed: 'computeShownItems(_items)'}) - _shownItems?: Array<BranchInfo | TagInfo>; - - @property({type: Number}) - _itemsPerPage = 25; - - @property({type: Boolean}) - _loading = true; + @property({type: Object}) + params?: AppElementRepoParams; - @property({type: String}) - _filter?: string; + // private but used in test + @state() detailType?: RepoDetailView.BRANCHES | RepoDetailView.TAGS; + + // private but used in test + @state() isOwner = false; + + @state() private loggedIn = false; + + @state() private offset = 0; + + // private but used in test + @state() repo?: RepoName; + + // private but used in test + @state() items?: BranchInfo[] | TagInfo[]; + + @state() private readonly itemsPerPage = 25; + + @state() private loading = true; + + @state() private filter?: string; + + @state() private refName?: GitRef; + + @state() private newItemName = false; + + // private but used in test + @state() isEditing = false; + + // private but used in test + @state() revisedRef?: GitRef; + + private readonly restApiService = getAppContext().restApiService; + + static override get styles() { + return [ + formStyles, + tableStyles, + sharedStyles, + css` + .tags td.name { + min-width: 25em; + } + td.name, + td.revision, + td.message { + word-break: break-word; + } + td.revision.tags { + width: 27em; + } + td.message, + td.tagger { + max-width: 15em; + } + .editing .editItem { + display: inherit; + } + .editItem, + .editing .editBtn, + .canEdit .revisionNoEditing, + .editing .revisionWithEditing, + .revisionEdit, + .hideItem { + display: none; + } + .revisionEdit gr-button { + margin-left: var(--spacing-m); + } + .editBtn { + margin-left: var(--spacing-l); + } + .canEdit .revisionEdit { + align-items: center; + display: flex; + } + .deleteButton:not(.show) { + display: none; + } + .tagger.hide { + display: none; + } + `, + ]; + } - @property({type: String}) - _refName?: GitRef; + override render() { + return html` + <gr-list-view + .createNew=${this.loggedIn} + .filter=${this.filter} + .itemsPerPage=${this.itemsPerPage} + .items=${this.items} + .loading=${this.loading} + .offset=${this.offset} + .path=${this.getPath(this.repo, this.detailType)} + @create-clicked=${() => { + this.handleCreateClicked(); + }} + > + <table id="list" class="genericList gr-form-styles"> + <tbody> + <tr class="headerRow"> + <th class="name topHeader">Name</th> + <th class="revision topHeader">Revision</th> + <th + class="message topHeader ${this.detailType === + RepoDetailView.BRANCHES + ? 'hideItem' + : ''}" + > + Message + </th> + <th + class="tagger topHeader ${this.detailType === + RepoDetailView.BRANCHES + ? 'hideItem' + : ''}" + > + Tagger + </th> + <th class="repositoryBrowser topHeader">Repository Browser</th> + <th class="delete topHeader"></th> + </tr> + <tr + id="loading" + class="loadingMsg ${this.loading ? 'loading' : ''}" + > + <td>Loading...</td> + </tr> + </tbody> + <tbody class=${this.loading ? 'loading' : ''}> + ${this.items + ?.slice(0, SHOWN_ITEMS_COUNT) + .map((item, index) => this.renderItemList(item, index))} + </tbody> + </table> + <gr-overlay id="overlay" with-backdrop> + <gr-confirm-delete-item-dialog + class="confirmDialog" + .item=${this.refName} + .itemTypeName=${this.computeItemName(this.detailType)} + @confirm=${() => this.handleDeleteItemConfirm()} + @cancel=${() => { + this.handleConfirmDialogCancel(); + }} + ></gr-confirm-delete-item-dialog> + </gr-overlay> + </gr-list-view> + <gr-overlay id="createOverlay" with-backdrop> + <gr-dialog + id="createDialog" + ?disabled=${!this.newItemName} + confirm-label="Create" + @confirm=${() => { + this.handleCreateItem(); + }} + @cancel=${() => { + this.handleCloseCreate(); + }} + > + <div class="header" slot="header"> + Create ${this.computeItemName(this.detailType)} + </div> + <div class="main" slot="main"> + <gr-create-pointer-dialog + id="createNewModal" + .detailType=${this.computeItemName(this.detailType)} + .itemDetail=${this.detailType} + .repoName=${this.repo} + @update-item-name=${() => { + this.handleUpdateItemName(); + }} + ></gr-create-pointer-dialog> + </div> + </gr-dialog> + </gr-overlay> + `; + } - @property({type: Boolean}) - _hasNewItemName = false; + private renderItemList(item: BranchInfo | TagInfo, index: number) { + return html` + <tr class="table"> + <td class="${this.detailType} name"> + <a href=${ifDefined(this.computeFirstWebLink(item))}> + ${this.stripRefs(item.ref, this.detailType)} + </a> + </td> + <td + class="${this.detailType} revision ${this.computeCanEditClass( + item.ref, + this.detailType, + this.isOwner + )}" + > + <span class="revisionNoEditing"> ${item.revision} </span> + <span class="revisionEdit ${this.isEditing ? 'editing' : ''}"> + <span class="revisionWithEditing"> ${item.revision} </span> + <gr-button + class="editBtn" + link + data-index=${index} + @click=${() => { + this.handleEditRevision(index); + }} + > + edit + </gr-button> + <iron-input + class="editItem" + .bindValue=${this.revisedRef} + @bind-value-changed=${this.handleRevisedRefBindValueChanged} + > + <input /> + </iron-input> + <gr-button + class="cancelBtn editItem" + link + @click=${() => { + this.handleCancelRevision(); + }} + > + Cancel + </gr-button> + <gr-button + class="saveBtn editItem" + link + data-index=${index} + ?disabled=${!this.revisedRef} + @click=${() => { + this.handleSaveRevision(index); + }} + > + Save + </gr-button> + </span> + </td> + <td + class="message ${this.detailType === RepoDetailView.BRANCHES + ? 'hideItem' + : ''}" + > + ${(item as TagInfo)?.message + ? (item as TagInfo).message?.split(PGP_START)[0] + : ''} + </td> + <td + class="tagger ${this.detailType === RepoDetailView.BRANCHES + ? 'hideItem' + : ''}" + > + ${this.renderTagger((item as TagInfo).tagger)} + </td> + <td class="repositoryBrowser"> + ${this.computeWeblink(item).map(link => this.renderWeblink(link))} + </td> + <td class="delete"> + <gr-button + class="deleteButton ${item.can_delete ? 'show' : ''}" + link + data-index=${index} + @click=${() => { + this.handleDeleteItem(index); + }} + > + Delete + </gr-button> + </td> + </tr> + `; + } - @property({type: Boolean}) - _isEditing = false; + private renderTagger(tagger?: GitPersonInfo) { + if (!tagger) return; + + return html` + <div class="tagger"> + <gr-account-label .account=${tagger} clickable> </gr-account-label> + (<gr-date-formatter withTooltip .dateStr=${tagger.date}> + </gr-date-formatter + >) + </div> + `; + } - @property({type: String}) - _revisedRef?: GitRef; + private renderWeblink(link: WebLinkInfo) { + return html` + <a href=${link.url} class="webLink" rel="noopener" target="_blank"> + (${link.name}) + </a> + `; + } - private readonly restApiService = appContext.restApiService; + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('params')) { + this.paramsChanged(); + } + } - _determineIfOwner(repo: RepoName) { + // private but used in test + determineIfOwner(repo: RepoName) { return this.restApiService .getRepoAccess(repo) - .then(access => (this._isOwner = !!access?.[repo]?.is_owner)); + .then(access => (this.isOwner = !!access?.[repo]?.is_owner)); } - _paramsChanged(params?: AppElementRepoParams) { - if (!params?.repo) { + // private but used in test + paramsChanged() { + if (!this.params?.repo) { return Promise.reject(new Error('undefined repo')); } @@ -134,40 +381,40 @@ export class GrRepoDetailList extends PolymerElement { // to false and polymer removes this component, hence check for params if ( !( - params?.detail === RepoDetailView.BRANCHES || - params?.detail === RepoDetailView.TAGS + this.params?.detail === RepoDetailView.BRANCHES || + this.params?.detail === RepoDetailView.TAGS ) ) { return; } - this._repo = params.repo; + this.repo = this.params.repo; - this._getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; - if (loggedIn && this._repo) { - this._determineIfOwner(this._repo); + this.getLoggedIn().then(loggedIn => { + this.loggedIn = loggedIn; + if (loggedIn && this.repo) { + this.determineIfOwner(this.repo); } }); - this.detailType = params.detail; + this.detailType = this.params.detail; - this._filter = params?.filter ?? ''; - this._offset = Number(params?.offset ?? 0); + this.filter = this.params?.filter ?? ''; + this.offset = Number(this.params?.offset ?? 0); if (!this.detailType) return Promise.reject(new Error('undefined detailType')); - return this._getItems( - this._filter, - this._repo, - this._itemsPerPage, - this._offset, + return this.getItems( + this.filter, + this.repo, + this.itemsPerPage, + this.offset, this.detailType ); } // TODO(TS) Move this to object for easier read, understand. - _getItems( + private getItems( filter: string | undefined, repo: RepoName | undefined, itemsPerPage: number, @@ -177,9 +424,9 @@ export class GrRepoDetailList extends PolymerElement { if (filter === undefined || !repo || offset === undefined) { return Promise.reject(new Error('filter or repo or offset undefined')); } - this._loading = true; - this._items = []; - flush(); + this.loading = true; + this.items = []; + const errFn: ErrorCallback = response => { firePageError(response); }; @@ -188,52 +435,42 @@ export class GrRepoDetailList extends PolymerElement { return this.restApiService .getRepoBranches(filter, repo, itemsPerPage, offset, errFn) .then(items => { - if (!items) { - return; - } - this._items = items; - this._loading = false; + this.items = items ?? []; + this.loading = false; + }) + .finally(() => { + this.loading = false; }); } else if (detailType === RepoDetailView.TAGS) { return this.restApiService .getRepoTags(filter, repo, itemsPerPage, offset, errFn) .then(items => { - if (!items) { - return; - } - this._items = items; - this._loading = false; + this.items = items ?? []; + }) + .finally(() => { + this.loading = false; }); } return Promise.reject(new Error('unknown detail type')); } - _getPath(repo?: RepoName, detailType?: RepoDetailView) { + private getPath(repo?: RepoName, detailType?: RepoDetailView) { return `/admin/repos/${encodeURL(repo ?? '', false)},${detailType}`; } - _computeWeblink(repo: ProjectInfo | BranchInfo | TagInfo) { - if (!repo.web_links) { - return ''; - } + private computeWeblink(repo: ProjectInfo | BranchInfo | TagInfo) { + if (!repo.web_links) return []; const webLinks = repo.web_links; - return webLinks.length ? webLinks : null; + return webLinks.length ? webLinks : []; } - _computeFirstWebLink(repo: ProjectInfo | BranchInfo | TagInfo) { - const webLinks = this._computeWeblink(repo); - return webLinks ? webLinks[0].url : null; + private computeFirstWebLink(repo: ProjectInfo | BranchInfo | TagInfo) { + const webLinks = this.computeWeblink(repo); + return webLinks.length > 0 ? webLinks[0].url : undefined; } - _computeMessage(message?: string) { - if (!message) { - return; - } - // Strip PGP info. - return message.split(PGP_START)[0]; - } - - _stripRefs(item: GitRef, detailType?: RepoDetailView) { + // private but used in test + stripRefs(item: GitRef, detailType?: RepoDetailView) { if (detailType === RepoDetailView.BRANCHES) { return item.replace('refs/heads/', ''); } else if (detailType === RepoDetailView.TAGS) { @@ -242,62 +479,61 @@ export class GrRepoDetailList extends PolymerElement { throw new Error('unknown detailType'); } - _getLoggedIn() { + // private but used in test + getLoggedIn() { return this.restApiService.getLoggedIn(); } - _computeEditingClass(isEditing: boolean) { - return isEditing ? 'editing' : ''; - } - - _computeCanEditClass( + private computeCanEditClass( ref?: GitRef, detailType?: RepoDetailView, isOwner?: boolean ) { if (ref === undefined || detailType === undefined) return ''; - return isOwner && this._stripRefs(ref, detailType) === 'HEAD' + return isOwner && this.stripRefs(ref, detailType) === 'HEAD' ? 'canEdit' : ''; } - _handleEditRevision(e: PolymerDomRepeatEvent<BranchInfo | TagInfo>) { - this._revisedRef = e.model.get('item.revision') as unknown as GitRef; - this._isEditing = true; + private handleEditRevision(index: number) { + if (!this.items) return; + + this.revisedRef = this.items[index].revision as GitRef; + this.isEditing = true; } - _handleCancelRevision() { - this._isEditing = false; + private handleCancelRevision() { + this.isEditing = false; } - _handleSaveRevision(e: PolymerDomRepeatEvent<BranchInfo | TagInfo>) { - if (this._revisedRef && this._repo) - this._setRepoHead(this._repo, this._revisedRef, e); + // private but used in test + handleSaveRevision(index: number) { + if (this.revisedRef && this.repo) + this.setRepoHead(this.repo, this.revisedRef, index); } - _setRepoHead( - repo: RepoName, - ref: GitRef, - e: PolymerDomRepeatEvent<BranchInfo | TagInfo> - ) { + // private but used in test + setRepoHead(repo: RepoName, ref: GitRef, index: number) { + if (!this.items) return; return this.restApiService.setRepoHead(repo, ref).then(res => { if (res.status < 400) { - this._isEditing = false; - e.model.set('item.revision', ref); - // This is needed to refresh _items property with fresh data, + this.isEditing = false; + this.items![index].revision = ref; + // This is needed to refresh 'items' property with fresh data, // specifically can_delete from the json response. - this._getItems( - this._filter, - this._repo, - this._itemsPerPage, - this._offset, + this.getItems( + this.filter, + this.repo, + this.itemsPerPage, + this.offset, this.detailType! ); } }); } - _computeItemName(detailType?: RepoDetailView) { + // private but used in test + computeItemName(detailType?: RepoDetailView) { if (detailType === undefined) return ''; if (detailType === RepoDetailView.BRANCHES) { return 'Branch'; @@ -307,35 +543,36 @@ export class GrRepoDetailList extends PolymerElement { throw new Error('unknown detailType'); } - _handleDeleteItemConfirm() { - this.$.overlay.close(); - if (!this._repo || !this._refName) { + private handleDeleteItemConfirm() { + assertIsDefined(this.overlay, 'overlay'); + this.overlay.close(); + if (!this.repo || !this.refName) { return Promise.reject(new Error('undefined repo or refName')); } if (this.detailType === RepoDetailView.BRANCHES) { return this.restApiService - .deleteRepoBranches(this._repo, this._refName) + .deleteRepoBranches(this.repo, this.refName) .then(itemDeleted => { if (itemDeleted.status === 204) { - this._getItems( - this._filter, - this._repo, - this._itemsPerPage, - this._offset, + this.getItems( + this.filter, + this.repo, + this.itemsPerPage, + this.offset, this.detailType! ); } }); } else if (this.detailType === RepoDetailView.TAGS) { return this.restApiService - .deleteRepoTags(this._repo, this._refName) + .deleteRepoTags(this.repo, this.refName) .then(itemDeleted => { if (itemDeleted.status === 204) { - this._getItems( - this._filter, - this._repo, - this._itemsPerPage, - this._offset, + this.getItems( + this.filter, + this.repo, + this.itemsPerPage, + this.offset, this.detailType! ); } @@ -344,61 +581,49 @@ export class GrRepoDetailList extends PolymerElement { return Promise.reject(new Error('unknown detail type')); } - _handleConfirmDialogCancel() { - this.$.overlay.close(); + private handleConfirmDialogCancel() { + assertIsDefined(this.overlay, 'overlay'); + this.overlay.close(); } - _handleDeleteItem(e: PolymerDomRepeatEvent<BranchInfo | TagInfo>) { - const name = this._stripRefs( - e.model.get('item.ref'), + private handleDeleteItem(index: number) { + if (!this.items) return; + assertIsDefined(this.overlay, 'overlay'); + const name = this.stripRefs( + this.items[index].ref, this.detailType ) as GitRef; - if (!name) { - return; - } - this._refName = name; - this.$.overlay.open(); - } - - _computeHideDeleteClass(owner?: boolean, canDelete?: boolean) { - if (canDelete || owner) { - return 'show'; - } - - return ''; + if (!name) return; + this.refName = name; + this.overlay.open(); } - _handleCreateItem() { - this.$.createNewModal.handleCreateItem(); - this._handleCloseCreate(); + // private but used in test + handleCreateItem() { + assertIsDefined(this.createNewModal, 'createNewModal'); + this.createNewModal.handleCreateItem(); + this.handleCloseCreate(); } - _handleCloseCreate() { - this.$.createOverlay.close(); - } - - _handleCreateClicked() { - this.$.createOverlay.open(); - } - - _hideIfBranch(type?: RepoDetailView) { - if (type === RepoDetailView.BRANCHES) { - return 'hideItem'; - } - - return ''; + // private but used in test + handleCloseCreate() { + assertIsDefined(this.createOverlay, 'createOverlay'); + this.createOverlay.close(); } - _computeHideTagger(tagger?: GitPersonInfo) { - return tagger ? '' : 'hide'; + // private but used in test + handleCreateClicked() { + assertIsDefined(this.createOverlay, 'createOverlay'); + this.createOverlay.open(); } - computeLoadingClass(loading: boolean) { - return loading ? 'loading' : ''; + private handleUpdateItemName() { + assertIsDefined(this.createNewModal, 'createNewModal'); + this.newItemName = !!this.createNewModal.itemName; } - computeShownItems(items: BranchInfo[] | TagInfo[]) { - return items.slice(0, SHOWN_ITEMS_COUNT); + private handleRevisedRefBindValueChanged(e: BindValueChangeEvent) { + this.revisedRef = e.detail.value as GitRef; } } |