Save new workflow scope type preference (#2099)

Resolves https://github.com/webrecorder/browsertrix/issues/2091

<!-- Fixes #issue_number -->

### Changes

Saves scope type chosen from "+ New Workflow" dropdown to local storage, as well as from within workflow editor when creating a new workflow (but not editing an existing one).
This commit is contained in:
sua yoo 2024-10-02 19:08:20 -07:00 committed by GitHub
parent bb6e703f6a
commit 22435ddaf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 97 additions and 37 deletions

View File

@ -3,11 +3,13 @@ import { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { TailwindElement } from "@/classes/TailwindElement"; import { TailwindElement } from "@/classes/TailwindElement";
import type { FormState as WorkflowFormState } from "@/utils/workflow"; import { WorkflowScopeType } from "@/types/workflow";
import seededCrawlSvg from "~assets/images/new-crawl-config_Seeded-Crawl.svg"; import seededCrawlSvg from "~assets/images/new-crawl-config_Seeded-Crawl.svg";
import urlListSvg from "~assets/images/new-crawl-config_URL-List.svg"; import urlListSvg from "~assets/images/new-crawl-config_URL-List.svg";
export type SelectJobTypeEvent = CustomEvent<WorkflowFormState["scopeType"]>; export type SelectJobTypeEvent = CustomEvent<
(typeof WorkflowScopeType)[keyof typeof WorkflowScopeType]
>;
/** /**
* @event select-job-type SelectJobTypeEvent * @event select-job-type SelectJobTypeEvent
@ -33,8 +35,8 @@ export class NewWorkflowDialog extends TailwindElement {
@click=${() => { @click=${() => {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("select-job-type", { new CustomEvent("select-job-type", {
detail: "page-list", detail: WorkflowScopeType.PageList,
}) as SelectJobTypeEvent, }),
); );
}} }}
> >
@ -63,7 +65,7 @@ export class NewWorkflowDialog extends TailwindElement {
@click=${() => { @click=${() => {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("select-job-type", { new CustomEvent("select-job-type", {
detail: "prefix", detail: WorkflowScopeType.Prefix,
}) as SelectJobTypeEvent, }) as SelectJobTypeEvent,
); );
}} }}

View File

@ -67,6 +67,7 @@ import {
import { maxLengthValidator } from "@/utils/form"; import { maxLengthValidator } from "@/utils/form";
import { getLocale } from "@/utils/localization"; import { getLocale } from "@/utils/localization";
import { isArchivingDisabled } from "@/utils/orgs"; import { isArchivingDisabled } from "@/utils/orgs";
import { AppStateService } from "@/utils/state";
import { regexEscape } from "@/utils/string"; import { regexEscape } from "@/utils/string";
import { tw } from "@/utils/tailwind"; import { tw } from "@/utils/tailwind";
import { import {
@ -1712,6 +1713,13 @@ https://archiveweb.page/images/${"logo.svg"}`}
} }
} }
if (!this.configId) {
// Remember scope type for new workflows
AppStateService.partialUpdateUserPreferences({
newWorkflowScopeType: value,
});
}
this.updateFormState(formState); this.updateFormState(formState);
} }

View File

@ -539,6 +539,13 @@ export class Org extends LiteElement {
@select-new-dialog=${this.onSelectNewDialog} @select-new-dialog=${this.onSelectNewDialog}
@select-job-type=${(e: SelectJobTypeEvent) => { @select-job-type=${(e: SelectJobTypeEvent) => {
this.openDialogName = undefined; this.openDialogName = undefined;
if (e.detail !== this.appState.userPreferences?.newWorkflowScopeType) {
AppStateService.partialUpdateUserPreferences({
newWorkflowScopeType: e.detail,
});
}
this.navTo(`${this.orgBasePath}/workflows/new`, { this.navTo(`${this.orgBasePath}/workflows/new`, {
scopeType: e.detail, scopeType: e.detail,
}); });

View File

@ -219,9 +219,12 @@ export class WorkflowsList extends LiteElement {
<sl-button <sl-button
variant="primary" variant="primary"
size="small" size="small"
href="${this.orgBasePath}/workflows/new"
?disabled=${this.org?.readOnly} ?disabled=${this.org?.readOnly}
@click=${this.navLink} @click=${() =>
this.navTo(`${this.orgBasePath}/workflows/new`, {
scopeType:
this.appState.userPreferences?.newWorkflowScopeType,
})}
> >
<sl-icon slot="prefix" name="plus-lg"></sl-icon> <sl-icon slot="prefix" name="plus-lg"></sl-icon>
${msg("New Workflow")}</sl-button ${msg("New Workflow")}</sl-button

View File

@ -8,36 +8,10 @@ import { ScopeType, type Seed, type WorkflowParams } from "./types";
import type { UserGuideEventMap } from "@/index"; import type { UserGuideEventMap } from "@/index";
import { pageNav, type Breadcrumb } from "@/layouts/pageHeader"; import { pageNav, type Breadcrumb } from "@/layouts/pageHeader";
import { WorkflowScopeType } from "@/types/workflow";
import LiteElement, { html } from "@/utils/LiteElement"; import LiteElement, { html } from "@/utils/LiteElement";
import type { FormState as WorkflowFormState } from "@/utils/workflow"; import type { FormState as WorkflowFormState } from "@/utils/workflow";
const defaultValue = {
name: "",
description: null,
profileid: null,
schedule: "",
config: {
seeds: [],
scopeType: ScopeType.Page,
exclude: [],
behaviorTimeout: null,
pageLoadTimeout: null,
pageExtraDelay: null,
postLoadDelay: null,
useSitemap: false,
failOnFailedSeed: false,
userAgent: null,
},
tags: [],
crawlTimeout: null,
maxCrawlSize: null,
jobType: "custom",
scale: 1,
autoAddCollections: [],
crawlerChannel: "default",
proxyId: null,
} as WorkflowParams;
/** /**
* Usage: * Usage:
* ```ts * ```ts
@ -59,6 +33,35 @@ export class WorkflowsNew extends LiteElement {
@property({ type: Object }) @property({ type: Object })
initialWorkflow?: WorkflowParams; initialWorkflow?: WorkflowParams;
private get defaultNewWorkflow(): WorkflowParams {
return {
name: "",
description: null,
profileid: null,
schedule: "",
config: {
scopeType: (this.appState.userPreferences?.newWorkflowScopeType ||
WorkflowScopeType.Page) as ScopeType,
exclude: [],
behaviorTimeout: null,
pageLoadTimeout: null,
pageExtraDelay: null,
postLoadDelay: null,
useSitemap: false,
failOnFailedSeed: false,
userAgent: null,
},
tags: [],
crawlTimeout: null,
maxCrawlSize: null,
jobType: "custom",
scale: 1,
autoAddCollections: [],
crawlerChannel: "default",
proxyId: null,
};
}
private renderBreadcrumbs() { private renderBreadcrumbs() {
const breadcrumbs: Breadcrumb[] = [ const breadcrumbs: Breadcrumb[] = [
{ {
@ -103,7 +106,7 @@ export class WorkflowsNew extends LiteElement {
</header> </header>
${when(this.org, (org) => { ${when(this.org, (org) => {
const initialWorkflow = mergeDeep( const initialWorkflow = mergeDeep(
defaultValue, this.defaultNewWorkflow,
{ {
profileid: org.crawlingDefaults?.profileid, profileid: org.crawlingDefaults?.profileid,
config: { config: {

View File

@ -1,6 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { accessCodeSchema } from "./org"; import { accessCodeSchema } from "./org";
import { WorkflowScopeType } from "./workflow";
export const userOrgSchema = z.object({ export const userOrgSchema = z.object({
default: z.boolean().optional(), default: z.boolean().optional(),
@ -44,3 +45,8 @@ export const userInfoSchema = z.object({
orgs: z.array(userOrgSchema), orgs: z.array(userOrgSchema),
}); });
export type UserInfo = z.infer<typeof userInfoSchema>; export type UserInfo = z.infer<typeof userInfoSchema>;
export const userPreferencesSchema = z.object({
newWorkflowScopeType: z.nativeEnum(WorkflowScopeType).optional(),
});
export type UserPreferences = z.infer<typeof userPreferencesSchema>;

View File

@ -1,13 +1,20 @@
/** /**
* Store and access application-wide state * Store and access application-wide state
*/ */
import { mergeDeep } from "immutable";
import { locked, options, transaction, use } from "lit-shared-state"; import { locked, options, transaction, use } from "lit-shared-state";
import { persist } from "./persist"; import { persist } from "./persist";
import { authSchema, type Auth } from "@/types/auth"; import { authSchema, type Auth } from "@/types/auth";
import type { OrgData } from "@/types/org"; import type { OrgData } from "@/types/org";
import { userInfoSchema, type UserInfo, type UserOrg } from "@/types/user"; import {
userInfoSchema,
userPreferencesSchema,
type UserInfo,
type UserOrg,
type UserPreferences,
} from "@/types/user";
import type { AppSettings } from "@/utils/app"; import type { AppSettings } from "@/utils/app";
import { isAdmin, isCrawler } from "@/utils/orgs"; import { isAdmin, isCrawler } from "@/utils/orgs";
@ -28,11 +35,15 @@ export function makeAppStateService() {
@options(persist(window.sessionStorage)) @options(persist(window.sessionStorage))
userInfo: UserInfo | null = null; userInfo: UserInfo | null = null;
@options(persist(window.localStorage))
userPreferences: UserPreferences | null = null;
// TODO persist here // TODO persist here
auth: Auth | null = null; auth: Auth | null = null;
// Store org slug in local storage in order to redirect // Store org slug in local storage in order to redirect
// to the most recently visited org on next log in // to the most recently visited org on next log in
// TODO move to `userPreferences`
@options(persist(window.localStorage)) @options(persist(window.localStorage))
orgSlug: string | null = null; orgSlug: string | null = null;
@ -52,7 +63,7 @@ export function makeAppStateService() {
)) || )) ||
null; null;
if (!userOrg) { if (appState.userInfo && !userOrg) {
console.debug("no user org matching slug in state"); console.debug("no user org matching slug in state");
} }
@ -113,6 +124,25 @@ export function makeAppStateService() {
} }
} }
@transaction()
@unlock()
partialUpdateUserPreferences(
userPreferences: Partial<AppState["userPreferences"]>,
) {
userPreferencesSchema.nullable().parse(userPreferences);
if (appState.userPreferences && userPreferences) {
appState.userPreferences = mergeDeep(
appState.userPreferences,
userPreferences,
);
} else {
appState.userPreferences = userPreferences;
}
console.log("appState.userPreferences:", appState.userPreferences);
}
@transaction() @transaction()
@unlock() @unlock()
updateOrgSlug(orgSlug: AppState["orgSlug"]) { updateOrgSlug(orgSlug: AppState["orgSlug"]) {
@ -152,6 +182,7 @@ export function makeAppStateService() {
private _resetUser() { private _resetUser() {
appState.auth = null; appState.auth = null;
appState.userInfo = null; appState.userInfo = null;
appState.userPreferences = null;
appState.orgSlug = null; appState.orgSlug = null;
appState.org = undefined; appState.org = undefined;
} }