From 22435ddaf9f3f25efac2b1f5ca8aab31505e53d8 Mon Sep 17 00:00:00 2001 From: sua yoo Date: Wed, 2 Oct 2024 19:08:20 -0700 Subject: [PATCH] Save new workflow scope type preference (#2099) Resolves https://github.com/webrecorder/browsertrix/issues/2091 ### 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). --- .../crawl-workflows/new-workflow-dialog.ts | 12 ++-- .../crawl-workflows/workflow-editor.ts | 8 +++ frontend/src/pages/org/index.ts | 7 +++ frontend/src/pages/org/workflows-list.ts | 7 ++- frontend/src/pages/org/workflows-new.ts | 59 ++++++++++--------- frontend/src/types/user.ts | 6 ++ frontend/src/utils/state.ts | 35 ++++++++++- 7 files changed, 97 insertions(+), 37 deletions(-) diff --git a/frontend/src/features/crawl-workflows/new-workflow-dialog.ts b/frontend/src/features/crawl-workflows/new-workflow-dialog.ts index 8e8a523ee..5ac689b1f 100644 --- a/frontend/src/features/crawl-workflows/new-workflow-dialog.ts +++ b/frontend/src/features/crawl-workflows/new-workflow-dialog.ts @@ -3,11 +3,13 @@ import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; 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 urlListSvg from "~assets/images/new-crawl-config_URL-List.svg"; -export type SelectJobTypeEvent = CustomEvent; +export type SelectJobTypeEvent = CustomEvent< + (typeof WorkflowScopeType)[keyof typeof WorkflowScopeType] +>; /** * @event select-job-type SelectJobTypeEvent @@ -33,8 +35,8 @@ export class NewWorkflowDialog extends TailwindElement { @click=${() => { this.dispatchEvent( new CustomEvent("select-job-type", { - detail: "page-list", - }) as SelectJobTypeEvent, + detail: WorkflowScopeType.PageList, + }), ); }} > @@ -63,7 +65,7 @@ export class NewWorkflowDialog extends TailwindElement { @click=${() => { this.dispatchEvent( new CustomEvent("select-job-type", { - detail: "prefix", + detail: WorkflowScopeType.Prefix, }) as SelectJobTypeEvent, ); }} diff --git a/frontend/src/features/crawl-workflows/workflow-editor.ts b/frontend/src/features/crawl-workflows/workflow-editor.ts index c661db817..7545cd63e 100644 --- a/frontend/src/features/crawl-workflows/workflow-editor.ts +++ b/frontend/src/features/crawl-workflows/workflow-editor.ts @@ -67,6 +67,7 @@ import { import { maxLengthValidator } from "@/utils/form"; import { getLocale } from "@/utils/localization"; import { isArchivingDisabled } from "@/utils/orgs"; +import { AppStateService } from "@/utils/state"; import { regexEscape } from "@/utils/string"; import { tw } from "@/utils/tailwind"; 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); } diff --git a/frontend/src/pages/org/index.ts b/frontend/src/pages/org/index.ts index d9defdb7d..0cfb33fee 100644 --- a/frontend/src/pages/org/index.ts +++ b/frontend/src/pages/org/index.ts @@ -539,6 +539,13 @@ export class Org extends LiteElement { @select-new-dialog=${this.onSelectNewDialog} @select-job-type=${(e: SelectJobTypeEvent) => { this.openDialogName = undefined; + + if (e.detail !== this.appState.userPreferences?.newWorkflowScopeType) { + AppStateService.partialUpdateUserPreferences({ + newWorkflowScopeType: e.detail, + }); + } + this.navTo(`${this.orgBasePath}/workflows/new`, { scopeType: e.detail, }); diff --git a/frontend/src/pages/org/workflows-list.ts b/frontend/src/pages/org/workflows-list.ts index f9d2891dc..2b4fb2685 100644 --- a/frontend/src/pages/org/workflows-list.ts +++ b/frontend/src/pages/org/workflows-list.ts @@ -219,9 +219,12 @@ export class WorkflowsList extends LiteElement { + this.navTo(`${this.orgBasePath}/workflows/new`, { + scopeType: + this.appState.userPreferences?.newWorkflowScopeType, + })} > ${msg("New Workflow")} ${when(this.org, (org) => { const initialWorkflow = mergeDeep( - defaultValue, + this.defaultNewWorkflow, { profileid: org.crawlingDefaults?.profileid, config: { diff --git a/frontend/src/types/user.ts b/frontend/src/types/user.ts index 466e36ab3..9757bb034 100644 --- a/frontend/src/types/user.ts +++ b/frontend/src/types/user.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { accessCodeSchema } from "./org"; +import { WorkflowScopeType } from "./workflow"; export const userOrgSchema = z.object({ default: z.boolean().optional(), @@ -44,3 +45,8 @@ export const userInfoSchema = z.object({ orgs: z.array(userOrgSchema), }); export type UserInfo = z.infer; + +export const userPreferencesSchema = z.object({ + newWorkflowScopeType: z.nativeEnum(WorkflowScopeType).optional(), +}); +export type UserPreferences = z.infer; diff --git a/frontend/src/utils/state.ts b/frontend/src/utils/state.ts index 5ba408770..3e2b82195 100644 --- a/frontend/src/utils/state.ts +++ b/frontend/src/utils/state.ts @@ -1,13 +1,20 @@ /** * Store and access application-wide state */ +import { mergeDeep } from "immutable"; import { locked, options, transaction, use } from "lit-shared-state"; import { persist } from "./persist"; import { authSchema, type Auth } from "@/types/auth"; 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 { isAdmin, isCrawler } from "@/utils/orgs"; @@ -28,11 +35,15 @@ export function makeAppStateService() { @options(persist(window.sessionStorage)) userInfo: UserInfo | null = null; + @options(persist(window.localStorage)) + userPreferences: UserPreferences | null = null; + // TODO persist here auth: Auth | null = null; // Store org slug in local storage in order to redirect // to the most recently visited org on next log in + // TODO move to `userPreferences` @options(persist(window.localStorage)) orgSlug: string | null = null; @@ -52,7 +63,7 @@ export function makeAppStateService() { )) || null; - if (!userOrg) { + if (appState.userInfo && !userOrg) { console.debug("no user org matching slug in state"); } @@ -113,6 +124,25 @@ export function makeAppStateService() { } } + @transaction() + @unlock() + partialUpdateUserPreferences( + userPreferences: Partial, + ) { + 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() @unlock() updateOrgSlug(orgSlug: AppState["orgSlug"]) { @@ -152,6 +182,7 @@ export function makeAppStateService() { private _resetUser() { appState.auth = null; appState.userInfo = null; + appState.userPreferences = null; appState.orgSlug = null; appState.org = undefined; }