| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| /* eslint-disable @devtools/no-lit-render-outside-of-view, @devtools/enforce-custom-element-definitions-location */ |
| |
| import '../icon_button/icon_button.js'; |
| |
| import * as Common from '../../../core/common/common.js'; |
| import * as i18n from '../../../core/i18n/i18n.js'; |
| import * as IssuesManager from '../../../models/issues_manager/issues_manager.js'; |
| import type * as IconButton from '../../../ui/components/icon_button/icon_button.js'; |
| import {html, render} from '../../lit/lit.js'; |
| |
| import issueCounterStyles from './issueCounter.css.js'; |
| |
| const UIStrings = { |
| /** |
| * @description Label for link to Issues tab, specifying how many issues there are. |
| */ |
| pageErrors: '{issueCount, plural, =1 {# page error} other {# page errors}}', |
| /** |
| * @description Label for link to Issues tab, specifying how many issues there are. |
| */ |
| breakingChanges: '{issueCount, plural, =1 {# breaking change} other {# breaking changes}}', |
| /** |
| * @description Label for link to Issues tab, specifying how many issues there are. |
| */ |
| possibleImprovements: '{issueCount, plural, =1 {# possible improvement} other {# possible improvements}}', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('ui/components/issue_counter/IssueCounter.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export function getIssueKindIconName(issueKind: IssuesManager.Issue.IssueKind): string { |
| switch (issueKind) { |
| case IssuesManager.Issue.IssueKind.PAGE_ERROR: |
| return 'issue-cross-filled'; |
| case IssuesManager.Issue.IssueKind.BREAKING_CHANGE: |
| return 'issue-exclamation-filled'; |
| case IssuesManager.Issue.IssueKind.IMPROVEMENT: |
| return 'issue-text-filled'; |
| } |
| } |
| |
| function toIconGroup(iconName: string, sizeOverride?: string): IconButton.IconButton.IconWithTextData { |
| if (sizeOverride) { |
| return {iconName, iconWidth: sizeOverride, iconHeight: sizeOverride}; |
| } |
| return {iconName}; |
| } |
| |
| export const enum DisplayMode { |
| OMIT_EMPTY = 'OmitEmpty', |
| SHOW_ALWAYS = 'ShowAlways', |
| ONLY_MOST_IMPORTANT = 'OnlyMostImportant', |
| } |
| |
| export interface IssueCounterData { |
| clickHandler?: () => void; |
| tooltipCallback?: () => void; |
| leadingText?: string; |
| displayMode?: DisplayMode; |
| issuesManager: IssuesManager.IssuesManager.IssuesManager; |
| throttlerTimeout?: number; |
| accessibleName?: string; |
| compact?: boolean; |
| } |
| |
| // Lazily instantiate the formatter as the constructor takes 50ms+ |
| // TODO: move me and others like me to i18n module |
| const listFormatter = (function defineFormatter() { |
| let intlListFormat: Intl.ListFormat; |
| return { |
| format(...args: Parameters<Intl.ListFormat['format']>): ReturnType<Intl.ListFormat['format']> { |
| if (!intlListFormat) { |
| const opts: Intl.ListFormatOptions = {type: 'unit', style: 'short'}; |
| intlListFormat = new Intl.ListFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, opts); |
| } |
| return intlListFormat.format(...args); |
| }, |
| }; |
| })(); |
| |
| export function getIssueCountsEnumeration( |
| issuesManager: IssuesManager.IssuesManager.IssuesManager, omitEmpty = true): string { |
| const counts: [number, number, number] = [ |
| issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.PAGE_ERROR), |
| issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.BREAKING_CHANGE), |
| issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.IMPROVEMENT), |
| ]; |
| const phrases = [ |
| i18nString(UIStrings.pageErrors, {issueCount: counts[0]}), |
| i18nString(UIStrings.breakingChanges, {issueCount: counts[1]}), |
| i18nString(UIStrings.possibleImprovements, {issueCount: counts[2]}), |
| ]; |
| return listFormatter.format(phrases.filter((_, i) => omitEmpty ? counts[i] > 0 : true)); |
| } |
| |
| export class IssueCounter extends HTMLElement { |
| readonly #shadow = this.attachShadow({mode: 'open'}); |
| #clickHandler?: () => void; |
| #tooltipCallback?: () => void; |
| #leadingText = ''; |
| #throttler?: Common.Throttler.Throttler; |
| #counts: [number, number, number] = [0, 0, 0]; |
| #displayMode: DisplayMode = DisplayMode.OMIT_EMPTY; |
| #issuesManager?: IssuesManager.IssuesManager.IssuesManager; |
| #accessibleName?: string; |
| #throttlerTimeout?: number; |
| #compact = false; |
| |
| scheduleUpdate(): void { |
| if (this.#throttler) { |
| void this.#throttler.schedule(async () => this.#render()); |
| } else { |
| this.#render(); |
| } |
| } |
| |
| set data(data: IssueCounterData) { |
| this.#clickHandler = data.clickHandler; |
| this.#leadingText = data.leadingText ?? ''; |
| this.#tooltipCallback = data.tooltipCallback; |
| this.#displayMode = data.displayMode ?? DisplayMode.OMIT_EMPTY; |
| this.#accessibleName = data.accessibleName; |
| this.#throttlerTimeout = data.throttlerTimeout; |
| this.#compact = Boolean(data.compact); |
| if (this.#issuesManager !== data.issuesManager) { |
| this.#issuesManager?.removeEventListener( |
| IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.scheduleUpdate, this); |
| this.#issuesManager = data.issuesManager; |
| this.#issuesManager.addEventListener( |
| IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.scheduleUpdate, this); |
| } |
| if (data.throttlerTimeout !== 0) { |
| this.#throttler = new Common.Throttler.Throttler(data.throttlerTimeout ?? 100); |
| } else { |
| this.#throttler = undefined; |
| } |
| this.scheduleUpdate(); |
| } |
| |
| get data(): IssueCounterData { |
| return { |
| clickHandler: this.#clickHandler, |
| leadingText: this.#leadingText, |
| tooltipCallback: this.#tooltipCallback, |
| displayMode: this.#displayMode, |
| accessibleName: this.#accessibleName, |
| throttlerTimeout: this.#throttlerTimeout, |
| compact: this.#compact, |
| issuesManager: this.#issuesManager as IssuesManager.IssuesManager.IssuesManager, |
| }; |
| } |
| |
| #render(): void { |
| if (!this.#issuesManager) { |
| return; |
| } |
| this.#counts = [ |
| this.#issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.PAGE_ERROR), |
| this.#issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.BREAKING_CHANGE), |
| this.#issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.IMPROVEMENT), |
| ]; |
| const importance = [ |
| IssuesManager.Issue.IssueKind.PAGE_ERROR, |
| IssuesManager.Issue.IssueKind.BREAKING_CHANGE, |
| IssuesManager.Issue.IssueKind.IMPROVEMENT, |
| ]; |
| const mostImportant = importance[this.#counts.findIndex(x => x > 0) ?? 2]; |
| |
| const countToString = (kind: IssuesManager.Issue.IssueKind, count: number): string|undefined => { |
| switch (this.#displayMode) { |
| case DisplayMode.OMIT_EMPTY: |
| return count > 0 ? `${count}` : undefined; |
| case DisplayMode.SHOW_ALWAYS: |
| return `${count}`; |
| case DisplayMode.ONLY_MOST_IMPORTANT: |
| return kind === mostImportant ? `${count}` : undefined; |
| } |
| }; |
| const iconSize = '2ex'; |
| const data: IconButton.IconButton.IconButtonData = { |
| groups: [ |
| { |
| ...toIconGroup(getIssueKindIconName(IssuesManager.Issue.IssueKind.PAGE_ERROR), iconSize), |
| text: countToString(IssuesManager.Issue.IssueKind.PAGE_ERROR, this.#counts[0]), |
| }, |
| { |
| ...toIconGroup(getIssueKindIconName(IssuesManager.Issue.IssueKind.BREAKING_CHANGE), iconSize), |
| text: countToString(IssuesManager.Issue.IssueKind.BREAKING_CHANGE, this.#counts[1]), |
| }, |
| { |
| ...toIconGroup(getIssueKindIconName(IssuesManager.Issue.IssueKind.IMPROVEMENT), iconSize), |
| text: countToString(IssuesManager.Issue.IssueKind.IMPROVEMENT, this.#counts[2]), |
| }, |
| ], |
| clickHandler: this.#clickHandler, |
| leadingText: this.#leadingText, |
| accessibleName: this.#accessibleName, |
| compact: this.#compact, |
| }; |
| render( |
| html` |
| <style>${issueCounterStyles}</style> |
| <icon-button .data=${data} .accessibleName=${this.#accessibleName}></icon-button> |
| `, |
| this.#shadow, {host: this}); |
| this.#tooltipCallback?.(); |
| } |
| } |
| |
| customElements.define('devtools-issue-counter', IssueCounter); |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'devtools-issue-counter': IssueCounter; |
| } |
| } |