| // 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 '../../../ui/kit/kit.js'; |
| |
| import * as Common from '../../../core/common/common.js'; |
| import * as i18n from '../../../core/i18n/i18n.js'; |
| import type * as Platform from '../../../core/platform/platform.js'; |
| import type * as Protocol from '../../../generated/protocol.js'; |
| import * as IssuesManager from '../../../models/issues_manager/issues_manager.js'; |
| import * as RenderCoordinator from '../../../ui/components/render_coordinator/render_coordinator.js'; |
| import * as Lit from '../../../ui/lit/lit.js'; |
| import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; |
| |
| import {getIssueKindIconName} from './IssueCounter.js'; |
| import IssueLinkIconStyles from './issueLinkIcon.css.js'; |
| |
| const {html} = Lit; |
| |
| const UIStrings = { |
| /** |
| * @description Title for a link to show an issue in the issues tab |
| */ |
| clickToShowIssue: 'Click to show issue in the issues tab', |
| /** |
| * @description Title for a link to show an issue in the issues tab |
| * @example {A title of an Issue} title |
| */ |
| clickToShowIssueWithTitle: 'Click to open the issue tab and show issue: {title}', |
| /** |
| * @description Title for an link to show an issue that is unavailable because the issue couldn't be resolved |
| */ |
| issueUnavailable: 'Issue unavailable at this time', |
| } as const; |
| const str_ = i18n.i18n.registerUIStrings('ui/components/issue_counter/IssueLinkIcon.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export interface IssueLinkIconData { |
| issue?: IssuesManager.Issue.Issue|null; |
| issueId?: Protocol.Audits.IssueId; |
| issueResolver?: IssuesManager.IssueResolver.IssueResolver; |
| additionalOnClickAction?: () => void; |
| revealOverride?: (revealable: unknown, omitFocus?: boolean) => Promise<void>; |
| } |
| |
| export const extractShortPath = (path: string): string => { |
| // 1st regex matches everything after last '/' |
| // if path ends with '/', 2nd regex returns everything between the last two '/' |
| return (/[^/]+$/.exec(path) || /[^/]+\/$/.exec(path) || [''])[0]; |
| }; |
| |
| export class IssueLinkIcon extends HTMLElement { |
| readonly #shadow = this.attachShadow({mode: 'open'}); |
| // The value `null` indicates that the issue is not available, |
| // `undefined` that it is still being resolved. |
| #issue?: IssuesManager.Issue.Issue|null; |
| #issueTitle: string|null = null; |
| #issueId?: Protocol.Audits.IssueId; |
| #issueResolver?: IssuesManager.IssueResolver.IssueResolver; |
| #additionalOnClickAction?: () => void; |
| #reveal = Common.Revealer.reveal; |
| |
| set data(data: IssueLinkIconData) { |
| this.#issue = data.issue; |
| this.#issueId = data.issueId; |
| this.#issueResolver = data.issueResolver; |
| if (!this.#issue) { |
| if (!this.#issueId) { |
| throw new Error('Either `issue` or `issueId` must be provided'); |
| } else if (!this.#issueResolver) { |
| throw new Error('An `IssueResolver` must be provided if an `issueId` is provided.'); |
| } |
| } |
| this.#additionalOnClickAction = data.additionalOnClickAction; |
| if (data.revealOverride) { |
| this.#reveal = data.revealOverride; |
| } |
| void this.#fetchIssueData(); |
| void this.#render(); |
| } |
| |
| async #fetchIssueData(): Promise<void> { |
| if (!this.#issue && this.#issueId) { |
| try { |
| this.#issue = await this.#issueResolver?.waitFor(this.#issueId); |
| } catch { |
| this.#issue = null; |
| } |
| } |
| const description = this.#issue?.getDescription(); |
| if (description) { |
| const title = await IssuesManager.MarkdownIssueDescription.getIssueTitleFromMarkdownDescription(description); |
| if (title) { |
| this.#issueTitle = title; |
| } |
| } |
| await this.#render(); |
| } |
| |
| get data(): IssueLinkIconData { |
| return { |
| issue: this.#issue, |
| issueId: this.#issueId, |
| issueResolver: this.#issueResolver, |
| additionalOnClickAction: this.#additionalOnClickAction, |
| revealOverride: this.#reveal !== Common.Revealer.reveal ? this.#reveal : undefined, |
| }; |
| } |
| |
| handleClick(event: MouseEvent): void { |
| if (event.button !== 0) { |
| return; // Only handle left-click for now. |
| } |
| if (this.#issue) { |
| void this.#reveal(this.#issue); |
| } |
| this.#additionalOnClickAction?.(); |
| event.consume(); |
| } |
| |
| #getTooltip(): Platform.UIString.LocalizedString { |
| if (this.#issueTitle) { |
| return i18nString(UIStrings.clickToShowIssueWithTitle, {title: this.#issueTitle}); |
| } |
| if (this.#issue) { |
| return i18nString(UIStrings.clickToShowIssue); |
| } |
| return i18nString(UIStrings.issueUnavailable); |
| } |
| |
| #getIconName(): string { |
| if (!this.#issue) { |
| return 'issue-questionmark-filled'; |
| } |
| const iconName = getIssueKindIconName(this.#issue.getKind()); |
| return iconName; |
| } |
| |
| #render(): Promise<void> { |
| return RenderCoordinator.write(() => { |
| // clang-format off |
| Lit.render(html` |
| <style>${IssueLinkIconStyles}</style> |
| <button class=${Lit.Directives.classMap({link: Boolean(this.#issue)})} |
| title=${this.#getTooltip()} |
| jslog=${VisualLogging.link('issue').track({click: true})} |
| @click=${this.handleClick}> |
| <devtools-icon name=${this.#getIconName()}></devtools-icon> |
| </button>`, |
| this.#shadow, {host: this}); |
| // clang-format on |
| }); |
| } |
| } |
| |
| customElements.define('devtools-issue-link-icon', IssueLinkIcon); |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'devtools-issue-link-icon': IssueLinkIcon; |
| } |
| } |