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:
parent
318acaf5b3
commit
c53528332b
@ -17,6 +17,9 @@ enum HomeView {
|
|||||||
URL = "url",
|
URL = "url",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @fires btrix-change
|
||||||
|
*/
|
||||||
@localized()
|
@localized()
|
||||||
@customElement("btrix-collection-replay-dialog")
|
@customElement("btrix-collection-replay-dialog")
|
||||||
export class CollectionStartPageDialog extends BtrixElement {
|
export class CollectionStartPageDialog extends BtrixElement {
|
||||||
@ -82,13 +85,20 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const showTooltip =
|
||||||
|
this.homeView === HomeView.URL && !this.selectedSnapshot;
|
||||||
return html`
|
return html`
|
||||||
<btrix-dialog
|
<btrix-dialog
|
||||||
.label=${msg("Configure Replay Home")}
|
.label=${msg("Configure Replay Home")}
|
||||||
.open=${this.open}
|
.open=${this.open}
|
||||||
class="[--width:60rem]"
|
class="[--width:60rem]"
|
||||||
@sl-show=${() => (this.showContent = true)}
|
@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}
|
${this.showContent ? this.renderContent() : nothing}
|
||||||
<div slot="footer" class="flex items-center justify-between gap-3">
|
<div slot="footer" class="flex items-center justify-between gap-3">
|
||||||
@ -97,11 +107,15 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
size="small"
|
size="small"
|
||||||
@click=${() => void this.dialog?.hide()}
|
@click=${() => void this.dialog?.hide()}
|
||||||
>${msg("Cancel")}</sl-button
|
>${msg("Cancel")}</sl-button
|
||||||
|
>
|
||||||
|
<sl-tooltip
|
||||||
|
content=${msg("Choose a page snapshot")}
|
||||||
|
?disabled=${!showTooltip}
|
||||||
>
|
>
|
||||||
<sl-button
|
<sl-button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="small"
|
size="small"
|
||||||
?disabled=${!this.replayLoaded}
|
?disabled=${!this.replayLoaded || showTooltip}
|
||||||
?loading=${this.isSubmitting}
|
?loading=${this.isSubmitting}
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.form?.requestSubmit();
|
this.form?.requestSubmit();
|
||||||
@ -109,6 +123,7 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
>
|
>
|
||||||
${msg("Save")}
|
${msg("Save")}
|
||||||
</sl-button>
|
</sl-button>
|
||||||
|
</sl-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</btrix-dialog>
|
</btrix-dialog>
|
||||||
`;
|
`;
|
||||||
@ -144,12 +159,15 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
|
|
||||||
if (snapshot) {
|
if (snapshot) {
|
||||||
urlPreview = html`
|
urlPreview = html`
|
||||||
|
<sl-tooltip hoist>
|
||||||
<iframe
|
<iframe
|
||||||
class="inline-block size-full"
|
class="inline-block size-full"
|
||||||
id="thumbnailPreview"
|
id="thumbnailPreview"
|
||||||
src=${`/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`}
|
src=${`/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`}
|
||||||
>
|
>
|
||||||
</iframe>
|
</iframe>
|
||||||
|
<span slot="content" class="break-all">${snapshot.url}</span>
|
||||||
|
</sl-tooltip>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,6 +212,16 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
?disabled=${!this.replayLoaded}
|
?disabled=${!this.replayLoaded}
|
||||||
@sl-change=${(e: SlChangeEvent) => {
|
@sl-change=${(e: SlChangeEvent) => {
|
||||||
this.homeView = (e.currentTarget as SlSelect).value as HomeView;
|
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
|
${this.replayLoaded
|
||||||
@ -280,6 +308,12 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
const form = e.currentTarget as HTMLFormElement;
|
const form = e.currentTarget as HTMLFormElement;
|
||||||
const { homeView, useThumbnail } = serialize(form);
|
const { homeView, useThumbnail } = serialize(form);
|
||||||
|
|
||||||
|
if (homeView === HomeView.Pages && !this.homePageId) {
|
||||||
|
// No changes to save
|
||||||
|
this.open = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.isSubmitting = true;
|
this.isSubmitting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -288,6 +322,8 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
(homeView === HomeView.URL && this.selectedSnapshot?.pageId) || null,
|
(homeView === HomeView.URL && this.selectedSnapshot?.pageId) || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent("btrix-change"));
|
||||||
|
|
||||||
const shouldUpload =
|
const shouldUpload =
|
||||||
homeView === HomeView.URL &&
|
homeView === HomeView.URL &&
|
||||||
useThumbnail === "on" &&
|
useThumbnail === "on" &&
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type {
|
|||||||
SlInput,
|
SlInput,
|
||||||
SlSelect,
|
SlSelect,
|
||||||
} from "@shoelace-style/shoelace";
|
} from "@shoelace-style/shoelace";
|
||||||
|
import clsx from "clsx";
|
||||||
import { html, type PropertyValues } from "lit";
|
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";
|
||||||
@ -16,6 +17,7 @@ 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 { UnderlyingFunction } from "@/types/utils";
|
import type { UnderlyingFunction } from "@/types/utils";
|
||||||
|
import { tw } from "@/utils/tailwind";
|
||||||
|
|
||||||
type Snapshot = {
|
type Snapshot = {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -29,8 +31,10 @@ type Page = {
|
|||||||
snapshots: Snapshot[];
|
snapshots: Snapshot[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SnapshotItem = Snapshot & { url: string };
|
||||||
|
|
||||||
export type SelectSnapshotDetail = {
|
export type SelectSnapshotDetail = {
|
||||||
item: Snapshot & { url: string };
|
item: SnapshotItem | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PROTOCOL = "http";
|
const DEFAULT_PROTOCOL = "http";
|
||||||
@ -59,12 +63,15 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
private selectedPage?: Page;
|
private selectedPage?: Page;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private selectedSnapshot?: Snapshot;
|
public selectedSnapshot?: Snapshot;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private pageUrlError?: string;
|
||||||
|
|
||||||
@query("btrix-combobox")
|
@query("btrix-combobox")
|
||||||
private readonly combobox?: Combobox | null;
|
private readonly combobox?: Combobox | null;
|
||||||
|
|
||||||
@query("sl-input")
|
@query("#pageUrlInput")
|
||||||
private readonly input?: SlInput | null;
|
private readonly input?: SlInput | null;
|
||||||
|
|
||||||
public get page() {
|
public get page() {
|
||||||
@ -83,6 +90,20 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
this.searchQuery = this.homeUrl;
|
this.searchQuery = this.homeUrl;
|
||||||
void this.initSelection();
|
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() {
|
private async initSelection() {
|
||||||
@ -123,8 +144,11 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
${this.renderPageSearch()}
|
${this.renderPageSearch()}
|
||||||
<sl-select
|
<sl-select
|
||||||
label=${msg("Snapshot")}
|
label=${msg("Snapshot")}
|
||||||
placeholder="--"
|
placeholder=${this.selectedPage
|
||||||
|
? msg("Choose a snapshot")
|
||||||
|
: msg("Enter a page URL to choose snapshot")}
|
||||||
value=${this.selectedSnapshot?.pageId || ""}
|
value=${this.selectedSnapshot?.pageId || ""}
|
||||||
|
?required=${this.selectedPage && !this.selectedSnapshot}
|
||||||
?disabled=${!this.selectedPage}
|
?disabled=${!this.selectedPage}
|
||||||
@sl-change=${async (e: SlChangeEvent) => {
|
@sl-change=${async (e: SlChangeEvent) => {
|
||||||
const { value } = e.currentTarget as SlSelect;
|
const { value } = e.currentTarget as SlSelect;
|
||||||
@ -134,19 +158,6 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
this.selectedSnapshot = this.selectedPage?.snapshots.find(
|
this.selectedSnapshot = this.selectedPage?.snapshots.find(
|
||||||
({ pageId }) => pageId === value,
|
({ pageId }) => pageId === value,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.selectedSnapshot) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent<SelectSnapshotDetail>("btrix-select", {
|
|
||||||
detail: {
|
|
||||||
item: {
|
|
||||||
url: this.selectedPage!.url,
|
|
||||||
...this.selectedSnapshot,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${when(
|
${when(
|
||||||
@ -179,6 +190,29 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderPageSearch() {
|
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`
|
return html`
|
||||||
<btrix-combobox
|
<btrix-combobox
|
||||||
@request-close=${() => {
|
@request-close=${() => {
|
||||||
@ -186,26 +220,86 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<sl-input
|
<sl-input
|
||||||
|
id="pageUrlInput"
|
||||||
label=${msg("Page URL")}
|
label=${msg("Page URL")}
|
||||||
placeholder=${msg("Start typing a URL...")}
|
placeholder=${msg("Start typing a URL...")}
|
||||||
clearable
|
clearable
|
||||||
@sl-focus=${() => {
|
@sl-focus=${() => {
|
||||||
|
this.resetInputValidity();
|
||||||
this.combobox?.show();
|
this.combobox?.show();
|
||||||
}}
|
}}
|
||||||
@sl-clear=${async () => {
|
@sl-clear=${async () => {
|
||||||
|
this.resetInputValidity();
|
||||||
|
|
||||||
this.searchQuery = "";
|
this.searchQuery = "";
|
||||||
|
this.selectedPage = undefined;
|
||||||
|
this.selectedSnapshot = undefined;
|
||||||
}}
|
}}
|
||||||
@sl-input=${this.onSearchInput as UnderlyingFunction<
|
@sl-input=${this.onSearchInput as UnderlyingFunction<
|
||||||
typeof this.onSearchInput
|
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>
|
</sl-input>
|
||||||
${this.renderSearchResults()}
|
${this.renderSearchResults()}
|
||||||
</btrix-combobox>
|
</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() {
|
private renderSearchResults() {
|
||||||
return this.searchResults.render({
|
return this.searchResults.render({
|
||||||
pending: () => html`
|
pending: () => html`
|
||||||
@ -217,7 +311,7 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return html`
|
return html`
|
||||||
<sl-menu-item slot="menu-item" disabled>
|
<sl-menu-item slot="menu-item" disabled>
|
||||||
${msg("No matching pages found.")}
|
${msg("No matching page found.")}
|
||||||
</sl-menu-item>
|
</sl-menu-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -241,18 +335,6 @@ export class SelectCollectionStartPage extends BtrixElement {
|
|||||||
this.combobox?.hide();
|
this.combobox?.hide();
|
||||||
|
|
||||||
this.selectedSnapshot = this.selectedPage.snapshots[0];
|
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}
|
>${item.url}
|
||||||
</sl-menu-item>
|
</sl-menu-item>
|
||||||
|
|||||||
@ -241,15 +241,13 @@ export class CollectionDetail extends BtrixElement {
|
|||||||
|
|
||||||
<btrix-collection-replay-dialog
|
<btrix-collection-replay-dialog
|
||||||
?open=${this.openDialogName === "editStartPage"}
|
?open=${this.openDialogName === "editStartPage"}
|
||||||
@sl-hide=${async () => {
|
@btrix-change=${() => {
|
||||||
this.openDialogName = undefined;
|
|
||||||
|
|
||||||
// Don't do full refresh of rwp so that rwp-url-change fires
|
// Don't do full refresh of rwp so that rwp-url-change fires
|
||||||
this.isRwpLoaded = false;
|
this.isRwpLoaded = false;
|
||||||
|
|
||||||
await this.fetchCollection();
|
void this.fetchCollection();
|
||||||
await this.updateComplete;
|
|
||||||
}}
|
}}
|
||||||
|
@sl-hide=${async () => (this.openDialogName = undefined)}
|
||||||
collectionId=${this.collectionId}
|
collectionId=${this.collectionId}
|
||||||
.homeUrl=${this.collection?.homeUrl}
|
.homeUrl=${this.collection?.homeUrl}
|
||||||
.homePageId=${this.collection?.homeUrlPageId}
|
.homePageId=${this.collection?.homeUrlPageId}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user