refactor: scriptWorkspaceStore.ts

This commit is contained in:
tao.chen
2026-08-25 11:12:22 +08:00
parent 2ed8d7b2cc
commit baa1e04af6
12 changed files with 2133 additions and 1446 deletions
+19
View File
@@ -102,6 +102,25 @@ Lessons from splitting `frontend/app/features/platform/ModelPlatformApp.tsx` (10
- **An unmatched nested child leaves `<Outlet />` blank → white screen inside the layout.** Always cover `/` either with `index(...)` or by letting the parent layout `<Navigate>` on a location check. `/login` is the only top-level path that escapes this trap.
- **`pnpm typecheck` runs `react-router typegen && tsc`.** Type errors from the generated `+types/...` files surface here too. New route files must be registered in `routes.ts` first.
### Frontend store slice split (zustand)
Lessons from splitting `frontend/app/features/schedules/state/schedulesStore.ts` (1251 lines, 40KB) into 8 files: `types.ts`, `helpers.ts`, `canvasSlice.ts`, `listSlice.ts`, `dialogSlice.ts`, `runsSlice.ts`, `logsSlice.ts`, `useSchedulesStore.ts`. Public exports (`useSchedulesStore`, `bindSchedulesApi`) and selector protocol unchanged.
- **`StateCreator<any, ...>` cascades implicit `any` through the slice body.** Writing `StateCreator<any, [], [], SliceState & SliceActions>` to give `get()` cross-slice access makes every nested call (`get().schedules.filter((item) => ...)`) fail with TS7006 — `eslint-disable @typescript-eslint/no-explicit-any` does NOT save you, because TS still infers `any`. Fix: declare a combined `SchedulesStore = State & Actions` type in the **root composition file** (`useSchedulesStore.ts`), then each slice `import type { SchedulesStore } from "./useSchedulesStore"` and uses `StateCreator<SchedulesStore, [], [], SliceState & SliceActions>`. TypeScript accepts the circular `import type` because it erases at build time. `get()` now returns a fully typed snapshot and `.map((item) => ...)` infers correctly.
- **Don't write `<S extends ListFieldSlice & { positionDraftCount: number; busy: string | null; refreshLists: ... }>` constraints on cross-slice helpers.** Every helper needs to know about every other slice's fields, and the constraint chain keeps growing. Better: have helpers (`withMutation`, `applyServerUpdatedSchedule`, `handleError`) take `set: (partial: Partial<SchedulesStore> | ((s: SchedulesStore) => Partial<SchedulesStore>)) => void` and `get: () => SchedulesStore` directly. `SchedulesStore` already enumerates everything; no constraint to extend.
- **Inner-closure helpers become standalone functions.** Monolithic `create((set, get) => { async function withMutation(...) { ... } })` captures `set`/`get` implicitly. When splitting the store you must reify these as exported functions in `helpers.ts` taking `(set, get)` arguments. This forces signatures to spell out exactly which fields they touch — which is what makes the `SchedulesStore`-typed approach pay off.
- **"De-duplicate" requires value comparison, not just name matching.** Plan item "make `constants.ts` re-export `EMPTY_SCHEDULE_FORM` from `state/types`" looks like obvious dedup, but the values differ:
```ts
// constants.ts (utils.ts imports this)
cronExpression: "0 9 * * *",
// state/types (canvasSlice initial state)
cronExpression: "",
```
Consolidating silently changes `utils.ts`'s runtime defaults. Per "never break userspace", leave `constants.ts` alone. **Rule: before any dedup, grep both call sites and diff the actual values, not just the symbol names.**
- **Cross-slice field ownership belongs to layout, not data.** `scheduleKeyword` / `artifactKeyword` filter inputs look like listSlice state because they filter schedules, but they're bound to the left-panel UI and updated by the canvas layout component — they belong in `canvasSlice`. Decision rule: "which layout component writes this field?" not "which data does this field filter?".
- **`positionDrafts` Map stays at module scope in `helpers.ts`.** It must NOT move into `CanvasSliceState`. The original behavior — `reset()` does NOT clear drag-in-progress drafts — is a feature; users expect their unsaved drag to survive reset. If you move it into slice state, audit every `clearAllPositionDrafts()` call site and decide whether each should fire on reset.
- **`reset()` mirrors original `set({ ...initial, loading: true })` semantics.** The legacy reset wipes ALL slice fields including user-edited `scheduleForm` / `nodeForm` (because `initial.scheduleForm = EMPTY_SCHEDULE_FORM`). Any "preserve user input in reset()" change silently diverges from original behavior. If you want to preserve form values, do it as an intentional new feature with its own API, not a side-effect of refactoring.
### Frontend coupling
- **`UserManagementPage.tsx` and `api.ts` still call `/api/v1/admin/employees`.** Don't migrate them in the same change as a `/api/v1/platform/employees` addition — the contract surface is intentionally duplicated.