browsertrix/frontend/src/features/archived-items/crawl-list.ts
Emma Segal-Grossman 07edf697f0
Hotfix: Crawls page table click targets not applied to the right elements (#1524)
Fixes #1525

### Changes

- Changes one of the table cell component usages in the crawl list page
to correctly use the `rowClickTarget` prop, rather than setting the
class to `rowClickTarget`.
- Updates the `rowClickTarget` styling to only apply _within_ a
`<btrix-table-cell>`
2024-02-08 14:41:33 -08:00

365 lines
10 KiB
TypeScript

/**
* Display list of crawls
*
* Usage example:
* ```ts
* <btrix-crawl-list>
* <btrix-crawl-list-item .crawl=${crawl1}>
* </btrix-crawl-list-item>
* <btrix-crawl-list-item .crawl=${crawl2}>
* </btrix-crawl-list-item>
* </btrix-crawl-list>
* ```
*/
import type { TemplateResult } from "lit";
import { html, css, nothing } from "lit";
import {
customElement,
property,
query,
queryAssignedElements,
} from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { msg, localized, str } from "@lit/localize";
import { RelativeDuration } from "@/components/ui/relative-duration";
import type { Crawl } from "@/types/crawler";
import type { OverflowDropdown } from "@/components/ui/overflow-dropdown";
import { renderName } from "@/utils/crawler";
import { TailwindElement } from "@/classes/TailwindElement";
import { NavigateController } from "@/controllers/navigate";
/**
* @slot menu
*/
@localized()
@customElement("btrix-crawl-list-item")
export class CrawlListItem extends TailwindElement {
static styles = css`
:host {
display: contents;
}
btrix-table-row {
border-top: var(--btrix-border-top, 0);
border-radius: var(--btrix-border-radius-top, 0)
var(--btrix-border-radius-to, 0) var(--btrix-border-radius-bottom, 0)
var(--btrix-border-radius-bottom, 0);
}
`;
@property({ type: Object })
crawl?: Crawl;
@property({ type: String })
collectionId?: string;
@property({ type: String })
workflowId?: string;
@property({ type: String })
href?: string;
@query(".row")
row!: HTMLElement;
@query("btrix-overflow-dropdown")
dropdownMenu!: OverflowDropdown;
// TODO localize
private readonly numberFormatter = new Intl.NumberFormat(undefined, {
notation: "compact",
});
private readonly navigate = new NavigateController(this);
render() {
if (!this.crawl) return;
let idCell: TemplateResult;
if (this.workflowId) {
const label = html`
<div>
${this.safeRender(
(crawl) => html`
<sl-format-date
date=${`${crawl.started}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
hour="2-digit"
minute="2-digit"
></sl-format-date>
`,
)}
</div>
`;
idCell = html`
<btrix-table-cell
rowClickTarget=${ifDefined(this.href ? "a" : undefined)}
>
${this.href
? html`<a href=${this.href} @click=${this.navigate.link}>
${label}
</a>`
: html`<div>${label}</div> `}
</btrix-table-cell>
`;
} else {
const label = html`
<btrix-table-cell class="clickLabel" role="generic">
${this.safeRender((workflow) => renderName(workflow))}
</btrix-table-cell>
`;
idCell = html`
<btrix-table-cell rowClickTarget="a">
${this.href
? html`<a href=${this.href} @click=${this.navigate.link}>
${label}
</a>`
: html`<div>${label}</div> `}
</btrix-table-cell>
`;
}
return html`
<btrix-table-row
class=${this.href
? "cursor-pointer select-none transition-colors hover:bg-neutral-50 focus-within:bg-neutral-50"
: ""}
@click=${async (e: MouseEvent) => {
if (e.target === this.dropdownMenu) {
return;
}
e.preventDefault();
}}
>
<btrix-table-cell class="pr-0">
${this.safeRender(
(workflow) => html`
<btrix-crawl-status
state=${workflow.state}
hideLabel
></btrix-crawl-status>
`,
)}
</btrix-table-cell>
${idCell}
${this.workflowId
? nothing
: html`
<btrix-table-cell>
${this.safeRender(
(crawl) => html`
<sl-format-date
date=${`${crawl.started}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
hour="2-digit"
minute="2-digit"
></sl-format-date>
`,
)}
</btrix-table-cell>
`}
<btrix-table-cell>
${this.safeRender((crawl) =>
crawl.finished
? html`
<sl-format-date
date=${`${crawl.finished}Z`}
month="2-digit"
day="2-digit"
year="2-digit"
hour="2-digit"
minute="2-digit"
></sl-format-date>
`
: html`<span class="text-neutral-400" role="presentation"
>---</span
>`,
)}
</btrix-table-cell>
<btrix-table-cell>
${this.safeRender((crawl) =>
RelativeDuration.humanize(
(crawl.finished
? new Date(`${crawl.finished}Z`)
: new Date()
).valueOf() - new Date(`${crawl.started}Z`).valueOf(),
),
)}
</btrix-table-cell>
<btrix-table-cell>
<sl-format-bytes
value=${this.crawl.fileSize || 0}
display="narrow"
></sl-format-bytes>
</btrix-table-cell>
<btrix-table-cell>
${this.safeRender((crawl) => {
const pagesComplete = +(crawl.stats?.done || 0);
const pagesFound = +(crawl.stats?.found || 0);
if (crawl.finished) {
return pagesComplete === 1
? msg(str`${this.numberFormatter.format(pagesComplete)} page`)
: msg(str`${this.numberFormatter.format(pagesComplete)} pages`);
}
return pagesFound === 1
? msg(
str`${this.numberFormatter.format(
pagesComplete,
)} / ${this.numberFormatter.format(pagesFound)} page`,
)
: msg(
str`${this.numberFormatter.format(
pagesComplete,
)} / ${this.numberFormatter.format(pagesFound)} pages`,
);
})}
</btrix-table-cell>
<btrix-table-cell>
<div class="max-w-sm truncate">
${this.safeRender((crawl) => crawl.userName)}
</div>
</btrix-table-cell>
<btrix-table-cell class="pl-1 pr-1">
${this.renderActions()}
</btrix-table-cell>
</btrix-table-row>
`;
}
private safeRender(
render: (crawl: Crawl) => string | TemplateResult<1> | undefined,
) {
if (!this.crawl) {
return html`<sl-skeleton></sl-skeleton>`;
}
return render(this.crawl);
}
private renderActions() {
return html` <div class="col action">
<btrix-overflow-dropdown
@click=${(e: MouseEvent) => {
// Prevent navigation to detail view
e.preventDefault();
e.stopPropagation();
}}
>
<slot name="menu"></slot>
</btrix-overflow-dropdown>
</div>`;
}
}
/**
* @slot
*/
@localized()
@customElement("btrix-crawl-list")
export class CrawlList extends TailwindElement {
static styles = css`
btrix-table {
--btrix-cell-gap: var(--sl-spacing-x-small);
--btrix-cell-padding-left: var(--sl-spacing-small);
--btrix-cell-padding-right: var(--sl-spacing-small);
}
btrix-table-body {
--btrix-cell-padding-top: var(--sl-spacing-2x-small);
--btrix-cell-padding-bottom: var(--sl-spacing-2x-small);
}
btrix-table-body ::slotted(*:nth-of-type(n + 2)) {
--btrix-border-top: 1px solid var(--sl-panel-border-color);
}
btrix-table-body ::slotted(*:first-of-type) {
--btrix-border-radius-top: var(--sl-border-radius-medium);
}
btrix-table-body ::slotted(*:last-of-type) {
--btrix-border-radius-bottom: var(--sl-border-radius-medium);
}
`;
@property({ type: String })
collectionId?: string;
@property({ type: String })
workflowId?: string;
@queryAssignedElements({ selector: "btrix-crawl-list-item" })
listItems!: HTMLElement[];
render() {
return html` <style>
btrix-table {
grid-template-columns:
min-content [clickable-start]
${this.workflowId ? "" : `auto `}auto auto
auto auto auto auto [clickable-end] min-content;
}
</style>
<div class="overflow-auto">
<btrix-table>
<btrix-table-head class="mb-2">
<btrix-table-header-cell class="pr-0">
<span class="sr-only">${msg("Status")}</span>
</btrix-table-header-cell>
${this.workflowId
? nothing
: html`
<btrix-table-header-cell>
${msg("Name")}
</btrix-table-header-cell>
`}
<btrix-table-header-cell>
${msg("Started")}
</btrix-table-header-cell>
<btrix-table-header-cell>
${msg("Finished")}
</btrix-table-header-cell>
<btrix-table-header-cell
>${msg("Duration")}</btrix-table-header-cell
>
<btrix-table-header-cell>${msg("Size")}</btrix-table-header-cell>
<btrix-table-header-cell
>${msg("Pages Crawled")}</btrix-table-header-cell
>
<btrix-table-header-cell>
${msg("Created By")}
</btrix-table-header-cell>
<btrix-table-header-cell class="pl-1 pr-1">
<span class="sr-only">${msg("Row actions")}</span>
</btrix-table-header-cell>
</btrix-table-head>
<btrix-table-body class="rounded border">
<slot @slotchange=${this.handleSlotchange}></slot>
</btrix-table-body>
</btrix-table>
</div>`;
}
private handleSlotchange() {
const assignProp = (
el: HTMLElement,
attr: { name: string; value: string },
) => {
if (!el.attributes.getNamedItem(attr.name)) {
el.setAttribute(attr.name, attr.value);
}
};
this.listItems.forEach((item) => {
assignProp(item, {
name: "collectionId",
value: this.collectionId || "",
});
assignProp(item, { name: "workflowId", value: this.workflowId || "" });
});
}
}