diff options
Diffstat (limited to 'polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts')
-rw-r--r-- | polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts new file mode 100644 index 0000000000..85ea644e5d --- /dev/null +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-section/gr-change-list-section.ts @@ -0,0 +1,351 @@ +/** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {LitElement, html, css, PropertyValues} from 'lit'; +import {customElement, property, state} from 'lit/decorators'; +import {ChangeListSection} from '../gr-change-list/gr-change-list'; +import '../gr-change-list-action-bar/gr-change-list-action-bar'; +import { + CLOSED, + YOUR_TURN, + GerritNav, +} from '../../core/gr-navigation/gr-navigation'; +import {KnownExperimentId} from '../../../services/flags/flags'; +import {getAppContext} from '../../../services/app-context'; +import {ChangeInfo, ServerInfo, AccountInfo} from '../../../api/rest-api'; +import {changeListStyles} from '../../../styles/gr-change-list-styles'; +import {fontStyles} from '../../../styles/gr-font-styles'; +import {sharedStyles} from '../../../styles/shared-styles'; +import {Metadata} from '../../../utils/change-metadata-util'; +import {WAITING} from '../../../constants/constants'; +import {ifDefined} from 'lit/directives/if-defined'; +import {provide} from '../../../models/dependency'; +import { + bulkActionsModelToken, + BulkActionsModel, +} from '../../../models/bulk-actions/bulk-actions-model'; +import {subscribe} from '../../lit/subscription-controller'; + +const NUMBER_FIXED_COLUMNS = 3; +const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--'; +const MAX_SHORTCUT_CHARS = 5; +const INVALID_TOKENS = ['limit:', 'age:', '-age:']; + +export function computeLabelShortcut(labelName: string) { + if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) { + labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length); + } + // Compute label shortcut by splitting token by - and capitalizing first + // letter of each token. + return labelName + .split('-') + .reduce((previousValue, currentValue) => { + if (!currentValue) { + return previousValue; + } + return previousValue + currentValue[0].toUpperCase(); + }, '') + .slice(0, MAX_SHORTCUT_CHARS); +} + +@customElement('gr-change-list-section') +export class GrChangeListSection extends LitElement { + @property({type: Array}) + visibleChangeTableColumns?: string[]; + + @property({type: Boolean}) + showStar = false; + + @property({type: Boolean}) + showNumber?: boolean; // No default value to prevent flickering. + + @property({type: Number}) + selectedIndex?: number; // The relative index of the change that is selected + + @property({type: Array}) + labelNames: string[] = []; + + @property({type: Array}) + dynamicHeaderEndpoints?: string[]; + + @property({type: Object}) + changeSection!: ChangeListSection; + + @property({type: Object}) + config?: ServerInfo; + + @property({type: Boolean}) + isCursorMoving = false; + + /** + * The logged-in user's account, or an empty object if no user is logged + * in. + */ + @property({type: Object}) + account: AccountInfo | undefined = undefined; + + @state() showBulkActionsHeader = false; + + private readonly flagsService = getAppContext().flagsService; + + bulkActionsModel: BulkActionsModel = new BulkActionsModel( + getAppContext().restApiService + ); + + static override get styles() { + return [ + changeListStyles, + fontStyles, + sharedStyles, + css` + :host { + display: contents; + } + .section-count-label { + color: var(--deemphasized-text-color); + font-family: var(--font-family); + font-size: var(--font-size-small); + font-weight: var(--font-weight-normal); + line-height: var(--line-height-small); + } + `, + ]; + } + + constructor() { + super(); + provide(this, bulkActionsModelToken, () => this.bulkActionsModel); + } + + override connectedCallback() { + super.connectedCallback(); + subscribe( + this, + this.bulkActionsModel.selectedChangeNums$, + selectedChanges => + (this.showBulkActionsHeader = selectedChanges.length > 0) + ); + } + + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('changeSection')) { + // In case the list of changes is updated due to auto reloading, we want + // to ensure the model removes any stale change that is not a part of the + // new section changes. + if (this.flagsService.isEnabled(KnownExperimentId.BULK_ACTIONS)) { + this.bulkActionsModel.sync(this.changeSection.results); + } + } + } + + override render() { + const columns = this.computeColumns(); + const colSpan = this.computeColspan(columns); + return html` + ${this.renderSectionHeader(colSpan)} + <tbody class="groupContent"> + ${this.isEmpty() + ? this.renderNoChangesRow(colSpan) + : this.renderColumnHeaders(columns)} + ${this.changeSection.results.map((change, index) => + this.renderChangeRow(change, index, columns) + )} + </tbody> + `; + } + + private renderNoChangesRow(colSpan: number) { + return html` + <tr class="noChanges"> + <td class="leftPadding" aria-hidden="true"></td> + <td + class="star" + ?aria-hidden=${!this.showStar} + ?hidden=${!this.showStar} + ></td> + <td class="cell" colspan=${colSpan}> + ${this.changeSection.emptyStateSlotName + ? html`<slot name=${this.changeSection.emptyStateSlotName}></slot>` + : 'No changes'} + </td> + </tr> + `; + } + + private renderSectionHeader(colSpan: number) { + if ( + this.changeSection.name === undefined || + this.changeSection.countLabel === undefined || + this.changeSection.query === undefined + ) + return; + + return html` + <tbody> + <tr class="groupHeader"> + <td aria-hidden="true" class="leftPadding"></td> + ${this.renderSelectionHeader()} + <td aria-hidden="true" class="star" ?hidden=${!this.showStar}></td> + <td class="cell" colspan=${colSpan}> + <h2 class="heading-3"> + <a + href=${this.sectionHref(this.changeSection.query)} + class="section-title" + > + <span class="section-name">${this.changeSection.name}</span> + <span class="section-count-label" + >${this.changeSection.countLabel}</span + > + </a> + </h2> + </td> + </tr> + </tbody> + `; + } + + private renderColumnHeaders(columns: string[]) { + return html` + <tr class="groupTitle"> + ${this.showBulkActionsHeader && + this.flagsService.isEnabled(KnownExperimentId.BULK_ACTIONS) + ? html`<gr-change-list-action-bar></gr-change-list-action-bar>` + : html` <td class="leftPadding" aria-hidden="true"></td> + ${this.renderSelectionHeader()} + <td + class="star" + aria-label="Star status column" + ?hidden=${!this.showStar} + ></td> + <td class="number" ?hidden=${!this.showNumber}>#</td> + ${columns.map(item => this.renderHeaderCell(item))} + ${this.labelNames?.map(labelName => + this.renderLabelHeader(labelName) + )} + ${this.dynamicHeaderEndpoints?.map(pluginHeader => + this.renderEndpointHeader(pluginHeader) + )}`} + </tr> + `; + } + + private renderSelectionHeader() { + if (!this.flagsService.isEnabled(KnownExperimentId.BULK_ACTIONS)) return; + return html`<td aria-hidden="true" class="selection"></td>`; + } + + private renderHeaderCell(item: string) { + return html`<td class=${item.toLowerCase()}>${item}</td>`; + } + + private renderLabelHeader(labelName: string) { + return html` + <td class="label" title=${labelName}> + ${computeLabelShortcut(labelName)} + </td> + `; + } + + private renderEndpointHeader(pluginHeader: string) { + return html` + <td class="endpoint"> + <gr-endpoint-decorator .name=${pluginHeader}></gr-endpoint-decorator> + </td> + `; + } + + private renderChangeRow( + change: ChangeInfo, + index: number, + columns: string[] + ) { + const ariaLabel = this.computeAriaLabel(change); + const selected = this.computeItemSelected(index); + const tabindex = this.computeTabIndex(index); + return html` + <gr-change-list-item + .account=${this.account} + ?selected=${selected} + .change=${change} + .config=${this.config} + .sectionName=${this.changeSection.name} + .visibleChangeTableColumns=${columns} + .showNumber=${this.showNumber} + ?showStar=${this.showStar} + tabindex=${ifDefined(tabindex)} + .labelNames=${this.labelNames} + aria-label=${ariaLabel} + ></gr-change-list-item> + `; + } + + /** + * This methods allows us to customize the columns per section. + * Private but used in test + * + */ + computeColumns() { + const section = this.changeSection; + if (!section || !this.visibleChangeTableColumns) return []; + const cols = [...this.visibleChangeTableColumns]; + const updatedIndex = cols.indexOf(Metadata.UPDATED); + if (section.name === YOUR_TURN.name && updatedIndex !== -1) { + cols[updatedIndex] = WAITING; + } + if (section.name === CLOSED.name && updatedIndex !== -1) { + cols[updatedIndex] = Metadata.SUBMITTED; + } + return cols; + } + + // private but used in test + computeItemSelected(index: number) { + return index === this.selectedIndex; + } + + private computeTabIndex(index: number) { + if (this.isCursorMoving) return 0; + return this.computeItemSelected(index) ? 0 : undefined; + } + + // private but used in test + computeColspan(cols: string[]) { + if (!cols || !this.labelNames) return 1; + return cols.length + this.labelNames.length + NUMBER_FIXED_COLUMNS; + } + + // private but used in test + processQuery(query: string) { + let tokens = query.split(' '); + tokens = tokens.filter( + token => + !INVALID_TOKENS.some(invalidToken => token.startsWith(invalidToken)) + ); + return tokens.join(' '); + } + + private sectionHref(query?: string) { + if (!query) return; + return GerritNav.getUrlForSearchQuery(this.processQuery(query)); + } + + // private but used in test + isEmpty() { + return !this.changeSection.results?.length; + } + + private computeAriaLabel(change?: ChangeInfo) { + const sectionName = this.changeSection.name; + if (!change) return ''; + return change.subject + (sectionName ? `, section: ${sectionName}` : ''); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'gr-change-list-section': GrChangeListSection; + } +} |