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 type { Dialog } from "@/components/ui/dialog";
import type { Collection } from "@/types/collection";
/**
* @fires btrix-change
@ -40,14 +41,8 @@ export class CollectionStartPageDialog extends BtrixElement {
@property({ type: String })
collectionId?: string;
@property({ type: String })
homeUrl?: string | null = null;
@property({ type: String })
homePageId?: string | null = null;
@property({ type: String })
homeTs?: string | null = null;
@property({ type: Object })
collection?: Collection;
@property({ type: Boolean })
open = false;
@ -77,8 +72,8 @@ export class CollectionStartPageDialog extends BtrixElement {
private readonly thumbnailPreview?: CollectionSnapshotPreview | null;
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("homeUrl")) {
this.homeView = this.homeUrl ? HomeView.URL : HomeView.Pages;
if (changedProperties.has("collection") && this.collection) {
this.homeView = this.collection.homeUrl ? HomeView.URL : HomeView.Pages;
}
}
@ -92,7 +87,12 @@ export class CollectionStartPageDialog extends BtrixElement {
class="[--width:60rem]"
@sl-show=${() => (this.showContent = true)}
@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.selectedSnapshot = null;
this.showContent = false;
@ -142,11 +142,11 @@ export class CollectionStartPageDialog extends BtrixElement {
private renderPreview() {
const snapshot =
this.selectedSnapshot ||
(this.homeUrl
(this.collection?.homeUrl
? {
url: this.homeUrl,
ts: this.homeTs,
pageId: this.homePageId,
url: this.collection.homeUrl,
ts: this.collection.homeUrlTs,
pageId: this.collection.homeUrlPageId,
status: 200,
}
: null);
@ -207,8 +207,8 @@ export class CollectionStartPageDialog extends BtrixElement {
if (this.homeView === HomeView.Pages) {
if (
!this.homePageId ||
this.homePageId !== this.selectedSnapshot?.pageId
!this.collection?.homeUrlPageId ||
this.collection.homeUrlPageId !== this.selectedSnapshot?.pageId
) {
// Reset unsaved selected snapshot
this.selectedSnapshot = null;
@ -244,8 +244,7 @@ export class CollectionStartPageDialog extends BtrixElement {
<section>
<btrix-select-collection-start-page
.collectionId=${this.collectionId}
.homeUrl=${this.homeUrl}
.homeTs=${this.homeTs}
.collection=${this.collection}
@btrix-select=${async (
e: CustomEvent<SelectSnapshotDetail>,
) => {
@ -280,10 +279,10 @@ export class CollectionStartPageDialog extends BtrixElement {
const { homeView, useThumbnail } = serialize(form);
if (
(homeView === HomeView.Pages && !this.homePageId) ||
(homeView === HomeView.Pages && !this.collection?.homeUrlPageId) ||
(homeView === HomeView.URL &&
this.selectedSnapshot &&
this.homePageId === this.selectedSnapshot.pageId)
this.collection?.homeUrlPageId === this.selectedSnapshot.pageId)
) {
// No changes to save
this.open = false;
@ -302,7 +301,7 @@ export class CollectionStartPageDialog extends BtrixElement {
homeView === HomeView.URL &&
useThumbnail === "on" &&
this.selectedSnapshot &&
this.homePageId !== this.selectedSnapshot.pageId;
this.collection?.homeUrlPageId !== this.selectedSnapshot.pageId;
// TODO get filename from rwp?
const fileName = `page-thumbnail_${this.selectedSnapshot?.pageId}.jpeg`;
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 { when } from "lit/directives/when.js";
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 { BtrixElement } from "@/classes/BtrixElement";
import type { Combobox } from "@/components/ui/combobox";
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
import type { Collection } from "@/types/collection";
import type { UnderlyingFunction } from "@/types/utils";
import { tw } from "@/utils/tailwind";
@ -39,7 +42,11 @@ export type SelectSnapshotDetail = {
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
@ -50,11 +57,8 @@ export class SelectCollectionStartPage extends BtrixElement {
@property({ type: String })
collectionId?: string;
@property({ type: String })
homeUrl?: string | null = null;
@property({ type: String })
homeTs?: string | null = null;
@property({ type: Object })
collection?: Collection;
@state()
private searchQuery = "";
@ -82,14 +86,13 @@ export class SelectCollectionStartPage extends BtrixElement {
return this.selectedSnapshot;
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("homeUrl") && this.homeUrl) {
if (this.input) {
this.input.value = this.homeUrl;
}
this.searchQuery = this.homeUrl;
void this.initSelection();
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("collection") && this.collection) {
void this.initSelection(this.collection);
}
}
updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selectedSnapshot")) {
this.dispatchEvent(
new CustomEvent<SelectSnapshotDetail>("btrix-select", {
@ -106,26 +109,50 @@ export class SelectCollectionStartPage extends BtrixElement {
}
}
private async initSelection() {
await this.updateComplete;
await this.searchResults.taskComplete;
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,
);
}
private async initSelection(collection: Collection) {
if (!collection.homeUrl && collection.pageCount !== 1) {
return;
}
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, {
task: async ([searchValue], { signal }) => {
const searchResults = await this.getPageUrls(
const pageUrls = await this.getPageUrls(
{
id: this.collectionId!,
urlPrefix: searchValue,
@ -133,7 +160,7 @@ export class SelectCollectionStartPage extends BtrixElement {
signal,
);
return searchResults;
return pageUrls;
},
args: () => [this.searchQuery] as const,
});
@ -150,6 +177,7 @@ export class SelectCollectionStartPage extends BtrixElement {
value=${this.selectedSnapshot?.pageId || ""}
?required=${this.selectedPage && !this.selectedSnapshot}
?disabled=${!this.selectedPage}
hoist
@sl-change=${async (e: SlChangeEvent) => {
const { value } = e.currentTarget as SlSelect;
@ -160,35 +188,22 @@ export class SelectCollectionStartPage extends BtrixElement {
);
}}
>
${when(
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>
`,
),
)}
${when(this.selectedPage, this.renderSnapshotOptions)}
</sl-select>
</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() {
let prefix: {
icon: string;
@ -223,7 +238,7 @@ export class SelectCollectionStartPage extends BtrixElement {
id="pageUrlInput"
label=${msg("Page URL")}
placeholder=${msg("Start typing a URL...")}
clearable
?clearable=${this.collection && this.collection.pageCount > 1}
@sl-focus=${() => {
this.resetInputValidity();
this.combobox?.show();
@ -295,7 +310,7 @@ export class SelectCollectionStartPage extends BtrixElement {
this.selectedSnapshot = undefined;
} else if (results.total === 1) {
// 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];
}
};
@ -326,11 +341,7 @@ export class SelectCollectionStartPage extends BtrixElement {
this.input.value = item.url;
}
this.selectedPage = {
...item,
// TODO check if backend can sort
snapshots: sortByTs(item.snapshots).reverse(),
};
this.selectedPage = this.formatPage(item);
this.combobox?.hide();

View File

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