feat: Make saving simple workflow more efficient (#2626)
- Sticks workflow form save/run buttons to the viewport if all the required fields are filled - Adds keyboard shortcuts to save (cmd/ctrl + S to save, cmd/ctrl + Enter to save and run) - Adds "Cancel" button to new workflow
This commit is contained in:
parent
858ae15ce6
commit
2aad7b8dc0
@ -20,10 +20,15 @@ export class Observable extends LitElement {
|
|||||||
@property({ type: Object })
|
@property({ type: Object })
|
||||||
options?: IntersectionObserverInit;
|
options?: IntersectionObserverInit;
|
||||||
|
|
||||||
private readonly observable = new ObservableController(this);
|
private observable?: ObservableController;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.observable = new ObservableController(this, this.options);
|
||||||
|
}
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
this.observable.observe(this);
|
this.observable?.observe(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { consume } from "@lit/context";
|
import { consume } from "@lit/context";
|
||||||
import { localized, msg, str } from "@lit/localize";
|
import { localized, msg, str } from "@lit/localize";
|
||||||
import type {
|
import type {
|
||||||
|
SlBlurEvent,
|
||||||
SlCheckbox,
|
SlCheckbox,
|
||||||
SlDetails,
|
SlDetails,
|
||||||
SlHideEvent,
|
SlHideEvent,
|
||||||
@ -15,6 +16,7 @@ import Fuse from "fuse.js";
|
|||||||
import { mergeDeep } from "immutable";
|
import { mergeDeep } from "immutable";
|
||||||
import type { LanguageCode } from "iso-639-1";
|
import type { LanguageCode } from "iso-639-1";
|
||||||
import {
|
import {
|
||||||
|
css,
|
||||||
html,
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
type LitElement,
|
type LitElement,
|
||||||
@ -220,6 +222,21 @@ type CrawlConfigResponse = {
|
|||||||
@customElement("btrix-workflow-editor")
|
@customElement("btrix-workflow-editor")
|
||||||
@localized()
|
@localized()
|
||||||
export class WorkflowEditor extends BtrixElement {
|
export class WorkflowEditor extends BtrixElement {
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
@keyframes sticky-footer {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
@consume({ context: proxiesContext, subscribe: true })
|
@consume({ context: proxiesContext, subscribe: true })
|
||||||
private readonly proxies?: ProxiesContext;
|
private readonly proxies?: ProxiesContext;
|
||||||
|
|
||||||
@ -258,9 +275,17 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
@state()
|
@state()
|
||||||
private serverError?: TemplateResult | string;
|
private serverError?: TemplateResult | string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private stickyFooter: "animate" | boolean = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isCrawlRunning: boolean | null = this.configId ? null : false;
|
private isCrawlRunning: boolean | null = this.configId ? null : false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private showKeyboardShortcuts = false;
|
||||||
|
|
||||||
|
private saveAndRun = false;
|
||||||
|
|
||||||
// For observing panel sections position in viewport
|
// For observing panel sections position in viewport
|
||||||
private readonly observable = new ObservableController(this, {
|
private readonly observable = new ObservableController(this, {
|
||||||
// Add some padding to account for stickied elements
|
// Add some padding to account for stickied elements
|
||||||
@ -390,6 +415,10 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private animateStickyFooter() {
|
||||||
|
this.stickyFooter = "animate";
|
||||||
|
}
|
||||||
|
|
||||||
async firstUpdated() {
|
async firstUpdated() {
|
||||||
// Observe form sections to get scroll position
|
// Observe form sections to get scroll position
|
||||||
this.panels?.forEach((panel) => {
|
this.panels?.forEach((panel) => {
|
||||||
@ -399,6 +428,11 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
if (this.progressState?.activeTab !== STEPS[0]) {
|
if (this.progressState?.activeTab !== STEPS[0]) {
|
||||||
void this.scrollToActivePanel();
|
void this.scrollToActivePanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always show footer when editing or duplicating workflow
|
||||||
|
if (this.configId || this.hasRequiredFields()) {
|
||||||
|
this.stickyFooter = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeEditor() {
|
private initializeEditor() {
|
||||||
@ -433,7 +467,22 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
sticky: true,
|
sticky: true,
|
||||||
stickyTopClassname: tw`lg:top-16`,
|
stickyTopClassname: tw`lg:top-16`,
|
||||||
})}
|
})}
|
||||||
|
<btrix-observable
|
||||||
|
.options=${{
|
||||||
|
threshold: 1,
|
||||||
|
}}
|
||||||
|
@btrix-intersect=${this.stickyFooter
|
||||||
|
? null
|
||||||
|
: (e: IntersectEvent) => {
|
||||||
|
const [entry] = e.detail.entries;
|
||||||
|
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.stickyFooter = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
${this.renderFooter()}
|
${this.renderFooter()}
|
||||||
|
</btrix-observable>
|
||||||
</form>
|
</form>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -454,7 +503,7 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<btrix-tab-list
|
<btrix-tab-list
|
||||||
class="hidden lg:block"
|
class="mb-5 hidden lg:block"
|
||||||
tab=${ifDefined(this.progressState?.activeTab)}
|
tab=${ifDefined(this.progressState?.activeTab)}
|
||||||
>
|
>
|
||||||
${STEPS.map(button)}
|
${STEPS.map(button)}
|
||||||
@ -617,22 +666,35 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderFooter() {
|
private renderFooter() {
|
||||||
|
const keyboardShortcut = (key: string) => {
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
const metaKey = ua.includes("Mac")
|
||||||
|
? html`<kbd class="font-sans">⌘</kbd>`
|
||||||
|
: html`<kbd class="font-sans">Ctrl</kbd>`;
|
||||||
|
|
||||||
|
return html`<kbd
|
||||||
|
class="inline-flex items-center gap-0.5 rounded-sm border border-black/20 bg-white/20 px-1 py-0.5 text-xs leading-none"
|
||||||
|
slot="suffix"
|
||||||
|
>
|
||||||
|
${metaKey}<kbd class="font-sans">${key}</kbd></kbd
|
||||||
|
> `;
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<footer
|
<footer
|
||||||
class=${clsx(
|
class=${clsx(
|
||||||
"flex items-center justify-end gap-2 rounded-lg border bg-white px-6 py-4 mb-7",
|
tw`bottom-3 z-50 mb-7 flex items-center justify-end gap-2 rounded-lg border bg-white px-6 py-4 shadow duration-slow`,
|
||||||
this.configId || this.serverError
|
this.stickyFooter && [
|
||||||
? tw`z- sticky bottom-3 z-20 shadow-md`
|
tw`sticky`,
|
||||||
: tw`shadow`,
|
this.stickyFooter === "animate" &&
|
||||||
|
tw`motion-safe:animate-[sticky-footer_var(--sl-transition-medium)_ease-in-out]`,
|
||||||
|
],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${this.configId
|
|
||||||
? html`
|
|
||||||
<sl-button class="mr-auto" size="small" type="reset">
|
<sl-button class="mr-auto" size="small" type="reset">
|
||||||
${msg("Cancel")}
|
${msg("Cancel")}
|
||||||
</sl-button>
|
</sl-button>
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${when(this.serverError, (error) => this.renderErrorAlert(error))}
|
${when(this.serverError, (error) => this.renderErrorAlert(error))}
|
||||||
${when(this.configId, this.renderCrawlStatus)}
|
${when(this.configId, this.renderCrawlStatus)}
|
||||||
|
|
||||||
@ -645,6 +707,7 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
?loading=${this.isSubmitting}
|
?loading=${this.isSubmitting}
|
||||||
>
|
>
|
||||||
${msg("Save")}
|
${msg("Save")}
|
||||||
|
${when(this.showKeyboardShortcuts, () => keyboardShortcut("S"))}
|
||||||
</sl-button>
|
</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
<sl-tooltip
|
<sl-tooltip
|
||||||
@ -665,6 +728,7 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
?loading=${this.isSubmitting || this.isCrawlRunning === null}
|
?loading=${this.isSubmitting || this.isCrawlRunning === null}
|
||||||
>
|
>
|
||||||
${msg(this.isCrawlRunning ? "Update Crawl" : "Run Crawl")}
|
${msg(this.isCrawlRunning ? "Update Crawl" : "Run Crawl")}
|
||||||
|
${when(this.showKeyboardShortcuts, () => keyboardShortcut("Enter"))}
|
||||||
</sl-button>
|
</sl-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
</footer>
|
</footer>
|
||||||
@ -808,10 +872,15 @@ export class WorkflowEditor extends BtrixElement {
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if (!inputEl.checkValidity() && validURL(inputEl.value)) {
|
const valid = validURL(inputEl.value);
|
||||||
|
if (!inputEl.checkValidity() && valid) {
|
||||||
inputEl.setCustomValidity("");
|
inputEl.setCustomValidity("");
|
||||||
inputEl.helpText = "";
|
inputEl.helpText = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
this.animateStickyFooter();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
@sl-blur=${async (e: Event) => {
|
@sl-blur=${async (e: Event) => {
|
||||||
const inputEl = e.target as SlInput;
|
const inputEl = e.target as SlInput;
|
||||||
@ -858,7 +927,15 @@ https://archiveweb.page/guide`}
|
|||||||
}}
|
}}
|
||||||
@sl-input=${(e: CustomEvent) => {
|
@sl-input=${(e: CustomEvent) => {
|
||||||
const inputEl = e.target as SlInput;
|
const inputEl = e.target as SlInput;
|
||||||
if (!inputEl.value) {
|
const value = inputEl.value;
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
const { isValid } = this.validateUrlList(inputEl.value);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
this.animateStickyFooter();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
inputEl.helpText = msg("At least 1 URL is required.");
|
inputEl.helpText = msg("At least 1 URL is required.");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -2183,22 +2260,52 @@ https://archiveweb.page/images/${"logo.svg"}`}
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onKeyDown(event: KeyboardEvent) {
|
private onKeyDown(event: KeyboardEvent) {
|
||||||
|
const { key, metaKey } = event;
|
||||||
|
|
||||||
|
if (metaKey && !this.showKeyboardShortcuts) {
|
||||||
|
// Show meta keyboard shortcut
|
||||||
|
this.showKeyboardShortcuts = true;
|
||||||
|
this.animateStickyFooter();
|
||||||
|
}
|
||||||
|
|
||||||
const el = event.target as HTMLElement;
|
const el = event.target as HTMLElement;
|
||||||
const tagName = el.tagName.toLowerCase();
|
if (!("value" in el)) return;
|
||||||
if (tagName !== "sl-input") return;
|
|
||||||
const { key } = event;
|
if (metaKey) {
|
||||||
if ((el as SlInput).type === "number") {
|
if (key === "s" || key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.saveAndRun = key === "Enter";
|
||||||
|
|
||||||
|
// Trigger blur to run value transformations and validation
|
||||||
|
el.addEventListener(
|
||||||
|
"sl-blur",
|
||||||
|
async (e: SlBlurEvent) => {
|
||||||
|
const input = e.currentTarget as SlInput;
|
||||||
|
|
||||||
|
// Wait for all transformations and validations to run
|
||||||
|
await input.updateComplete;
|
||||||
|
await this.updateComplete;
|
||||||
|
|
||||||
|
this.formElem?.requestSubmit();
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
el.blur();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((el as unknown as SlInput).type === "number") {
|
||||||
// Prevent typing non-numeric keys
|
// Prevent typing non-numeric keys
|
||||||
if (
|
if (!metaKey && !event.shiftKey && key.length === 1 && /\D/.test(key)) {
|
||||||
!event.metaKey &&
|
|
||||||
!event.shiftKey &&
|
|
||||||
key.length === 1 &&
|
|
||||||
/\D/.test(key)
|
|
||||||
) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
key === "Enter" &&
|
key === "Enter" &&
|
||||||
this.progressState!.activeTab !== STEPS[STEPS.length - 1]
|
this.progressState!.activeTab !== STEPS[STEPS.length - 1]
|
||||||
@ -2211,13 +2318,16 @@ https://archiveweb.page/images/${"logo.svg"}`}
|
|||||||
private async onSubmit(event: SubmitEvent) {
|
private async onSubmit(event: SubmitEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Submitter may not exist if requesting submit from keyboard shortcuts
|
||||||
|
if (event.submitter) {
|
||||||
const submitType = (
|
const submitType = (
|
||||||
event.submitter as HTMLButtonElement & {
|
event.submitter as HTMLButtonElement & {
|
||||||
value?: SubmitType;
|
value?: SubmitType;
|
||||||
}
|
}
|
||||||
).value;
|
).value;
|
||||||
|
|
||||||
const saveAndRun = submitType === SubmitType.SaveAndRun;
|
this.saveAndRun = submitType === SubmitType.SaveAndRun;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.formElem) return;
|
if (!this.formElem) return;
|
||||||
|
|
||||||
@ -2248,11 +2358,11 @@ https://archiveweb.page/images/${"logo.svg"}`}
|
|||||||
|
|
||||||
const config: CrawlConfigParams & WorkflowRunParams = {
|
const config: CrawlConfigParams & WorkflowRunParams = {
|
||||||
...this.parseConfig(),
|
...this.parseConfig(),
|
||||||
runNow: saveAndRun && !this.isCrawlRunning,
|
runNow: this.saveAndRun && !this.isCrawlRunning,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.configId) {
|
if (this.configId) {
|
||||||
config.updateRunning = saveAndRun && Boolean(this.isCrawlRunning);
|
config.updateRunning = this.saveAndRun && Boolean(this.isCrawlRunning);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isSubmitting = true;
|
this.isSubmitting = true;
|
||||||
|
Loading…
Reference in New Issue
Block a user