Fix truncation for long workflow / item names (#1773)

Fixes #1771

### Changes

- Previously these would either get cut off or overflow, now they get
truncated!
- Titles also have a tooltip now

### Screenshots (after, see issue for before)

<img width="780" alt="Screenshot 2024-05-02 at 1 25 00 PM"
src="https://github.com/webrecorder/browsertrix/assets/5672810/337f90e4-c3b2-4adf-b63e-fd4fdf85d593">

<img width="1184" alt="Screenshot 2024-05-02 at 1 58 21 PM"
src="https://github.com/webrecorder/browsertrix/assets/5672810/a0e5111a-ffd4-4d03-8833-cc1831d28e60">

**Before**
<img width="655" alt="Screenshot 2024-05-02 at 1 59 26 PM"
src="https://github.com/webrecorder/browsertrix/assets/5672810/a69d3359-3932-4183-9563-2d49b4082990">

**After**
<img width="489" alt="Screenshot 2024-05-02 at 2 46 49 PM"
src="https://github.com/webrecorder/browsertrix/assets/5672810/c1ead7ae-0c26-44e6-916e-df7a398f97df">

### Caveats
- Doesn't replace the in-page dialog.  Added a TODO note.

---------

Co-authored-by: emma <hi@emma.cafe>
This commit is contained in:
Henry Wilkinson 2024-05-03 13:48:18 -04:00 committed by GitHub
parent 93c35ee2ee
commit cf1592a809
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 151 additions and 49 deletions

View File

@ -0,0 +1,76 @@
import { localized } from "@lit/localize";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { TailwindElement } from "@/classes/TailwindElement";
import { type ArchivedItem, type Workflow } from "@/types/crawler";
import { formatNumber } from "@/utils/localization";
import { pluralOf } from "@/utils/pluralize";
enum TitleSource {
Name,
ID,
FirstSeed,
}
type Item = Pick<
ArchivedItem & Workflow,
"name" | "firstSeed" | "seedCount" | "id"
>;
@localized()
@customElement("btrix-detail-page-title")
export class DetailPageTitle extends TailwindElement {
@property({ type: Object })
item: Item | undefined;
static styles = css`
:host {
display: contents;
}
sl-tooltip::part(body) {
word-break: break-all;
max-width: min(var(--max-width), calc(100vw - 0.5rem));
}
`;
private primaryTitle(item: Item): {
title: string;
source: TitleSource;
} {
if (item.name) return { title: item.name, source: TitleSource.Name };
if (!item.firstSeed || !item.seedCount)
return { title: item.id, source: TitleSource.ID };
return { title: item.firstSeed, source: TitleSource.FirstSeed };
}
private renderTitle(item: Item) {
const { title, source } = this.primaryTitle(item);
if (source !== TitleSource.FirstSeed)
return html`<span class="truncate">${title}</span>`;
const remainder = item.seedCount - 1;
return html`<span class="truncate">${item.firstSeed}</span>${remainder
? html` <span class="whitespace-nowrap text-neutral-500"
>+${formatNumber(remainder)} ${pluralOf("URLs", remainder)}</span
>`
: nothing}`;
}
render() {
if (!this.item)
return html`<sl-skeleton class="inline-block h-8 w-60"></sl-skeleton>`;
return html`<sl-tooltip
content="${this.primaryTitle(this.item).title}"
hoist
>
<h1 class="flex min-w-32 text-xl font-semibold leading-8">
${this.renderTitle(this.item)}
</h1>
</sl-tooltip>`;
}
}

View File

@ -5,3 +5,4 @@ import("./orgs-list");
import("./not-found");
import("./screencast");
import("./beta-badges");
import("./detail-page-title");

View File

@ -139,6 +139,7 @@ export class CrawlListItem extends TailwindElement {
<btrix-crawl-status
state=${workflow.state}
hideLabel
hoist
></btrix-crawl-status>
`,
)}

View File

@ -420,22 +420,27 @@ export class ArchivedItemDetail extends TailwindElement {
if (!this.crawl)
return html`<sl-skeleton class="inline-block h-8 w-60"></sl-skeleton>`;
if (this.crawl.name) return this.crawl.name;
if (this.crawl.name)
return html`<span class="truncate">${this.crawl.name}</span>`;
if (!this.crawl.firstSeed || !this.crawl.seedCount) return this.crawl.id;
const remainder = this.crawl.seedCount - 1;
let crawlName: TemplateResult = html`<span class="break-words"
let crawlName: TemplateResult = html`<span class="truncate"
>${this.crawl.firstSeed}</span
>`;
if (remainder) {
if (remainder === 1) {
crawlName = msg(
html`<span class="break-words">${this.crawl.firstSeed}</span>
<span class="text-neutral-500">+${remainder} URL</span>`,
html`<span class="truncate">${this.crawl.firstSeed}</span>
<span class="whitespace-nowrap text-neutral-500"
>+${remainder} URL</span
>`,
);
} else {
crawlName = msg(
html`<span class="break-words">${this.crawl.firstSeed}</span>
<span class="text-neutral-500">+${remainder} URLs</span>`,
html`<span class="truncate">${this.crawl.firstSeed}</span>
<span class="whitespace-nowrap text-neutral-500"
>+${remainder} URLs</span
>`,
);
}
}
@ -534,17 +539,9 @@ export class ArchivedItemDetail extends TailwindElement {
private renderHeader() {
return html`
<header class="mb-3 flex flex-wrap items-center gap-2 border-b pb-3">
<h1
class="grid min-w-0 flex-auto truncate text-xl font-semibold leading-7"
>
${this.renderName()}
</h1>
<div
class="${this.isActive
? "justify-between"
: "justify-end ml-auto"} grid grid-flow-col gap-2"
>
<header class="mb-3 flex flex-wrap gap-2 border-b pb-3">
<btrix-detail-page-title .item=${this.crawl}></btrix-detail-page-title>
<div class="ml-auto flex flex-wrap justify-end gap-2">
${this.isActive
? html`
<sl-button-group>
@ -1296,13 +1293,10 @@ ${this.crawl?.description}
return !formEl.querySelector("[data-invalid]");
}
// TODO replace with in-page dialog
private async deleteCrawl() {
if (
!window.confirm(
msg(
str`Are you sure you want to delete crawl of ${this.renderName()}?`,
),
)
!window.confirm(msg(str`Are you sure you want to delete this crawl?`))
) {
return;
}

View File

@ -282,22 +282,20 @@ export class WorkflowDetail extends LiteElement {
<div class="grid grid-cols-1 gap-7">
${this.renderHeader()}
<header class="col-span-1 items-end justify-between md:flex">
<h2>
<span
class="inline-block align-middle text-xl font-semibold leading-10 md:mr-2"
>${this.renderName()}</span
>
${when(
this.workflow?.inactive,
() => html`
<btrix-badge class="inline-block align-middle" variant="warning"
>${msg("Inactive")}</btrix-badge
>
`,
)}
</h2>
<div class="flex-0 flex justify-end">
<header class="col-span-1 flex flex-wrap gap-2">
<btrix-detail-page-title
.item=${this.workflow}
></btrix-detail-page-title>
${when(
this.workflow?.inactive,
() => html`
<btrix-badge class="inline-block align-middle" variant="warning"
>${msg("Inactive")}</btrix-badge
>
`,
)}
<div class="flex-0 ml-auto flex flex-wrap justify-end gap-2">
${when(
this.isCrawler && this.workflow && !this.workflow.inactive,
this.renderActions,
@ -559,7 +557,9 @@ export class WorkflowDetail extends LiteElement {
${this.renderHeader(this.workflow!.id)}
<header>
<h2 class="text-xl font-semibold leading-10">${this.renderName()}</h2>
<h2 class="break-all text-xl font-semibold leading-10">
${this.renderName()}
</h2>
</header>
${when(
@ -593,7 +593,7 @@ export class WorkflowDetail extends LiteElement {
${when(
this.workflow.isCrawlRunning,
() => html`
<sl-button-group class="mr-2">
<sl-button-group>
<sl-button
size="small"
@click=${() => (this.openDialogName = "stop")}
@ -629,7 +629,6 @@ export class WorkflowDetail extends LiteElement {
<sl-button
size="small"
variant="primary"
class="mr-2"
?disabled=${this.orgStorageQuotaReached ||
this.orgExecutionMinutesQuotaReached}
@click=${() => void this.runNow()}
@ -798,22 +797,28 @@ export class WorkflowDetail extends LiteElement {
}
private renderName() {
if (!this.workflow) return "";
if (this.workflow.name) return this.workflow.name;
if (!this.workflow)
return html`<sl-skeleton class="inline-block h-8 w-60"></sl-skeleton>`;
if (this.workflow.name)
return html`<span class="truncate">${this.workflow.name}</span>`;
const { seedCount, firstSeed } = this.workflow;
if (seedCount === 1) {
return firstSeed;
return html`<span class="truncate">${firstSeed}</span>`;
}
const remainderCount = seedCount - 1;
if (remainderCount === 1) {
return msg(
html`${firstSeed}
<span class="text-neutral-500">+${remainderCount} URL</span>`,
html` <span class="truncate">${firstSeed}</span>
<span class="whitespace-nowrap text-neutral-500"
>+${remainderCount} URL</span
>`,
);
}
return msg(
html`${firstSeed}
<span class="text-neutral-500">+${remainderCount} URLs</span>`,
html` <span class="truncate">${firstSeed}</span>
<span class="whitespace-nowrap text-neutral-500"
>+${remainderCount} URLs</span
>`,
);
}
@ -1050,7 +1055,6 @@ export class WorkflowDetail extends LiteElement {
this.workflow?.lastCrawlId,
() => html`
<sl-button
class="mr-2"
href=${`${this.orgBasePath}/items/crawl/${
this.workflow!.lastCrawlId
}#replay`}

View File

@ -56,6 +56,32 @@ const plurals = {
id: "comments.plural.other",
}),
},
URLs: {
zero: msg("URLs", {
desc: 'plural form of "URLs" for zero URLs',
id: "URLs.plural.zero",
}),
one: msg("URL", {
desc: 'singular form for "URL"',
id: "URLs.plural.one",
}),
two: msg("URLs", {
desc: 'plural form of "URLs" for two URLs',
id: "URLs.plural.two",
}),
few: msg("URLs", {
desc: 'plural form of "URLs" for few URLs',
id: "URLs.plural.few",
}),
many: msg("URLs", {
desc: 'plural form of "URLs" for many URLs',
id: "URLs.plural.many",
}),
other: msg("URLs", {
desc: 'plural form of "URLs" for multiple/other URLs',
id: "URLs.plural.other",
}),
},
};
export const pluralOf = (word: keyof typeof plurals, count: number) => {