fix: Validate collection page URL (#2291)

- Disables saving collection start page if valid snapshot is not
selected
- Shows full URL in page URL status check mark
- Shows error in page URL status exclamation mark
- Fixes pasting in URL
This commit is contained in:
sua yoo 2025-01-14 12:54:33 -08:00 committed by GitHub
parent 318acaf5b3
commit c53528332b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 169 additions and 53 deletions

View File

@ -17,6 +17,9 @@ enum HomeView {
URL = "url",
}
/**
* @fires btrix-change
*/
@localized()
@customElement("btrix-collection-replay-dialog")
export class CollectionStartPageDialog extends BtrixElement {
@ -82,13 +85,20 @@ export class CollectionStartPageDialog extends BtrixElement {
}
render() {
const showTooltip =
this.homeView === HomeView.URL && !this.selectedSnapshot;
return html`
<btrix-dialog
.label=${msg("Configure Replay Home")}
.open=${this.open}
class="[--width:60rem]"
@sl-show=${() => (this.showContent = true)}
@sl-after-hide=${() => (this.showContent = false)}
@sl-after-hide=${() => {
this.homeView = this.homeUrl ? HomeView.URL : HomeView.Pages;
this.isSubmitting = false;
this.selectedSnapshot = null;
this.showContent = false;
}}
>
${this.showContent ? this.renderContent() : nothing}
<div slot="footer" class="flex items-center justify-between gap-3">
@ -98,17 +108,22 @@ export class CollectionStartPageDialog extends BtrixElement {
@click=${() => void this.dialog?.hide()}
>${msg("Cancel")}</sl-button
>
<sl-button
variant="primary"
size="small"
?disabled=${!this.replayLoaded}
?loading=${this.isSubmitting}
@click=${() => {
this.form?.requestSubmit();
}}
<sl-tooltip
content=${msg("Choose a page snapshot")}
?disabled=${!showTooltip}
>
${msg("Save")}
</sl-button>
<sl-button
variant="primary"
size="small"
?disabled=${!this.replayLoaded || showTooltip}
?loading=${this.isSubmitting}
@click=${() => {
this.form?.requestSubmit();
}}
>
${msg("Save")}
</sl-button>
</sl-tooltip>
</div>
</btrix-dialog>
`;
@ -144,12 +159,15 @@ export class CollectionStartPageDialog extends BtrixElement {
if (snapshot) {
urlPreview = html`
<iframe
class="inline-block size-full"
id="thumbnailPreview"
src=${`/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`}
>
</iframe>
<sl-tooltip hoist>
<iframe
class="inline-block size-full"
id="thumbnailPreview"
src=${`/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`}
>
</iframe>
<span slot="content" class="break-all">${snapshot.url}</span>
</sl-tooltip>
`;
}
@ -194,6 +212,16 @@ export class CollectionStartPageDialog extends BtrixElement {
?disabled=${!this.replayLoaded}
@sl-change=${(e: SlChangeEvent) => {
this.homeView = (e.currentTarget as SlSelect).value as HomeView;
if (this.homeView === HomeView.Pages) {
if (
!this.homePageId ||
this.homePageId !== this.selectedSnapshot?.pageId
) {
// Reset unsaved selected snapshot
this.selectedSnapshot = null;
}
}
}}
>
${this.replayLoaded
@ -280,6 +308,12 @@ export class CollectionStartPageDialog extends BtrixElement {
const form = e.currentTarget as HTMLFormElement;
const { homeView, useThumbnail } = serialize(form);
if (homeView === HomeView.Pages && !this.homePageId) {
// No changes to save
this.open = false;
return;
}
this.isSubmitting = true;
try {
@ -288,6 +322,8 @@ export class CollectionStartPageDialog extends BtrixElement {
(homeView === HomeView.URL && this.selectedSnapshot?.pageId) || null,
});
this.dispatchEvent(new CustomEvent("btrix-change"));
const shouldUpload =
homeView === HomeView.URL &&
useThumbnail === "on" &&

View File

@ -5,6 +5,7 @@ import type {
SlInput,
SlSelect,
} from "@shoelace-style/shoelace";
import clsx from "clsx";
import { html, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { when } from "lit/directives/when.js";
@ -16,6 +17,7 @@ import { BtrixElement } from "@/classes/BtrixElement";
import type { Combobox } from "@/components/ui/combobox";
import type { APIPaginatedList, APIPaginationQuery } from "@/types/api";
import type { UnderlyingFunction } from "@/types/utils";
import { tw } from "@/utils/tailwind";
type Snapshot = {
pageId: string;
@ -29,8 +31,10 @@ type Page = {
snapshots: Snapshot[];
};
type SnapshotItem = Snapshot & { url: string };
export type SelectSnapshotDetail = {
item: Snapshot & { url: string };
item: SnapshotItem | null;
};
const DEFAULT_PROTOCOL = "http";
@ -59,12 +63,15 @@ export class SelectCollectionStartPage extends BtrixElement {
private selectedPage?: Page;
@state()
private selectedSnapshot?: Snapshot;
public selectedSnapshot?: Snapshot;
@state()
private pageUrlError?: string;
@query("btrix-combobox")
private readonly combobox?: Combobox | null;
@query("sl-input")
@query("#pageUrlInput")
private readonly input?: SlInput | null;
public get page() {
@ -83,6 +90,20 @@ export class SelectCollectionStartPage extends BtrixElement {
this.searchQuery = this.homeUrl;
void this.initSelection();
}
if (changedProperties.has("selectedSnapshot")) {
this.dispatchEvent(
new CustomEvent<SelectSnapshotDetail>("btrix-select", {
detail: {
item: this.selectedPage?.url
? ({
url: this.selectedPage.url,
...this.selectedSnapshot,
} as SnapshotItem)
: null,
},
}),
);
}
}
private async initSelection() {
@ -123,8 +144,11 @@ export class SelectCollectionStartPage extends BtrixElement {
${this.renderPageSearch()}
<sl-select
label=${msg("Snapshot")}
placeholder="--"
placeholder=${this.selectedPage
? msg("Choose a snapshot")
: msg("Enter a page URL to choose snapshot")}
value=${this.selectedSnapshot?.pageId || ""}
?required=${this.selectedPage && !this.selectedSnapshot}
?disabled=${!this.selectedPage}
@sl-change=${async (e: SlChangeEvent) => {
const { value } = e.currentTarget as SlSelect;
@ -134,19 +158,6 @@ export class SelectCollectionStartPage extends BtrixElement {
this.selectedSnapshot = this.selectedPage?.snapshots.find(
({ pageId }) => pageId === value,
);
if (this.selectedSnapshot) {
this.dispatchEvent(
new CustomEvent<SelectSnapshotDetail>("btrix-select", {
detail: {
item: {
url: this.selectedPage!.url,
...this.selectedSnapshot,
},
},
}),
);
}
}}
>
${when(
@ -179,6 +190,29 @@ export class SelectCollectionStartPage extends BtrixElement {
}
private renderPageSearch() {
let prefix: {
icon: string;
tooltip: string;
className?: string;
} = {
icon: "search",
tooltip: msg("Search for a page in this collection"),
};
if (this.pageUrlError) {
prefix = {
icon: "exclamation-lg",
tooltip: this.pageUrlError,
className: tw`text-danger`,
};
} else if (this.selectedPage) {
prefix = {
icon: "check-lg",
tooltip: msg("Page exists in collection"),
className: tw`text-success`,
};
}
return html`
<btrix-combobox
@request-close=${() => {
@ -186,26 +220,86 @@ export class SelectCollectionStartPage extends BtrixElement {
}}
>
<sl-input
id="pageUrlInput"
label=${msg("Page URL")}
placeholder=${msg("Start typing a URL...")}
clearable
@sl-focus=${() => {
this.resetInputValidity();
this.combobox?.show();
}}
@sl-clear=${async () => {
this.resetInputValidity();
this.searchQuery = "";
this.selectedPage = undefined;
this.selectedSnapshot = undefined;
}}
@sl-input=${this.onSearchInput as UnderlyingFunction<
typeof this.onSearchInput
>}
@sl-blur=${this.pageUrlOnBlur}
>
<sl-icon name="search" slot="prefix"></sl-icon>
<div slot="prefix" class="inline-flex items-center">
<sl-tooltip
hoist
content=${prefix.tooltip}
placement="bottom-start"
>
<sl-icon
name=${prefix.icon}
class=${clsx(tw`size-4 text-base`, prefix.className)}
></sl-icon>
</sl-tooltip>
</div>
</sl-input>
${this.renderSearchResults()}
</btrix-combobox>
`;
}
private resetInputValidity() {
this.pageUrlError = undefined;
this.input?.setCustomValidity("");
}
private readonly pageUrlOnBlur = async () => {
if (!this.searchQuery) return;
if (this.selectedPage) {
// Ensure input value matches the URL, e.g. if the user pressed
// backspace on an existing URL
if (this.searchQuery !== this.selectedPage.url && this.input) {
this.input.value = this.selectedPage.url;
}
return;
}
await this.searchResults.taskComplete;
const results = this.searchResults.value;
if (!results) return;
if (results.total === 0) {
if (this.input) {
this.pageUrlError = msg(
"Page not found in collection. Please check the URL and try again",
);
this.input.setCustomValidity(this.pageUrlError);
}
// Clear selection
this.selectedPage = undefined;
this.selectedSnapshot = undefined;
} else if (results.total === 1) {
// Choose only option, e.g. for copy-paste
this.selectedPage = this.searchResults.value.items[0];
this.selectedSnapshot = this.selectedPage.snapshots[0];
}
};
private renderSearchResults() {
return this.searchResults.render({
pending: () => html`
@ -217,7 +311,7 @@ export class SelectCollectionStartPage extends BtrixElement {
if (!items.length) {
return html`
<sl-menu-item slot="menu-item" disabled>
${msg("No matching pages found.")}
${msg("No matching page found.")}
</sl-menu-item>
`;
}
@ -241,18 +335,6 @@ export class SelectCollectionStartPage extends BtrixElement {
this.combobox?.hide();
this.selectedSnapshot = this.selectedPage.snapshots[0];
await this.updateComplete;
this.dispatchEvent(
new CustomEvent<SelectSnapshotDetail>("btrix-select", {
detail: {
item: {
url: this.selectedPage.url,
...this.selectedSnapshot,
},
},
}),
);
}}
>${item.url}
</sl-menu-item>

View File

@ -241,15 +241,13 @@ export class CollectionDetail extends BtrixElement {
<btrix-collection-replay-dialog
?open=${this.openDialogName === "editStartPage"}
@sl-hide=${async () => {
this.openDialogName = undefined;
@btrix-change=${() => {
// Don't do full refresh of rwp so that rwp-url-change fires
this.isRwpLoaded = false;
await this.fetchCollection();
await this.updateComplete;
void this.fetchCollection();
}}
@sl-hide=${async () => (this.openDialogName = undefined)}
collectionId=${this.collectionId}
.homeUrl=${this.collection?.homeUrl}
.homePageId=${this.collection?.homeUrlPageId}