feat: UX improvements to collections with single URL (#2325)

Resolves https://github.com/webrecorder/browsertrix/issues/2322

## Changes

- Sets default start page if collection only contains one page
- Removes status code from snapshot options
This commit is contained in:
sua yoo 2025-01-25 17:18:22 -08:00 committed by GitHub
parent 9363095d62
commit 84ae73df18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 94 additions and 86 deletions

View File

@ -14,6 +14,7 @@ import type { SelectSnapshotDetail } from "./select-collection-start-page";
import { BtrixElement } from "@/classes/BtrixElement"; import { BtrixElement } from "@/classes/BtrixElement";
import type { Dialog } from "@/components/ui/dialog"; import type { Dialog } from "@/components/ui/dialog";
import type { Collection } from "@/types/collection";
/** /**
* @fires btrix-change * @fires btrix-change
@ -40,14 +41,8 @@ export class CollectionStartPageDialog extends BtrixElement {
@property({ type: String }) @property({ type: String })
collectionId?: string; collectionId?: string;
@property({ type: String }) @property({ type: Object })
homeUrl?: string | null = null; collection?: Collection;
@property({ type: String })
homePageId?: string | null = null;
@property({ type: String })
homeTs?: string | null = null;
@property({ type: Boolean }) @property({ type: Boolean })
open = false; open = false;
@ -77,8 +72,8 @@ export class CollectionStartPageDialog extends BtrixElement {
private readonly thumbnailPreview?: CollectionSnapshotPreview | null; private readonly thumbnailPreview?: CollectionSnapshotPreview | null;
willUpdate(changedProperties: PropertyValues<this>) { willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("homeUrl")) { if (changedProperties.has("collection") && this.collection) {
this.homeView = this.homeUrl ? HomeView.URL : HomeView.Pages; this.homeView = this.collection.homeUrl ? HomeView.URL : HomeView.Pages;
} }
} }
@ -92,7 +87,12 @@ export class CollectionStartPageDialog extends BtrixElement {
class="[--width:60rem]" class="[--width:60rem]"
@sl-show=${() => (this.showContent = true)} @sl-show=${() => (this.showContent = true)}
@sl-after-hide=${() => { @sl-after-hide=${() => {
this.homeView = this.homeUrl ? HomeView.URL : HomeView.Pages; if (this.collection) {
this.homeView = this.collection.homeUrl
? HomeView.URL
: HomeView.Pages;
}
this.isSubmitting = false; this.isSubmitting = false;
this.selectedSnapshot = null; this.selectedSnapshot = null;
this.showContent = false; this.showContent = false;
@ -142,11 +142,11 @@ export class CollectionStartPageDialog extends BtrixElement {
private renderPreview() { private renderPreview() {
const snapshot = const snapshot =
this.selectedSnapshot || this.selectedSnapshot ||
(this.homeUrl (this.collection?.homeUrl
? { ? {
url: this.homeUrl, url: this.collection.homeUrl,
ts: this.homeTs, ts: this.collection.homeUrlTs,
pageId: this.homePageId, pageId: this.collection.homeUrlPageId,
status: 200, status: 200,
} }
: null); : null);
@ -207,8 +207,8 @@ export class CollectionStartPageDialog extends BtrixElement {
if (this.homeView === HomeView.Pages) { if (this.homeView === HomeView.Pages) {
if ( if (
!this.homePageId || !this.collection?.homeUrlPageId ||
this.homePageId !== this.selectedSnapshot?.pageId this.collection.homeUrlPageId !== this.selectedSnapshot?.pageId
) { ) {
// Reset unsaved selected snapshot // Reset unsaved selected snapshot
this.selectedSnapshot = null; this.selectedSnapshot = null;
@ -244,8 +244,7 @@ export class CollectionStartPageDialog extends BtrixElement {
<section> <section>
<btrix-select-collection-start-page <btrix-select-collection-start-page
.collectionId=${this.collectionId} .collectionId=${this.collectionId}
.homeUrl=${this.homeUrl} .collection=${this.collection}
.homeTs=${this.homeTs}
@btrix-select=${async ( @btrix-select=${async (
e: CustomEvent<SelectSnapshotDetail>, e: CustomEvent<SelectSnapshotDetail>,
) => { ) => {
@ -280,10 +279,10 @@ export class CollectionStartPageDialog extends BtrixElement {
const { homeView, useThumbnail } = serialize(form); const { homeView, useThumbnail } = serialize(form);
if ( if (
(homeView === HomeView.Pages && !this.homePageId) || (homeView === HomeView.Pages && !this.collection?.homeUrlPageId) ||
(homeView === HomeView.URL && (homeView === HomeView.URL &&
this.selectedSnapshot && this.selectedSnapshot &&
this.homePageId === this.selectedSnapshot.pageId) this.collection?.homeUrlPageId === this.selectedSnapshot.pageId)
) { ) {
// No changes to save // No changes to save
this.open = false; this.open = false;
@ -302,7 +301,7 @@ export class CollectionStartPageDialog extends BtrixElement {
homeView === HomeView.URL && homeView === HomeView.URL &&
useThumbnail === "on" && useThumbnail === "on" &&
this.selectedSnapshot && this.selectedSnapshot &&
this.homePageId !== this.selectedSnapshot.pageId; this.collection?.homeUrlPageId !== this.selectedSnapshot.pageId;
// TODO get filename from rwp? // TODO get filename from rwp?
const fileName = `page-thumbnail_${this.selectedSnapshot?.pageId}.jpeg`; const fileName = `page-thumbnail_${this.selectedSnapshot?.pageId}.jpeg`;
let file: File | undefined; let file: File | undefined;

View File

@ -10,12 +10,15 @@ import { html, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { when } from "lit/directives/when.js"; import { when } from "lit/directives/when.js";
import debounce from "lodash/fp/debounce"; import debounce from "lodash/fp/debounce";
import sortBy from "lodash/fp/sortBy"; import filter from "lodash/fp/filter";
import flow from "lodash/fp/flow";
import orderBy from "lodash/fp/orderBy";
import queryString from "query-string"; import queryString from "query-string";
import { BtrixElement } from "@/classes/BtrixElement"; import { BtrixElement } from "@/classes/BtrixElement";
import type { Combobox } from "@/components/ui/combobox"; import type { Combobox } from "@/components/ui/combobox";
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api"; import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
import type { Collection } from "@/types/collection";
import type { UnderlyingFunction } from "@/types/utils"; import type { UnderlyingFunction } from "@/types/utils";
import { tw } from "@/utils/tailwind"; import { tw } from "@/utils/tailwind";
@ -39,7 +42,11 @@ export type SelectSnapshotDetail = {
const DEFAULT_PROTOCOL = "http"; const DEFAULT_PROTOCOL = "http";
const sortByTs = sortBy<Snapshot>("ts"); // TODO Check if backend can sort and filter snapshots instead
const sortByTs = flow(
filter<Snapshot>(({ status }) => status < 300),
orderBy<Snapshot>("ts")("desc"),
) as (snapshots: Snapshot[]) => Snapshot[];
/** /**
* @fires btrix-select * @fires btrix-select
@ -50,11 +57,8 @@ export class SelectCollectionStartPage extends BtrixElement {
@property({ type: String }) @property({ type: String })
collectionId?: string; collectionId?: string;
@property({ type: String }) @property({ type: Object })
homeUrl?: string | null = null; collection?: Collection;
@property({ type: String })
homeTs?: string | null = null;
@state() @state()
private searchQuery = ""; private searchQuery = "";
@ -82,14 +86,13 @@ export class SelectCollectionStartPage extends BtrixElement {
return this.selectedSnapshot; return this.selectedSnapshot;
} }
updated(changedProperties: PropertyValues<this>) { protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("homeUrl") && this.homeUrl) { if (changedProperties.has("collection") && this.collection) {
if (this.input) { void this.initSelection(this.collection);
this.input.value = this.homeUrl;
}
this.searchQuery = this.homeUrl;
void this.initSelection();
} }
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selectedSnapshot")) { if (changedProperties.has("selectedSnapshot")) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent<SelectSnapshotDetail>("btrix-select", { new CustomEvent<SelectSnapshotDetail>("btrix-select", {
@ -106,26 +109,50 @@ export class SelectCollectionStartPage extends BtrixElement {
} }
} }
private async initSelection() { private async initSelection(collection: Collection) {
await this.updateComplete; if (!collection.homeUrl && collection.pageCount !== 1) {
await this.searchResults.taskComplete; return;
if (this.homeUrl && this.searchResults.value) {
this.selectedPage = this.searchResults.value.items.find(
({ url }) => url === this.homeUrl,
);
if (this.selectedPage && this.homeTs) {
this.selectedSnapshot = this.selectedPage.snapshots.find(
({ ts }) => ts === this.homeTs,
);
}
} }
const pageUrls = await this.getPageUrls({
id: collection.id,
urlPrefix: collection.homeUrl || "",
pageSize: 1,
});
if (!pageUrls.total) {
return;
}
const startPage = pageUrls.items[0];
if (this.input) {
this.input.value = startPage.url;
}
this.selectedPage = this.formatPage(startPage);
const homeTs = collection.homeUrlTs;
this.selectedSnapshot = homeTs
? this.selectedPage.snapshots.find(({ ts }) => ts === homeTs)
: this.selectedPage.snapshots[0];
}
/**
* Format page for display
* @TODO Check if backend can sort and filter snapshots instead
*/
private formatPage(page: Page) {
return {
...page,
snapshots: sortByTs(page.snapshots),
};
} }
private readonly searchResults = new Task(this, { private readonly searchResults = new Task(this, {
task: async ([searchValue], { signal }) => { task: async ([searchValue], { signal }) => {
const searchResults = await this.getPageUrls( const pageUrls = await this.getPageUrls(
{ {
id: this.collectionId!, id: this.collectionId!,
urlPrefix: searchValue, urlPrefix: searchValue,
@ -133,7 +160,7 @@ export class SelectCollectionStartPage extends BtrixElement {
signal, signal,
); );
return searchResults; return pageUrls;
}, },
args: () => [this.searchQuery] as const, args: () => [this.searchQuery] as const,
}); });
@ -150,6 +177,7 @@ export class SelectCollectionStartPage extends BtrixElement {
value=${this.selectedSnapshot?.pageId || ""} value=${this.selectedSnapshot?.pageId || ""}
?required=${this.selectedPage && !this.selectedSnapshot} ?required=${this.selectedPage && !this.selectedSnapshot}
?disabled=${!this.selectedPage} ?disabled=${!this.selectedPage}
hoist
@sl-change=${async (e: SlChangeEvent) => { @sl-change=${async (e: SlChangeEvent) => {
const { value } = e.currentTarget as SlSelect; const { value } = e.currentTarget as SlSelect;
@ -160,35 +188,22 @@ export class SelectCollectionStartPage extends BtrixElement {
); );
}} }}
> >
${when( ${when(this.selectedPage, this.renderSnapshotOptions)}
this.selectedSnapshot,
(snapshot) => html`
<btrix-badge
slot="suffix"
variant=${snapshot.status < 300 ? "success" : "danger"}
>${snapshot.status}</btrix-badge
>
`,
)}
${when(this.selectedPage, (item) =>
item.snapshots.map(
({ pageId, ts, status }) => html`
<sl-option value=${pageId}>
${this.localize.date(ts)}
<btrix-badge
slot="suffix"
variant=${status < 300 ? "success" : "danger"}
>${status}</btrix-badge
>
</sl-option>
`,
),
)}
</sl-select> </sl-select>
</div> </div>
`; `;
} }
private readonly renderSnapshotOptions = ({ snapshots }: Page) => {
return html`
${snapshots.map(
({ pageId, ts }) => html`
<sl-option value=${pageId}> ${this.localize.date(ts)} </sl-option>
`,
)}
`;
};
private renderPageSearch() { private renderPageSearch() {
let prefix: { let prefix: {
icon: string; icon: string;
@ -223,7 +238,7 @@ export class SelectCollectionStartPage extends BtrixElement {
id="pageUrlInput" id="pageUrlInput"
label=${msg("Page URL")} label=${msg("Page URL")}
placeholder=${msg("Start typing a URL...")} placeholder=${msg("Start typing a URL...")}
clearable ?clearable=${this.collection && this.collection.pageCount > 1}
@sl-focus=${() => { @sl-focus=${() => {
this.resetInputValidity(); this.resetInputValidity();
this.combobox?.show(); this.combobox?.show();
@ -295,7 +310,7 @@ export class SelectCollectionStartPage extends BtrixElement {
this.selectedSnapshot = undefined; this.selectedSnapshot = undefined;
} else if (results.total === 1) { } else if (results.total === 1) {
// Choose only option, e.g. for copy-paste // Choose only option, e.g. for copy-paste
this.selectedPage = this.searchResults.value.items[0]; this.selectedPage = this.formatPage(this.searchResults.value.items[0]);
this.selectedSnapshot = this.selectedPage.snapshots[0]; this.selectedSnapshot = this.selectedPage.snapshots[0];
} }
}; };
@ -326,11 +341,7 @@ export class SelectCollectionStartPage extends BtrixElement {
this.input.value = item.url; this.input.value = item.url;
} }
this.selectedPage = { this.selectedPage = this.formatPage(item);
...item,
// TODO check if backend can sort
snapshots: sortByTs(item.snapshots).reverse(),
};
this.combobox?.hide(); this.combobox?.hide();

View File

@ -265,9 +265,7 @@ export class CollectionDetail extends BtrixElement {
}} }}
@sl-hide=${async () => (this.openDialogName = undefined)} @sl-hide=${async () => (this.openDialogName = undefined)}
collectionId=${this.collectionId} collectionId=${this.collectionId}
.homeUrl=${this.collection?.homeUrl} .collection=${this.collection}
.homePageId=${this.collection?.homeUrlPageId}
.homeTs=${this.collection?.homeUrlTs}
?replayLoaded=${this.isRwpLoaded} ?replayLoaded=${this.isRwpLoaded}
></btrix-collection-replay-dialog> ></btrix-collection-replay-dialog>