From 23f9e08a224ee730a315721bd2cda93140e8843a Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 2 Apr 2025 17:45:27 -0700 Subject: [PATCH] feat: Add custom behaviors to workflow (#2520) Resolves https://github.com/webrecorder/browsertrix/issues/2151 Follows https://github.com/webrecorder/browsertrix/pull/2505 ## Changes - Allows users to set custom behaviors in workflow editor. - Allows one or more behaviors, as simple URL or Git URL to be added - Calls validation endpoint to check if URL is valid. --------- Co-authored-by: emma --- frontend/src/components/ui/config-details.ts | 10 + frontend/src/components/ui/copy-field.ts | 5 +- frontend/src/components/ui/url-input.ts | 25 +- frontend/src/controllers/api.ts | 1 + .../custom-behaviors-table-row.ts | 558 ++++++++++++++++++ .../crawl-workflows/custom-behaviors-table.ts | 164 +++++ .../src/features/crawl-workflows/index.ts | 1 + .../crawl-workflows/workflow-editor.ts | 57 +- frontend/src/pages/org/workflows-new.ts | 1 + .../src/strings/crawl-workflows/labels.ts | 1 + frontend/src/theme.stylesheet.css | 8 + frontend/src/types/api.ts | 7 +- frontend/src/types/crawler.ts | 1 + 13 files changed, 819 insertions(+), 20 deletions(-) create mode 100644 frontend/src/features/crawl-workflows/custom-behaviors-table-row.ts create mode 100644 frontend/src/features/crawl-workflows/custom-behaviors-table.ts diff --git a/frontend/src/components/ui/config-details.ts b/frontend/src/components/ui/config-details.ts index b2e0d2f4..bcf4dc08 100644 --- a/frontend/src/components/ui/config-details.ts +++ b/frontend/src/components/ui/config-details.ts @@ -178,6 +178,16 @@ export class ConfigDetails extends BtrixElement { .filter((v) => v) .join(", ") || none, )} + ${this.renderSetting( + labelFor.customBehaviors, + seedsConfig?.customBehaviors.length + ? html` + + ` + : none, + )} ${this.renderSetting( labelFor.pageLoadTimeoutSeconds, renderTimeLimit( diff --git a/frontend/src/components/ui/copy-field.ts b/frontend/src/components/ui/copy-field.ts index fbedc015..bbc71793 100644 --- a/frontend/src/components/ui/copy-field.ts +++ b/frontend/src/components/ui/copy-field.ts @@ -35,6 +35,9 @@ export class CopyField extends TailwindElement { @property({ type: Boolean }) hoist = false; + @property({ type: Boolean }) + border = true; + @property({ type: Boolean }) monostyle = true; @@ -65,7 +68,7 @@ export class CopyField extends TailwindElement {
{ - console.log("input 1"); - await this.updateComplete; - + private readonly onInput = () => { if (!this.checkValidity() && validURL(this.value)) { this.setCustomValidity(""); this.helpText = ""; } }; - private readonly onBlur = async () => { - await this.updateComplete; - - const value = this.value; + private readonly onChange = () => { + const value = this.value.trim(); if (value && !validURL(value)) { const text = msg("Please enter a valid URL."); diff --git a/frontend/src/controllers/api.ts b/frontend/src/controllers/api.ts index 884d435e..ac2d3acb 100644 --- a/frontend/src/controllers/api.ts +++ b/frontend/src/controllers/api.ts @@ -170,6 +170,7 @@ export class APIController implements ReactiveController { message: errorMessage, status: resp.status, details: errorDetails, + errorCode: errorDetail, }); } diff --git a/frontend/src/features/crawl-workflows/custom-behaviors-table-row.ts b/frontend/src/features/crawl-workflows/custom-behaviors-table-row.ts new file mode 100644 index 00000000..8e27f6fc --- /dev/null +++ b/frontend/src/features/crawl-workflows/custom-behaviors-table-row.ts @@ -0,0 +1,558 @@ +import { localized, msg } from "@lit/localize"; +import { Task } from "@lit/task"; +import type { + SlChangeEvent, + SlInput, + SlInputEvent, + SlSelect, +} from "@shoelace-style/shoelace"; +import clsx from "clsx"; +import { css, html, type PropertyValues } from "lit"; +import { + customElement, + property, + query, + queryAll, + state, +} from "lit/decorators.js"; +import { when } from "lit/directives/when.js"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import type { UrlInput } from "@/components/ui/url-input"; +import { notSpecified } from "@/layouts/empty"; +import { APIErrorDetail } from "@/types/api"; +import type { SeedConfig } from "@/types/crawler"; +import { APIError } from "@/utils/api"; +import { tw } from "@/utils/tailwind"; + +export type CustomBehaviors = SeedConfig["customBehaviors"]; +export type CustomBehaviorSource = CustomBehaviors[number]; +export enum CustomBehaviorType { + FileURL = "fileUrl", + GitRepo = "gitRepo", +} + +const ValidationErrorCodes = [ + APIErrorDetail.InvalidCustomBehavior, + APIErrorDetail.CustomBehaviorBranchNotFound, + APIErrorDetail.CustomBehaviorNotFound, +] as const; +type ValidationErrorCode = (typeof ValidationErrorCodes)[number]; +type RowValidation = { success: true }; + +type BehaviorBase = { + type: CustomBehaviorType; + url: string; + path?: string; + branch?: string; +}; + +export type BehaviorFileURL = BehaviorBase & { + type: CustomBehaviorType.FileURL; +}; + +export type BehaviorGitRepo = Required & { + type: CustomBehaviorType.GitRepo; +}; + +export type ChangeEventDetail = { + value: CustomBehaviorSource; +}; + +export type RemoveEventDetail = { + item: CustomBehaviorSource; +}; + +const labelFor: Record = { + [CustomBehaviorType.FileURL]: msg("URL"), + [CustomBehaviorType.GitRepo]: msg("Git Repo"), +}; + +const errorFor: Record = { + [APIErrorDetail.InvalidCustomBehavior]: msg("Please enter a valid URL"), + [APIErrorDetail.CustomBehaviorBranchNotFound]: msg( + "Please enter a valid branch", + ), + [APIErrorDetail.CustomBehaviorNotFound]: msg("Please enter an existing URL"), +}; + +const inputStyle = [ + tw`[--sl-input-background-color-hover:transparent] [--sl-input-background-color:transparent] [--sl-input-border-color-hover:transparent] [--sl-input-border-radius-medium:0] [--sl-input-spacing-medium:var(--sl-spacing-small)]`, + tw`data-[valid]:[--sl-input-border-color:transparent]`, + tw`part-[form-control-help-text]:mx-1 part-[form-control-help-text]:mb-1`, +]; + +const INPUT_CLASSNAME = "input" as const; +const INVALID_CLASSNAME = "invalid" as const; +export const GIT_PREFIX = "git+" as const; +export const isGitRepo = (url: CustomBehaviorSource) => + url.startsWith(GIT_PREFIX); +export const stringifyGitRepo = (behavior: BehaviorGitRepo): string => { + return `${GIT_PREFIX}${behavior.url}?branch=${behavior.branch}&path=${behavior.path}`; +}; +export const stringifyBehavior = (behavior: BehaviorBase): string => { + if (!behavior.url) return ""; + + if (behavior.type === CustomBehaviorType.GitRepo) { + return stringifyGitRepo(behavior as BehaviorGitRepo); + } + return behavior.url; +}; +const parseGitRepo = (repoUrl: string): Omit => { + const url = new URL(repoUrl.slice(GIT_PREFIX.length)); + + return { + url: `${url.origin}${url.pathname}`, + path: url.searchParams.get("path") || "", + branch: url.searchParams.get("branch") || "", + }; +}; +export const parseBehavior = (url: string): BehaviorBase => { + if (!url) { + return { + type: CustomBehaviorType.GitRepo, + url: "", + path: "", + branch: "", + }; + } + + if (isGitRepo(url)) { + try { + return { + type: CustomBehaviorType.GitRepo, + ...parseGitRepo(url), + }; + } catch { + return { + type: CustomBehaviorType.GitRepo, + url: "", + path: "", + branch: "", + }; + } + } + + return { + type: CustomBehaviorType.FileURL, + url, + }; +}; + +/** + * @fires btrix-change + * @fires btrix-invalid + * @fires btrix-remove + */ +@customElement("btrix-custom-behaviors-table-row") +@localized() +export class CustomBehaviorsTableRow extends BtrixElement { + static styles = css` + :host { + display: contents; + } + `; + + @property({ type: String }) + behaviorSource?: string; + + @property({ type: Boolean }) + editable = false; + + @state() + private behavior?: BehaviorBase; + + @queryAll(`.${INPUT_CLASSNAME}`) + private readonly inputs!: NodeListOf; + + @query(`#url`) + private readonly urlInput?: UrlInput | null; + + @query(`#branch`) + private readonly branchInput?: SlInput | null; + + @query(`#path`) + private readonly pathInput?: SlInput | null; + + public get taskComplete() { + return this.validateTask.taskComplete; + } + + public checkValidity(): boolean { + return ![...this.inputs].some((input) => !input.checkValidity()); + } + + public reportValidity(): boolean { + return ![...this.inputs].some((input) => !input.reportValidity()); + } + + private readonly validateTask = new Task(this, { + task: async ([behaviorSource], { signal }) => { + if (!behaviorSource) { + return null; + } + + try { + return await this.validateBehavior(behaviorSource, signal); + } catch (err) { + if ( + typeof err === "string" && + ValidationErrorCodes.includes(err as ValidationErrorCode) + ) { + this.setInputCustomValidity(err); + throw err; + } + + if (err instanceof Error && err.name === "AbortError") { + console.debug(err); + } else { + console.error(err); + } + } + }, + args: () => [this.behaviorSource] as const, + }); + + protected willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has("behaviorSource")) { + this.behavior = parseBehavior(this.behaviorSource || ""); + } + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.has("behavior") && this.behavior) { + this.dispatchEvent( + new CustomEvent("btrix-change", { + detail: { + value: stringifyBehavior(this.behavior), + }, + }), + ); + } + } + + render() { + const behavior = this.behavior; + + if (!behavior) return; + + return html` + + + ${this.renderType(behavior)} + + + ${behavior.type === CustomBehaviorType.GitRepo + ? this.renderGitRepoCell(behavior as BehaviorGitRepo) + : this.renderFileUrlCell(behavior as BehaviorFileURL)} + + ${when( + this.editable, + () => html` + + + this.dispatchEvent( + new CustomEvent("btrix-remove"), + )} + > + + `, + )} + + `; + } + + private renderType(behavior: BehaviorBase) { + if (!this.editable) { + return html`${labelFor[behavior.type]}`; + } + + return html` + { + const el = e.target as SlSelect; + + this.behavior = { + ...behavior, + type: el.value as CustomBehaviorType, + path: behavior.path || "", + branch: behavior.branch || "", + }; + }} + > + ${Object.values(CustomBehaviorType).map( + (behaviorType) => html` + + ${labelFor[behaviorType]} + + `, + )} + + `; + } + + private renderGitRepoCell(behavior: BehaviorGitRepo) { + const subgridStyle = tw`grid grid-cols-[max-content_1fr] border-t`; + const labelStyle = tw`flex inline-flex items-center justify-end border-r bg-neutral-50 p-2 text-xs leading-none text-neutral-700`; + const pathLabel = msg("Path"); + const branchLabel = msg("Branch"); + + if (!this.editable) { + return html` + ${this.renderReadonlyUrl(behavior)} +
+
${pathLabel}
+
${behavior.path || notSpecified}
+
${branchLabel}
+
${behavior.branch || notSpecified}
+
+ `; + } + + return html`${this.renderUrlInput(behavior, { + placeholder: msg("Enter URL to Git repository"), + })} +
+ +
+ ${this.renderGitDetailInput(behavior, { + placeholder: msg("Optional path"), + key: "path", + })} +
+ +
+ ${this.renderGitDetailInput(behavior, { + placeholder: msg("Optional branch"), + key: "branch", + })} +
+
`; + } + + private renderFileUrlCell(behavior: BehaviorFileURL) { + if (!this.editable) { + return this.renderReadonlyUrl(behavior); + } + + return this.renderUrlInput(behavior, { + placeholder: msg("Enter URL to JavaScript file"), + }); + } + + private renderReadonlyUrl(behavior: BehaviorBase) { + return html` + + + + + + + `; + } + + private renderUrlInput( + behavior: BehaviorBase, + { placeholder }: { placeholder: string }, + ) { + return html` + + this.dispatchEvent(new CustomEvent("btrix-invalid"))} + > + ${this.validateTask.render({ + pending: this.renderPendingValidation, + complete: this.renderValidTooltip, + error: this.renderInvalidTooltip, + })} + + `; + } + + private readonly renderPendingValidation = () => { + return html` +
+ +
+ `; + }; + + private readonly renderInvalidTooltip = (err: unknown) => { + const message = + typeof err === "string" && errorFor[err as ValidationErrorCode]; + + if (!message) { + console.debug("no message for error:", err); + return; + } + + return html` +
+ + + +
+ `; + }; + + private readonly renderValidTooltip = ( + validation: typeof this.validateTask.value, + ) => { + if (!validation) return; + + return html` +
+ + + +
+ `; + }; + + private renderGitDetailInput( + behavior: BehaviorGitRepo, + { placeholder, key }: { placeholder: string; key: "path" | "branch" }, + ) { + return html` + + this.dispatchEvent(new CustomEvent("btrix-invalid"))} + > + `; + } + + private readonly onInput = (e: SlInputEvent) => { + const el = e.target as SlInput; + + el.classList.remove(INVALID_CLASSNAME); + el.setCustomValidity(""); + }; + + private readonly onInputChangeForKey = + (behavior: BehaviorBase, key: string) => async (e: SlChangeEvent) => { + const el = e.target as SlInput; + const value = el.value.trim(); + + this.behavior = { + ...behavior, + [key]: value, + }; + }; + + private setInputCustomValidity(error: unknown) { + const updateValidity = ( + input: SlInput | null | undefined, + error?: ValidationErrorCode, + ) => { + if (!input) return; + + if (error) { + input.classList.add(INVALID_CLASSNAME); + } else { + input.classList.remove(INVALID_CLASSNAME); + } + + input.setCustomValidity(error ? errorFor[error] : ""); + }; + + switch (error) { + case APIErrorDetail.InvalidCustomBehavior: { + updateValidity(this.urlInput, APIErrorDetail.InvalidCustomBehavior); + updateValidity(this.branchInput); + updateValidity(this.pathInput); + break; + } + case APIErrorDetail.CustomBehaviorBranchNotFound: { + updateValidity( + this.branchInput, + APIErrorDetail.CustomBehaviorBranchNotFound, + ); + break; + } + case APIErrorDetail.CustomBehaviorNotFound: { + updateValidity(this.urlInput, APIErrorDetail.CustomBehaviorNotFound); + updateValidity(this.branchInput); + updateValidity(this.pathInput); + break; + } + default: + break; + } + } + + private async validateBehavior( + behaviorSource: CustomBehaviorSource, + signal: AbortSignal, + ): Promise { + try { + return await this.api.fetch( + `/orgs/${this.orgId}/crawlconfigs/validate/custom-behavior`, + { + method: "POST", + body: JSON.stringify({ + customBehavior: behaviorSource, + }), + signal, + }, + ); + } catch (err) { + if (err instanceof APIError) { + throw err.errorCode; + } + + throw err; + } + } +} diff --git a/frontend/src/features/crawl-workflows/custom-behaviors-table.ts b/frontend/src/features/crawl-workflows/custom-behaviors-table.ts new file mode 100644 index 00000000..4074576b --- /dev/null +++ b/frontend/src/features/crawl-workflows/custom-behaviors-table.ts @@ -0,0 +1,164 @@ +import { localized, msg } from "@lit/localize"; +import clsx from "clsx"; +import { html, type PropertyValues } from "lit"; +import { customElement, property, queryAll, state } from "lit/decorators.js"; +import { repeat } from "lit/directives/repeat.js"; +import { when } from "lit/directives/when.js"; +import { nanoid } from "nanoid"; +import { z } from "zod"; + +import { BtrixElement } from "@/classes/BtrixElement"; +import { + type CustomBehaviors, + type CustomBehaviorSource, + type CustomBehaviorsTableRow, + type ChangeEventDetail as RowChangeEventDetail, +} from "@/features/crawl-workflows/custom-behaviors-table-row"; +import { tw } from "@/utils/tailwind"; + +import "@/features/crawl-workflows/custom-behaviors-table-row"; + +type ChangeEventDetail = { + value: CustomBehaviors; +}; + +const rowIdSchema = z.string().nanoid(); +type RowId = z.infer; + +/** + * @fires btrix-change + * @fires btrix-invalid + */ +@customElement("btrix-custom-behaviors-table") +@localized() +export class CustomBehaviorsTable extends BtrixElement { + @property({ type: Array }) + customBehaviors: CustomBehaviors = []; + + @property({ type: Boolean }) + editable = false; + + @state() + private rows = new Map(); + + @queryAll("btrix-custom-behaviors-table-row") + private readonly rowElems!: NodeListOf; + + public get value(): CustomBehaviors { + return [...this.rows.values()].filter((v) => v); + } + + public get taskComplete() { + return Promise.all([...this.rowElems].map(async (row) => row.taskComplete)); + } + + public checkValidity(): boolean { + return ![...this.rowElems].some((row) => !row.checkValidity()); + } + + public reportValidity(): boolean { + return ![...this.rowElems].some((row) => !row.reportValidity()); + } + + protected willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has("customBehaviors")) { + if (!this.customBehaviors.length) { + const id = nanoid(); + this.rows = new Map([[id, ""]]); + } else { + // TODO Reuse IDs? + this.rows = new Map(this.customBehaviors.map((url) => [nanoid(), url])); + } + } + } + + protected updated(changedProperties: PropertyValues): void { + if (changedProperties.get("rows")) { + this.dispatchEvent( + new CustomEvent("btrix-change", { + detail: { + value: this.value, + }, + }), + ); + } + } + + render() { + return html` + + + ${msg("Source")} + + ${msg("Script Location")} + + ${when( + this.editable, + () => html` + + ${msg("Row actions")} + + `, + )} + + + ${repeat( + this.rows, + ([id]) => id, + (args) => this.renderRow(...args), + )} + + + ${when( + this.editable, + () => html` + this.addRow()}> + + ${msg("Add More")} + + `, + )} + `; + } + + private readonly renderRow = (id: RowId, url: CustomBehaviorSource) => { + return html` + this.removeRow(id)} + @btrix-change=${(e: CustomEvent) => { + const url = e.detail.value; + + this.rows = new Map(this.rows.set(id, url)); + }} + @btrix-invalid=${() => + this.dispatchEvent(new CustomEvent("btrix-invalid"))} + > + + `; + }; + + private addRow() { + const id = nanoid(); + + this.rows = new Map(this.rows.set(id, "")); + } + + private removeRow(id: RowId) { + this.rows.delete(id); + + if (this.rows.size === 0) { + this.addRow(); + } else { + this.rows = new Map(this.rows); + } + } +} diff --git a/frontend/src/features/crawl-workflows/index.ts b/frontend/src/features/crawl-workflows/index.ts index 98c3bdd6..69755955 100644 --- a/frontend/src/features/crawl-workflows/index.ts +++ b/frontend/src/features/crawl-workflows/index.ts @@ -1,3 +1,4 @@ +import("./custom-behaviors-table"); import("./exclusion-editor"); import("./live-workflow-status"); import("./link-selector-table"); diff --git a/frontend/src/features/crawl-workflows/workflow-editor.ts b/frontend/src/features/crawl-workflows/workflow-editor.ts index 642ae508..6157d087 100644 --- a/frontend/src/features/crawl-workflows/workflow-editor.ts +++ b/frontend/src/features/crawl-workflows/workflow-editor.ts @@ -52,6 +52,7 @@ import { } from "@/controllers/observable"; import { type SelectBrowserProfileChangeEvent } from "@/features/browser-profiles/select-browser-profile"; import type { CollectionsChangeEvent } from "@/features/collections/collections-add"; +import type { CustomBehaviorsTable } from "@/features/crawl-workflows/custom-behaviors-table"; import type { CrawlStatusChangedEventDetail } from "@/features/crawl-workflows/live-workflow-status"; import type { ExclusionChangeEvent, @@ -316,6 +317,9 @@ export class WorkflowEditor extends BtrixElement { @query("btrix-link-selector-table") private readonly linkSelectorTable?: LinkSelectorTable | null; + @query("btrix-custom-behaviors-table") + private readonly customBehaviorsTable?: CustomBehaviorsTable | null; + connectedCallback(): void { this.initializeEditor(); super.connectedCallback(); @@ -1259,6 +1263,7 @@ https://archiveweb.page/images/${"logo.svg"}`} ), false, )} + ${this.renderCustomBehaviors()} ${this.renderSectionHeading(msg("Page Timing"))} ${inputCol(html` { + this.customBehaviorsTable?.removeAttribute("data-invalid"); + this.customBehaviorsTable?.removeAttribute("data-user-invalid"); + }} + @btrix-invalid=${() => { + /** + * HACK Set data attribute manually so that + * table works with `syncTabErrorState` + * + * FIXME Should be fixed with + * https://github.com/webrecorder/browsertrix/issues/2497 + */ + this.customBehaviorsTable?.setAttribute("data-invalid", "true"); + this.customBehaviorsTable?.setAttribute( + "data-user-invalid", + "true", + ); + }} + >`, + )} + ${this.renderHelpTextCol( + msg( + `Enable custom page actions with behavior scripts. You can specify any publicly accessible URL or public Git repository.`, + ), + false, + )} + `; + } + private renderBrowserSettings() { if (!this.formState.lang) throw new Error("missing formstate.lang"); return html` @@ -1893,6 +1934,9 @@ https://archiveweb.page/images/${"logo.svg"}`} /** * HACK Set data attribute manually so that * exclusions table works with `syncTabErrorState` + * + * FIXME Should be fixed with + * https://github.com/webrecorder/browsertrix/issues/2497 */ private updateExclusionsValidity() { if (this.exclusionTable?.checkValidity() === false) { @@ -2065,6 +2109,14 @@ https://archiveweb.page/images/${"logo.svg"}`} private async save() { if (!this.formElem) return; + // Wait for custom behaviors validation to finish + try { + await this.customBehaviorsTable?.taskComplete; + } catch { + this.customBehaviorsTable?.reportValidity(); + return; + } + const isValid = await this.checkFormValidity(this.formElem); if (!isValid || this.formHasError) { @@ -2149,12 +2201,12 @@ https://archiveweb.page/images/${"logo.svg"}`} if (isApiErrorDetail(errorDetail)) { switch (errorDetail) { - case APIErrorDetail.WorkflowInvalidLinkSelector: + case APIErrorDetail.InvalidLinkSelector: errorDetailMessage = msg( "Page link selectors contain invalid selector or attribute", ); break; - case APIErrorDetail.WorkflowInvalidRegex: + case APIErrorDetail.InvalidRegex: errorDetailMessage = msg( "Page exclusion contains invalid regex", ); @@ -2280,6 +2332,7 @@ https://archiveweb.page/images/${"logo.svg"}`} selectLinks: this.linkSelectorTable?.value.length ? this.linkSelectorTable.value : DEFAULT_SELECT_LINKS, + customBehaviors: this.customBehaviorsTable?.value || [], }, crawlerChannel: this.formState.crawlerChannel || "default", proxyId: this.formState.proxyId, diff --git a/frontend/src/pages/org/workflows-new.ts b/frontend/src/pages/org/workflows-new.ts index 21830b05..51d78e6d 100644 --- a/frontend/src/pages/org/workflows-new.ts +++ b/frontend/src/pages/org/workflows-new.ts @@ -88,6 +88,7 @@ export class WorkflowsNew extends LiteElement { failOnFailedSeed: false, userAgent: null, selectLinks: DEFAULT_SELECT_LINKS, + customBehaviors: [], }, tags: [], crawlTimeout: null, diff --git a/frontend/src/strings/crawl-workflows/labels.ts b/frontend/src/strings/crawl-workflows/labels.ts index b398f6e7..1d62edd2 100644 --- a/frontend/src/strings/crawl-workflows/labels.ts +++ b/frontend/src/strings/crawl-workflows/labels.ts @@ -2,6 +2,7 @@ import { msg } from "@lit/localize"; export const labelFor = { behaviors: msg("Built-in Behaviors"), + customBehaviors: msg("Custom Behaviors"), autoscrollBehavior: msg("Autoscroll"), autoclickBehavior: msg("Autoclick"), pageLoadTimeoutSeconds: msg("Page Load Limit"), diff --git a/frontend/src/theme.stylesheet.css b/frontend/src/theme.stylesheet.css index ea87bab5..3bf67668 100644 --- a/frontend/src/theme.stylesheet.css +++ b/frontend/src/theme.stylesheet.css @@ -185,11 +185,19 @@ } /* Validation styles */ + /** + * FIXME Use [data-user-invalid] selector once following is fixed + * https://github.com/webrecorder/browsertrix/issues/2497 + */ + .invalid[data-invalid]:not([disabled])::part(base), + btrix-url-input[data-user-invalid]:not([disabled])::part(base), sl-input[data-user-invalid]:not([disabled])::part(base), sl-textarea[data-user-invalid]:not([disabled])::part(base) { border-color: var(--sl-color-danger-400); } + .invalid[data-invalid]:focus-within::part(base), + btrix-url-input[data-user-invalid]:focus-within::part(base), sl-input[data-user-invalid]:focus-within::part(base), sl-textarea[data-user-invalid]:focus-within::part(base) { box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-100); diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 2b34bee4..2491fb52 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -38,8 +38,11 @@ export type APISortQuery> = { // TODO Add all error codes // https://github.com/webrecorder/browsertrix/issues/2512 export enum APIErrorDetail { - WorkflowInvalidLinkSelector = "invalid_link_selector", - WorkflowInvalidRegex = "invalid_regex", + InvalidLinkSelector = "invalid_link_selector", + InvalidRegex = "invalid_regex", + InvalidCustomBehavior = "invalid_custom_behavior", + CustomBehaviorNotFound = "custom_behavior_not_found", + CustomBehaviorBranchNotFound = "custom_behavior_branch_not_found", } export const APIErrorDetailEnum = z.nativeEnum(APIErrorDetail); export type APIErrorDetailEnum = z.infer; diff --git a/frontend/src/types/crawler.ts b/frontend/src/types/crawler.ts index e5b3eac2..b16b5340 100644 --- a/frontend/src/types/crawler.ts +++ b/frontend/src/types/crawler.ts @@ -45,6 +45,7 @@ export type SeedConfig = Expand< depth?: number | null; userAgent?: string | null; selectLinks: string[]; + customBehaviors: string[]; } >;