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:
parent
93c35ee2ee
commit
cf1592a809
76
frontend/src/components/detail-page-title.ts
Normal file
76
frontend/src/components/detail-page-title.ts
Normal 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>`;
|
||||
}
|
||||
}
|
@ -5,3 +5,4 @@ import("./orgs-list");
|
||||
import("./not-found");
|
||||
import("./screencast");
|
||||
import("./beta-badges");
|
||||
import("./detail-page-title");
|
||||
|
@ -139,6 +139,7 @@ export class CrawlListItem extends TailwindElement {
|
||||
<btrix-crawl-status
|
||||
state=${workflow.state}
|
||||
hideLabel
|
||||
hoist
|
||||
></btrix-crawl-status>
|
||||
`,
|
||||
)}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -282,12 +282,10 @@ 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
|
||||
>
|
||||
<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`
|
||||
@ -296,8 +294,8 @@ export class WorkflowDetail extends LiteElement {
|
||||
>
|
||||
`,
|
||||
)}
|
||||
</h2>
|
||||
<div class="flex-0 flex justify-end">
|
||||
|
||||
<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`}
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user