feat: Enable viewing all workflow form sections at once (#2310)
- Displays workflow form as collapsible sections - Combines run now toggle into submit - Fixes exclusion field errors not preventing form submission - Refactors `<btrix-observable>` into new `Observable` controller --------- Co-authored-by: emma <hi@emma.cafe>
This commit is contained in:
parent
83211b2f19
commit
18e72262dd
@ -1,4 +1,3 @@
|
||||
import clsx from "clsx";
|
||||
import { html, type PropertyValues } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
@ -10,7 +9,7 @@ import type { TabClickDetail, TabGroupTab } from "./tab";
|
||||
import { type TabGroupPanel } from "./tab-panel";
|
||||
|
||||
import { TailwindElement } from "@/classes/TailwindElement";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
import { pageSectionsWithNav } from "@/layouts/pageSectionsWithNav";
|
||||
|
||||
/**
|
||||
* @example Usage:
|
||||
@ -33,6 +32,10 @@ export class TabGroup extends TailwindElement {
|
||||
@property({ type: String })
|
||||
placement: "top" | "start" = "top";
|
||||
|
||||
/* Nav sticky */
|
||||
@property({ type: Boolean })
|
||||
sticky = true;
|
||||
|
||||
@property({ type: String, noAccessor: true, reflect: true })
|
||||
role = "tablist";
|
||||
|
||||
@ -61,29 +64,16 @@ export class TabGroup extends TailwindElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class=${clsx(
|
||||
tw`flex flex-col`,
|
||||
this.placement === "start" && tw`gap-8 lg:flex-row`,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class=${clsx(
|
||||
tw`flex flex-1 flex-col gap-2`,
|
||||
this.placement === "start"
|
||||
? tw`lg:sticky lg:top-2 lg:max-w-[16.5rem] lg:self-start`
|
||||
: tw`lg:flex-row`,
|
||||
)}
|
||||
@keydown=${this.onKeyDown}
|
||||
>
|
||||
<slot name="nav" @btrix-select-tab=${this.onSelectTab}></slot>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return pageSectionsWithNav({
|
||||
nav: html`<slot
|
||||
name="nav"
|
||||
@btrix-select-tab=${this.onSelectTab}
|
||||
@keydown=${this.onKeyDown}
|
||||
></slot>`,
|
||||
main: html`<slot></slot>`,
|
||||
placement: this.placement,
|
||||
sticky: this.sticky,
|
||||
});
|
||||
}
|
||||
|
||||
private handleActiveChange() {
|
||||
|
@ -8,50 +8,10 @@ const DEFAULT_PANEL_ID = "default-panel";
|
||||
// postcss-lit-disable-next-line
|
||||
export const TWO_COL_SCREEN_MIN_CSS = css`64.5rem`;
|
||||
|
||||
/**
|
||||
* @deprecated Use `btrix-tab-group`
|
||||
*
|
||||
* Tab list
|
||||
*
|
||||
* Usage example:
|
||||
* ```ts
|
||||
* <btrix-tab-list activePanel="one">
|
||||
* <btrix-tab slot="nav" name="one">One</btrix-tab>
|
||||
* <btrix-tab slot="nav" name="two">Two</btrix-tab>
|
||||
* </btrix-tab-list>
|
||||
*
|
||||
* <btrix-tab-panel name="one">Tab one content</btrix-tab-panel>
|
||||
* <btrix-tab-panel name="two">Tab two content</btrix-tab-panel>
|
||||
* ```
|
||||
*/
|
||||
const tabTagName = "btrix-tab-list-tab" as const;
|
||||
|
||||
@customElement("btrix-tab-panel")
|
||||
export class TabPanel extends TailwindElement {
|
||||
@property({ type: String })
|
||||
name?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
active = false;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="flex-auto aria-hidden:hidden"
|
||||
role="tabpanel"
|
||||
id=${ifDefined(this.name)}
|
||||
aria-hidden=${!this.active}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `btrix-tab-group`
|
||||
*/
|
||||
@customElement("btrix-tab")
|
||||
export class Tab extends TailwindElement {
|
||||
@customElement(tabTagName)
|
||||
export class TabListTab extends TailwindElement {
|
||||
// ID of panel the tab labels/controls
|
||||
@property({ type: String })
|
||||
name?: string;
|
||||
@ -65,7 +25,7 @@ export class Tab extends TailwindElement {
|
||||
render() {
|
||||
return html`
|
||||
<li
|
||||
class="cursor-pointer px-3 py-4 font-semibold leading-tight text-neutral-500 transition-colors duration-fast aria-disabled:cursor-default aria-selected:text-primary-600"
|
||||
class="cursor-pointer p-3 font-semibold leading-tight text-neutral-500 transition-colors duration-fast aria-disabled:cursor-default aria-selected:text-primary-600"
|
||||
role="tab"
|
||||
aria-selected=${this.active}
|
||||
aria-controls=${ifDefined(this.name)}
|
||||
@ -78,11 +38,18 @@ export class Tab extends TailwindElement {
|
||||
}
|
||||
}
|
||||
|
||||
type TabElement = Tab & HTMLElement;
|
||||
type TabPanelElement = TabPanel & HTMLElement;
|
||||
type TabElement = TabListTab & HTMLElement;
|
||||
|
||||
/**
|
||||
* @deprecated Use `btrix-tab-group`
|
||||
* Tab list with indicator
|
||||
*
|
||||
* Usage example:
|
||||
* ```ts
|
||||
* <btrix-tab-list tab="one">
|
||||
* <btrix-tab name="one">One</btrix-tab>
|
||||
* <btrix-tab name="two">Two</btrix-tab>
|
||||
* </btrix-tab-list>
|
||||
* ```
|
||||
*/
|
||||
@customElement("btrix-tab-list")
|
||||
export class TabList extends TailwindElement {
|
||||
@ -91,30 +58,6 @@ export class TabList extends TailwindElement {
|
||||
--track-width: 4px;
|
||||
}
|
||||
|
||||
.btrix-tab-list-container {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"menu"
|
||||
"header"
|
||||
"main";
|
||||
grid-template-columns: 1fr;
|
||||
grid-column-gap: 1.5rem;
|
||||
grid-row-gap: 1rem;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) {
|
||||
.btrix-tab-list-container {
|
||||
grid-template-areas:
|
||||
". header"
|
||||
"menu main";
|
||||
grid-template-columns: 16.5rem 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.navWrapper {
|
||||
grid-area: menu;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) {
|
||||
.navWrapper {
|
||||
overflow: initial;
|
||||
@ -173,7 +116,7 @@ export class TabList extends TailwindElement {
|
||||
position: absolute;
|
||||
width: var(--track-width);
|
||||
border-radius: var(--track-width);
|
||||
background-color: var(--sl-color-primary-600);
|
||||
background-color: var(--sl-color-primary-500);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: ${TWO_COL_SCREEN_MIN_CSS}) {
|
||||
@ -184,9 +127,9 @@ export class TabList extends TailwindElement {
|
||||
}
|
||||
`;
|
||||
|
||||
// ID of visible panel
|
||||
// ID of active tab
|
||||
@property({ type: String })
|
||||
activePanel: string = DEFAULT_PANEL_ID;
|
||||
tab: string = DEFAULT_PANEL_ID;
|
||||
|
||||
// If panels are linear, the current panel in progress
|
||||
@property({ type: String })
|
||||
@ -202,8 +145,8 @@ export class TabList extends TailwindElement {
|
||||
private readonly indicatorElem!: HTMLElement;
|
||||
|
||||
updated(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("activePanel") && this.activePanel) {
|
||||
this.onActiveChange(!changedProperties.get("activePanel"));
|
||||
if (changedProperties.has("tab") && this.tab) {
|
||||
this.onActiveChange(!changedProperties.get("tab"));
|
||||
}
|
||||
if (changedProperties.has("progressPanel") && this.progressPanel) {
|
||||
this.onProgressChange(!changedProperties.get("progressPanel"));
|
||||
@ -235,15 +178,7 @@ export class TabList extends TailwindElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="btrix-tab-list-container">
|
||||
<div class="navWrapper min-w-0">${this.renderNav()}</div>
|
||||
<div class="header"><slot name="header"></slot></div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return html`<div class="navWrapper min-w-0">${this.renderNav()}</div>`;
|
||||
}
|
||||
|
||||
renderNav() {
|
||||
@ -256,35 +191,26 @@ export class TabList extends TailwindElement {
|
||||
class="nav ${this.progressPanel ? "linear" : "nonlinear"} ${this
|
||||
.hideIndicator
|
||||
? "hide-indicator"
|
||||
: "show-indicator"} -m-3 overflow-x-hidden p-3"
|
||||
: "show-indicator"} -mx-3 overflow-x-hidden px-3"
|
||||
>
|
||||
<div class="track" role="presentation">
|
||||
<div class="indicator" role="presentation"></div>
|
||||
</div>
|
||||
|
||||
<ul class="tablist -m-3 overflow-x-auto p-3" role="tablist">
|
||||
<slot name="nav"></slot>
|
||||
<ul class="tablist -mx-3 overflow-x-auto px-3" role="tablist">
|
||||
<slot></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</sl-resize-observer>
|
||||
`;
|
||||
}
|
||||
|
||||
private getPanels(): TabPanelElement[] {
|
||||
const slotElems = this.renderRoot
|
||||
.querySelector<HTMLSlotElement>(".content slot:not([name])")!
|
||||
.assignedElements();
|
||||
return ([...slotElems] as TabPanelElement[]).filter(
|
||||
(el) => el.tagName.toLowerCase() === "btrix-tab-panel",
|
||||
);
|
||||
}
|
||||
|
||||
private getTabs(): TabElement[] {
|
||||
const slotElems = this.renderRoot
|
||||
.querySelector<HTMLSlotElement>("slot[name='nav']")!
|
||||
.querySelector<HTMLSlotElement>("slot")!
|
||||
.assignedElements();
|
||||
return ([...slotElems] as TabElement[]).filter(
|
||||
(el) => el.tagName.toLowerCase() === "btrix-tab",
|
||||
(el) => el.tagName.toLowerCase() === tabTagName,
|
||||
);
|
||||
}
|
||||
|
||||
@ -305,7 +231,7 @@ export class TabList extends TailwindElement {
|
||||
|
||||
private onActiveChange(isFirstChange: boolean) {
|
||||
this.getTabs().forEach((tab) => {
|
||||
if (tab.name === this.activePanel) {
|
||||
if (tab.name === this.tab) {
|
||||
tab.active = true;
|
||||
|
||||
if (!this.progressPanel) {
|
||||
@ -315,15 +241,5 @@ export class TabList extends TailwindElement {
|
||||
tab.active = false;
|
||||
}
|
||||
});
|
||||
this.getPanels().forEach((panel) => {
|
||||
panel.active = panel.name === this.activePanel;
|
||||
if (panel.active) {
|
||||
panel.style.display = "flex";
|
||||
panel.setAttribute("aria-hidden", "false");
|
||||
} else {
|
||||
panel.style.display = "none";
|
||||
panel.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +1,32 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, query } from "lit/decorators.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
type IntersectionEventDetail = {
|
||||
entry: IntersectionObserverEntry;
|
||||
};
|
||||
export type IntersectEvent = CustomEvent<IntersectionEventDetail>;
|
||||
import { ObservableController } from "@/controllers/observable";
|
||||
|
||||
/**
|
||||
* Observe element with Intersection Observer API.
|
||||
*
|
||||
* @example Usage:
|
||||
* ```
|
||||
* <btrix-observable @intersect=${console.log}>
|
||||
* <btrix-observable @btrix-intersect=${console.log}>
|
||||
* Observe me!
|
||||
* </btrix-observable>
|
||||
* ```
|
||||
*
|
||||
* @event intersect { entry: IntersectionObserverEntry }
|
||||
* @fires btrix-intersect IntersectionEventDetail
|
||||
*/
|
||||
@customElement("btrix-observable")
|
||||
export class Observable extends LitElement {
|
||||
@query(".target")
|
||||
private readonly target?: HTMLElement;
|
||||
@property({ type: Object })
|
||||
options?: IntersectionObserverInit;
|
||||
|
||||
private observer?: IntersectionObserver;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.observer = new IntersectionObserver(this.handleIntersect);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.observer?.disconnect();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
private readonly observable = new ObservableController(this);
|
||||
|
||||
firstUpdated() {
|
||||
this.observer?.observe(this.target!);
|
||||
this.observable.observe(this);
|
||||
}
|
||||
|
||||
private readonly handleIntersect = ([entry]: IntersectionObserverEntry[]) => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<IntersectionEventDetail>("intersect", {
|
||||
detail: { entry },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`<div class="target"><slot></slot></div>`;
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
|
50
frontend/src/controllers/observable.ts
Normal file
50
frontend/src/controllers/observable.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
|
||||
type IntersectionEventDetail = {
|
||||
entries: IntersectionObserverEntry[];
|
||||
};
|
||||
export type IntersectEvent = CustomEvent<IntersectionEventDetail>;
|
||||
|
||||
/**
|
||||
* Observe one or more elements with Intersection Observer API.
|
||||
*
|
||||
* @fires btrix-intersect IntersectionEventDetail
|
||||
*/
|
||||
export class ObservableController implements ReactiveController {
|
||||
private readonly host: ReactiveControllerHost & EventTarget;
|
||||
|
||||
private observer?: IntersectionObserver;
|
||||
private readonly observerOptions?: IntersectionObserverInit;
|
||||
|
||||
constructor(
|
||||
host: ObservableController["host"],
|
||||
options?: IntersectionObserverInit,
|
||||
) {
|
||||
this.host = host;
|
||||
this.observerOptions = options;
|
||||
host.addController(this);
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.observer = new IntersectionObserver(
|
||||
this.handleIntersect,
|
||||
this.observerOptions,
|
||||
);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
this.observer?.disconnect();
|
||||
}
|
||||
|
||||
public observe(target: Element) {
|
||||
this.observer?.observe(target);
|
||||
}
|
||||
|
||||
private readonly handleIntersect = (entries: IntersectionObserverEntry[]) => {
|
||||
this.host.dispatchEvent(
|
||||
new CustomEvent<IntersectionEventDetail>("btrix-intersect", {
|
||||
detail: { entries },
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
@ -10,6 +10,7 @@ import { when } from "lit/directives/when.js";
|
||||
import throttle from "lodash/fp/throttle";
|
||||
|
||||
import { BtrixElement } from "@/classes/BtrixElement";
|
||||
import type { IntersectEvent } from "@/controllers/observable";
|
||||
|
||||
type Pages = string[];
|
||||
type ResponseData = {
|
||||
@ -201,7 +202,7 @@ export class CrawlQueue extends BtrixElement {
|
||||
${msg("End of queue")}
|
||||
</div>`,
|
||||
() => html`
|
||||
<btrix-observable @intersect=${this.onLoadMoreIntersect}>
|
||||
<btrix-observable @btrix-intersect=${this.onLoadMoreIntersect}>
|
||||
<div class="py-3">
|
||||
<sl-icon-button
|
||||
name="three-dots"
|
||||
@ -232,8 +233,8 @@ export class CrawlQueue extends BtrixElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly onLoadMoreIntersect = throttle(50)((e: CustomEvent) => {
|
||||
if (!e.detail.entry.isIntersecting) return;
|
||||
private readonly onLoadMoreIntersect = throttle(50)((e: IntersectEvent) => {
|
||||
if (!e.detail.entries[0].isIntersecting) return;
|
||||
this.loadMore();
|
||||
}) as (e: CustomEvent) => void;
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { localized, msg, str } from "@lit/localize";
|
||||
import type { SlInput, SlSelect } from "@shoelace-style/shoelace";
|
||||
import clsx from "clsx";
|
||||
import { css, html, type PropertyValues, type TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { when } from "lit/directives/when.js";
|
||||
import { html as staticHtml, unsafeStatic } from "lit/static-html.js";
|
||||
@ -12,18 +14,17 @@ import { TailwindElement } from "@/classes/TailwindElement";
|
||||
import { type PageChangeEvent } from "@/components/ui/pagination";
|
||||
import type { SeedConfig } from "@/pages/org/types";
|
||||
import { regexEscape, regexUnescape } from "@/utils/string";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
|
||||
export type ExclusionChangeEvent = CustomEvent<{
|
||||
export type ExclusionChangeEventDetail = {
|
||||
index: number;
|
||||
regex: string;
|
||||
}>;
|
||||
valid?: boolean;
|
||||
};
|
||||
|
||||
export type ExclusionRemoveEvent = CustomEvent<{
|
||||
index: number;
|
||||
regex: string;
|
||||
}>;
|
||||
export type ExclusionChangeEvent = CustomEvent<ExclusionChangeEventDetail>;
|
||||
|
||||
type SLInputElement = HTMLInputElement & { invalid: boolean };
|
||||
export type ExclusionRemoveEvent = CustomEvent<ExclusionChangeEventDetail>;
|
||||
|
||||
const MIN_LENGTH = 2;
|
||||
|
||||
@ -54,12 +55,6 @@ function formatValue(type: Exclusion["type"], value: Exclusion["value"]) {
|
||||
@localized()
|
||||
export class QueueExclusionTable extends TailwindElement {
|
||||
static styles = css`
|
||||
sl-input {
|
||||
--sl-input-border-radius-medium: 0;
|
||||
--sl-input-font-family: var(--sl-font-mono);
|
||||
--sl-input-spacing-medium: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
sl-input:not([data-invalid]) {
|
||||
--sl-input-border-width: 0;
|
||||
}
|
||||
@ -100,6 +95,19 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
@state()
|
||||
private exclusionToRemove?: string;
|
||||
|
||||
@queryAll("sl-input")
|
||||
private readonly inputs!: NodeListOf<SlInput>;
|
||||
|
||||
public reportValidity() {
|
||||
this.inputs.forEach((input) => {
|
||||
input.reportValidity();
|
||||
});
|
||||
}
|
||||
|
||||
public checkValidity() {
|
||||
return ![...this.inputs].some((input) => !input.validity.valid);
|
||||
}
|
||||
|
||||
private get total() {
|
||||
return this.exclusions?.length;
|
||||
}
|
||||
@ -187,11 +195,11 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
${msg("Exclusion Value")}
|
||||
</th>
|
||||
<th class="${actionColClass} w-10 bg-slate-50 px-2 font-normal">
|
||||
<span class="sr-only">Row actions</span>
|
||||
<span class="sr-only">${msg("Row actions")}</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="align-top">
|
||||
${this.results.map(this.renderItem)}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -220,12 +228,12 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
<tr
|
||||
class="${this.exclusionToRemove === exclusion.value
|
||||
? "text-neutral-200"
|
||||
: "text-neutral-600"} h-10"
|
||||
: "text-neutral-600"}"
|
||||
>
|
||||
<td class="${typeColClass} whitespace-nowrap">
|
||||
${this.renderType({ exclusion, index })}
|
||||
</td>
|
||||
<td class="${valueColClass} font-mono">
|
||||
<td class="${valueColClass}">
|
||||
${this.renderValue({ exclusion, index })}
|
||||
</td>
|
||||
<td class="${actionColClass} text-center text-[1rem]">
|
||||
@ -266,11 +274,23 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
return html`
|
||||
<sl-select
|
||||
placeholder=${msg("Select Type")}
|
||||
class="my-1"
|
||||
size="small"
|
||||
.value=${exclusion.type}
|
||||
value=${exclusion.type}
|
||||
@sl-hide=${this.stopProp}
|
||||
@sl-after-hide=${this.stopProp}
|
||||
@sl-change=${(e: Event) => {
|
||||
const inputElem = (e.target as SlSelect)
|
||||
.closest("tr")
|
||||
?.querySelector("sl-input");
|
||||
|
||||
if (inputElem) {
|
||||
this.checkInputValidity(inputElem);
|
||||
this.reportInputValidity(inputElem);
|
||||
} else {
|
||||
console.debug("no inputElem for ", e.target);
|
||||
}
|
||||
|
||||
void this.updateExclusion({
|
||||
type: (e.target as HTMLSelectElement).value as Exclusion["type"],
|
||||
value: exclusion.value,
|
||||
@ -302,7 +322,13 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
<sl-input
|
||||
name="exclusion-${index}"
|
||||
placeholder=${msg("Enter value")}
|
||||
class="m-0"
|
||||
class=${clsx(
|
||||
tw`m-0`,
|
||||
tw`[--sl-input-border-radius-medium:0] [--sl-input-spacing-medium:var(--sl-spacing-small)]`,
|
||||
tw`part-[form-control-help-text]:mx-1 part-[form-control-help-text]:mb-1`,
|
||||
exclusion.type === "regex" &&
|
||||
tw`[--sl-input-font-family:var(--sl-font-mono)]`,
|
||||
)}
|
||||
value=${exclusion.value}
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
@ -315,16 +341,16 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
});
|
||||
}}
|
||||
@sl-input=${(e: CustomEvent) => {
|
||||
const inputElem = e.target as SLInputElement;
|
||||
const validityMessage = this.getInputValidity(inputElem) || "";
|
||||
|
||||
inputElem.classList.remove("invalid");
|
||||
inputElem.setCustomValidity(validityMessage);
|
||||
const inputElem = e.target as SlInput;
|
||||
|
||||
this.checkInputValidity(inputElem);
|
||||
this.checkSiblingRowValidity(e);
|
||||
}}
|
||||
@sl-change=${(e: CustomEvent) => {
|
||||
const inputElem = e.target as SLInputElement;
|
||||
const inputElem = e.target as SlInput;
|
||||
|
||||
this.reportInputValidity(inputElem);
|
||||
|
||||
const values = this.getCurrentValues(inputElem);
|
||||
const params = {
|
||||
type: values.type || exclusion.type,
|
||||
@ -332,11 +358,6 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
index,
|
||||
};
|
||||
|
||||
if (!inputElem.checkValidity()) {
|
||||
inputElem.classList.add("invalid");
|
||||
}
|
||||
inputElem.reportValidity();
|
||||
|
||||
void this.updateExclusion(params);
|
||||
}}
|
||||
></sl-input>
|
||||
@ -344,7 +365,7 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
}
|
||||
|
||||
if (exclusion.type === "regex") {
|
||||
value = staticHtml`<span class="regex">${unsafeStatic(
|
||||
value = staticHtml`<span class="regex ${tw`font-mono`}">${unsafeStatic(
|
||||
new RegexColorize().colorizeText(exclusion.value) as string,
|
||||
)}</span>`;
|
||||
}
|
||||
@ -352,6 +373,27 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
return value;
|
||||
}
|
||||
|
||||
private checkInputValidity(inputElem: SlInput) {
|
||||
const validityMessage = this.getInputValidity(inputElem) || "";
|
||||
|
||||
inputElem.setCustomValidity(validityMessage);
|
||||
|
||||
if (inputElem.classList.contains("invalid")) {
|
||||
// Update help text on change
|
||||
this.reportInputValidity(inputElem);
|
||||
}
|
||||
}
|
||||
|
||||
private reportInputValidity(inputElem: SlInput) {
|
||||
if (inputElem.validationMessage) {
|
||||
inputElem.classList.add("invalid");
|
||||
} else {
|
||||
inputElem.classList.remove("invalid");
|
||||
}
|
||||
|
||||
inputElem.helpText = inputElem.validationMessage;
|
||||
}
|
||||
|
||||
private getColumnClassNames(
|
||||
index: number,
|
||||
count: number,
|
||||
@ -398,7 +440,7 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
return [typeColClass, valueColClass, actionColClass];
|
||||
}
|
||||
|
||||
private getCurrentValues(inputElem: SLInputElement) {
|
||||
private getCurrentValues(inputElem: SlInput) {
|
||||
// Get latest exclusion type value from select
|
||||
const typeSelectElem = inputElem.closest("tr")?.querySelector("sl-select");
|
||||
const exclusionType = typeSelectElem?.value;
|
||||
@ -408,7 +450,7 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
};
|
||||
}
|
||||
|
||||
private getInputDuplicateValidity(inputElem: SLInputElement) {
|
||||
private getInputDuplicateValidity(inputElem: SlInput) {
|
||||
const siblingElems = inputElem
|
||||
.closest("table")
|
||||
?.querySelectorAll(`sl-input:not([name="${inputElem.name}"])`);
|
||||
@ -417,7 +459,7 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
return;
|
||||
}
|
||||
const siblingValues = Array.from(siblingElems).map(
|
||||
(elem) => (elem as SLInputElement).value,
|
||||
(elem) => (elem as SlInput).value,
|
||||
);
|
||||
const { type, value } = this.getCurrentValues(inputElem);
|
||||
const formattedValue = formatValue(type!, value);
|
||||
@ -426,10 +468,24 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
}
|
||||
}
|
||||
|
||||
private getInputValidity(inputElem: SLInputElement): string | void {
|
||||
private getInputValidity(inputElem: SlInput): string | void {
|
||||
const { type, value } = this.getCurrentValues(inputElem);
|
||||
if (!value) return;
|
||||
|
||||
const validityMessage = this.getValidityMessage({ type, value });
|
||||
|
||||
if (validityMessage) return validityMessage;
|
||||
|
||||
return this.getInputDuplicateValidity(inputElem);
|
||||
}
|
||||
|
||||
private getValidityMessage({
|
||||
type,
|
||||
value,
|
||||
}: {
|
||||
type?: Exclusion["type"];
|
||||
value: string;
|
||||
}) {
|
||||
if (value.length < MIN_LENGTH) {
|
||||
return msg(str`Please enter ${MIN_LENGTH} or more characters`);
|
||||
}
|
||||
@ -439,13 +495,9 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
// Check if valid regex
|
||||
new RegExp(value);
|
||||
} catch (err) {
|
||||
return msg(
|
||||
"Please enter a valid Regular Expression constructor pattern",
|
||||
);
|
||||
return msg("Please enter a valid regular expression");
|
||||
}
|
||||
}
|
||||
|
||||
return this.getInputDuplicateValidity(inputElem);
|
||||
}
|
||||
|
||||
private checkSiblingRowValidity(e: CustomEvent) {
|
||||
@ -456,9 +508,8 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
Array.from(table.querySelectorAll("sl-input[data-invalid]")).map((elem) => {
|
||||
if (elem !== inputElem) {
|
||||
const validityMessage =
|
||||
this.getInputDuplicateValidity(elem as SLInputElement) || "";
|
||||
(elem as SLInputElement).setCustomValidity(validityMessage);
|
||||
(elem as SLInputElement).reportValidity();
|
||||
this.getInputDuplicateValidity(elem as SlInput) || "";
|
||||
(elem as SlInput).setCustomValidity(validityMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -477,11 +528,18 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
let valid: boolean | undefined;
|
||||
|
||||
if (value.length) {
|
||||
valid = !this.getValidityMessage({ type, value });
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("btrix-remove", {
|
||||
new CustomEvent<ExclusionChangeEventDetail>("btrix-remove", {
|
||||
detail: {
|
||||
index,
|
||||
regex,
|
||||
valid,
|
||||
},
|
||||
}) as ExclusionRemoveEvent,
|
||||
);
|
||||
@ -517,13 +575,20 @@ export class QueueExclusionTable extends TailwindElement {
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
let valid: boolean | undefined;
|
||||
|
||||
if (value.length) {
|
||||
valid = !this.getValidityMessage({ type, value });
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("btrix-change", {
|
||||
new CustomEvent<ExclusionChangeEventDetail>("btrix-change", {
|
||||
detail: {
|
||||
index,
|
||||
regex,
|
||||
valid,
|
||||
},
|
||||
}) as ExclusionChangeEvent,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
36
frontend/src/layouts/pageSectionsWithNav.ts
Normal file
36
frontend/src/layouts/pageSectionsWithNav.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import clsx from "clsx";
|
||||
import { html, type TemplateResult } from "lit";
|
||||
|
||||
import { tw } from "@/utils/tailwind";
|
||||
|
||||
export function pageSectionsWithNav({
|
||||
nav,
|
||||
main,
|
||||
placement = "start",
|
||||
sticky = false,
|
||||
}: {
|
||||
nav: TemplateResult;
|
||||
main: TemplateResult;
|
||||
placement?: "start" | "top";
|
||||
sticky?: boolean;
|
||||
}) {
|
||||
return html`
|
||||
<div
|
||||
class=${clsx(
|
||||
tw`flex flex-col`,
|
||||
placement === "start" && tw`gap-8 lg:flex-row`,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class=${clsx(
|
||||
tw`flex flex-1 flex-col gap-2`,
|
||||
sticky && tw`lg:sticky lg:top-2 lg:self-start`,
|
||||
placement === "start" ? tw`lg:max-w-[16.5rem]` : tw`lg:flex-row`,
|
||||
)}
|
||||
>
|
||||
${nav}
|
||||
</div>
|
||||
<div class="flex-1">${main}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
44
frontend/src/layouts/panel.ts
Normal file
44
frontend/src/layouts/panel.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { html, type TemplateResult } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { pageHeading } from "./page";
|
||||
|
||||
export function panelHeader({
|
||||
heading,
|
||||
actions,
|
||||
}: {
|
||||
heading: string | Parameters<typeof pageHeading>[0];
|
||||
actions?: TemplateResult;
|
||||
}) {
|
||||
return html`
|
||||
<header class="mb-3 flex min-h-8 items-baseline justify-between">
|
||||
${typeof heading === "string"
|
||||
? pageHeading({ content: heading })
|
||||
: pageHeading(heading)}
|
||||
${actions}
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
export function panelBody({ content }: { content: TemplateResult }) {
|
||||
return html`<div class="lg:rounded-lg lg:border lg:p-4">${content}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Refactor components to use panel
|
||||
*/
|
||||
export function panel({
|
||||
heading,
|
||||
actions,
|
||||
body,
|
||||
id,
|
||||
className,
|
||||
}: {
|
||||
body: TemplateResult;
|
||||
id?: string;
|
||||
className?: string;
|
||||
} & Parameters<typeof panelHeader>[0]) {
|
||||
return html`<section id=${ifDefined(id)} class=${ifDefined(className)}>
|
||||
${panelHeader({ heading, actions })} ${body}
|
||||
</section>`;
|
||||
}
|
@ -6,8 +6,9 @@ import { type FormState } from "@/utils/workflow";
|
||||
type Field = keyof FormState;
|
||||
|
||||
const infoText: Partial<Record<Field, string | TemplateResult>> = {
|
||||
exclusions: msg(`Specify exclusion rules for what pages should not be visited.
|
||||
Exclusions apply to all URLs.`),
|
||||
exclusions: msg(
|
||||
"Specify exclusion rules for what pages should not be visited.",
|
||||
),
|
||||
pageLimit: msg(
|
||||
"Adds a hard limit on the number of pages that will be crawled.",
|
||||
),
|
||||
|
@ -183,11 +183,13 @@
|
||||
}
|
||||
|
||||
/* Validation styles */
|
||||
[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);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
|
@ -155,9 +155,6 @@ export function getInitialFormState(params: {
|
||||
org?: OrgData | null;
|
||||
}): FormState {
|
||||
const defaultFormState = getDefaultFormState();
|
||||
if (!params.configId) {
|
||||
defaultFormState.runNow = true;
|
||||
}
|
||||
if (!params.initialWorkflow) return defaultFormState;
|
||||
const formState: Partial<FormState> = {};
|
||||
const seedsConfig = params.initialWorkflow.config;
|
||||
|
Loading…
Reference in New Issue
Block a user