@voidhash/mimic-react 1.0.0-beta.14 → 1.0.0-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -6
- package/dist/zustand-commander/commander.mjs.map +1 -1
- package/dist/zustand-commander/hooks.mjs.map +1 -1
- package/package.json +3 -3
- package/src/zustand-commander/commander.ts +2 -2
- package/src/zustand-commander/hooks.ts +1 -1
- package/tests/zustand-commander/commander.test.ts +321 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @voidhash/mimic-react@1.0.0-beta.
|
|
2
|
+
> @voidhash/mimic-react@1.0.0-beta.16 build /home/runner/work/mimic/mimic/packages/mimic-react
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.18.2[22m powered by rolldown [2mv1.0.0-beta.55[22m
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32mzustand/middleware.d.cts[39m [2m2.13 kB[22m [2m│ gzip: 0.85 kB[22m
|
|
39
39
|
[34mℹ[39m [33m[CJS][39m [2mdist/[22m[32mzustand/useDraft.d.cts[39m [2m1.54 kB[22m [2m│ gzip: 0.66 kB[22m
|
|
40
40
|
[34mℹ[39m [33m[CJS][39m 15 files, total: 28.71 kB
|
|
41
|
-
[32m✔[39m Build complete in [
|
|
41
|
+
[32m✔[39m Build complete in [32m3487ms[39m
|
|
42
42
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mzustand-commander/index.mjs[22m [2m 0.53 kB[22m [2m│ gzip: 0.21 kB[22m
|
|
43
43
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mzustand/index.mjs[22m [2m 0.11 kB[22m [2m│ gzip: 0.09 kB[22m
|
|
44
44
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[1mindex.mjs[22m [2m 0.01 kB[22m [2m│ gzip: 0.03 kB[22m
|
|
45
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand-commander/commander.mjs.map [2m15.
|
|
45
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand-commander/commander.mjs.map [2m15.09 kB[22m [2m│ gzip: 3.78 kB[22m
|
|
46
46
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand-commander/types.mjs.map [2m11.30 kB[22m [2m│ gzip: 2.83 kB[22m
|
|
47
|
-
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand-commander/hooks.mjs.map [2m10.
|
|
47
|
+
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand-commander/hooks.mjs.map [2m10.71 kB[22m [2m│ gzip: 3.28 kB[22m
|
|
48
48
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand/middleware.mjs.map [2m 6.89 kB[22m [2m│ gzip: 2.11 kB[22m
|
|
49
49
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand-commander/commander.mjs [2m 6.83 kB[22m [2m│ gzip: 1.88 kB[22m
|
|
50
50
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22mzustand/useDraft.mjs.map [2m 4.64 kB[22m [2m│ gzip: 1.59 kB[22m
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32mzustand-commander/commander.d.mts[39m [2m 2.28 kB[22m [2m│ gzip: 0.80 kB[22m
|
|
73
73
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32mzustand/middleware.d.mts[39m [2m 2.13 kB[22m [2m│ gzip: 0.85 kB[22m
|
|
74
74
|
[34mℹ[39m [34m[ESM][39m [2mdist/[22m[32mzustand/useDraft.d.mts[39m [2m 1.54 kB[22m [2m│ gzip: 0.66 kB[22m
|
|
75
|
-
[34mℹ[39m [34m[ESM][39m 33 files, total: 97.
|
|
76
|
-
[32m✔[39m Build complete in [
|
|
75
|
+
[34mℹ[39m [34m[ESM][39m 33 files, total: 97.85 kB
|
|
76
|
+
[32m✔[39m Build complete in [32m3508ms[39m
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commander.mjs","names":["DEFAULT_OPTIONS: Required<CommanderOptions>","_storeApi: StoreApi<TStore & CommanderSlice> | null","ctx: CommandContext<TStore & CommanderSlice, TSchema>","entry: UndoEntry<TParams, TReturn>","ctx: CommandContext<TStore>","newEntry: UndoEntry"],"sources":["../../src/zustand-commander/commander.ts"],"sourcesContent":["/**\n * @voidhash/mimic-react/zustand-commander\n *\n * Commander creation and command definition.\n *\n * @since 0.0.1\n */\n\nimport type { StoreApi } from \"zustand\";\nimport type { Primitive } from \"@voidhash/mimic\";\nimport type { ClientDocument } from \"@voidhash/mimic/client\";\nimport {\n COMMAND_SYMBOL,\n UNDOABLE_COMMAND_SYMBOL,\n isUndoableCommand,\n type Command,\n type Commander,\n type CommanderOptions,\n type CommanderSlice,\n type CommandContext,\n type CommandDispatch,\n type CommandFn,\n type RevertFn,\n type UndoableCommand,\n type UndoEntry,\n} from \"./types\";\n\n// =============================================================================\n// Default Options\n// =============================================================================\n\nconst DEFAULT_OPTIONS: Required<CommanderOptions> = {\n maxUndoStackSize: 100,\n};\n\n// =============================================================================\n// Transaction Helper\n// =============================================================================\n\n/**\n * Build a transaction function that routes to draft or document.\n */\nfunction buildTransaction<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n storeApi: StoreApi<TStore>\n): (fn: (root: Primitive.InferProxy<TSchema>) => void) => void {\n return (fn) => {\n const state = storeApi.getState();\n const draft = state._commander.activeDraft;\n if (draft) {\n draft.update(fn);\n } else {\n // Access mimic.document from the store\n const mimic = (state as any).mimic;\n if (!mimic?.document) {\n throw new Error(\n \"Commander: No active draft and no mimic document found on the store.\"\n );\n }\n mimic.document.transaction(fn);\n }\n };\n}\n\n// =============================================================================\n// Commander Implementation\n// =============================================================================\n\n/**\n * Creates a commander instance bound to a specific store type.\n *\n * @example\n * ```ts\n * // Create commander for your store type\n * const commander = createCommander<StoreState>();\n *\n * // Define commands\n * const addItem = commander.action(\n * Schema.Struct({ name: Schema.String }),\n * (ctx, params) => {\n * ctx.transaction(root => {\n * // add item\n * });\n * }\n * );\n *\n * // Create store with middleware\n * const useStore = create(\n * commander.middleware(\n * mimic(document, (set, get) => ({\n * // your state\n * }))\n * )\n * );\n * ```\n */\nexport function createCommander<TStore extends object, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n options: CommanderOptions = {}\n): Commander<TStore & CommanderSlice, TSchema> {\n const { maxUndoStackSize } = { ...DEFAULT_OPTIONS, ...options };\n\n // Track the store API once middleware is applied\n let _storeApi: StoreApi<TStore & CommanderSlice> | null = null;\n\n /**\n * Creates the dispatch function for use within command handlers.\n */\n const createDispatch = (): CommandDispatch<TStore & CommanderSlice, TSchema> => {\n return <TParams, TReturn>(\n command: Command<TStore & CommanderSlice, TParams, TReturn, TSchema>\n ) => {\n return (params: TParams): TReturn => {\n if (!_storeApi) {\n throw new Error(\n \"Commander: Store not initialized. Make sure to use the commander middleware.\"\n );\n }\n\n // Create context for the command\n const ctx: CommandContext<TStore & CommanderSlice, TSchema> = {\n getState: () => _storeApi!.getState(),\n setState: (partial) => _storeApi!.setState(partial as any),\n dispatch: createDispatch(),\n transaction: buildTransaction<TStore & CommanderSlice, TSchema>(_storeApi!),\n };\n\n // Execute the command\n const result = command.fn(ctx, params);\n\n // Skip undo stack when a draft is active\n const hasDraft = _storeApi!.getState()._commander.activeDraft !== null;\n\n // If it's an undoable command and no draft is active, add to undo stack\n if (isUndoableCommand(command) && !hasDraft) {\n const entry: UndoEntry<TParams, TReturn> = {\n command,\n params,\n result,\n timestamp: Date.now(),\n };\n\n _storeApi.setState((state: TStore & CommanderSlice) => {\n const { undoStack, redoStack: _redoStack } = state._commander;\n\n // Add to undo stack, respecting max size\n const newUndoStack = [...undoStack, entry].slice(-maxUndoStackSize);\n\n // Clear redo stack when a new command is executed\n return {\n ...state,\n _commander: {\n ...state._commander,\n undoStack: newUndoStack,\n redoStack: [],\n },\n };\n });\n }\n\n return result;\n };\n };\n };\n\n /**\n * Create a regular command (no undo support).\n */\n function action<TParams, TReturn = void>(\n fn: CommandFn<TStore & CommanderSlice, TParams, TReturn, TSchema>\n ): Command<TStore & CommanderSlice, TParams, TReturn, TSchema> {\n return {\n [COMMAND_SYMBOL]: true,\n fn,\n };\n }\n\n /**\n * Create an undoable command with undo/redo support.\n */\n function undoableAction<TParams, TReturn>(\n fn: CommandFn<TStore & CommanderSlice, TParams, TReturn, TSchema>,\n revert: RevertFn<TStore & CommanderSlice, TParams, TReturn, TSchema>\n ): UndoableCommand<TStore & CommanderSlice, TParams, TReturn, TSchema> {\n return {\n [COMMAND_SYMBOL]: true,\n [UNDOABLE_COMMAND_SYMBOL]: true,\n fn,\n revert,\n };\n }\n\n /**\n * Zustand middleware that adds commander functionality.\n */\n const middleware = <T extends object>(\n config: (\n set: StoreApi<T & CommanderSlice>[\"setState\"],\n get: StoreApi<T & CommanderSlice>[\"getState\"],\n api: StoreApi<T & CommanderSlice>\n ) => T\n ) => {\n return (\n set: StoreApi<T & CommanderSlice>[\"setState\"],\n get: StoreApi<T & CommanderSlice>[\"getState\"],\n api: StoreApi<T & CommanderSlice>\n ): T & CommanderSlice => {\n // Store the API reference for dispatch\n _storeApi = api as unknown as StoreApi<TStore & CommanderSlice>;\n\n // Get user's state\n const userState = config(set, get, api);\n\n // Add commander slice\n return {\n ...userState,\n _commander: {\n undoStack: [],\n redoStack: [],\n activeDraft: null,\n },\n };\n };\n };\n\n return {\n action,\n undoableAction,\n middleware: middleware as Commander<TStore & CommanderSlice, TSchema>[\"middleware\"],\n };\n}\n\n// =============================================================================\n// Draft Helpers\n// =============================================================================\n\n/**\n * Set the active draft on the commander slice.\n * While a draft is active, transactions route through `draft.update()` and undo is disabled.\n */\nexport function setActiveDraft<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>,\n draft: ClientDocument.DraftHandle<any>\n): void {\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n activeDraft: draft,\n },\n }));\n}\n\n/**\n * Clear the active draft from the commander slice.\n */\nexport function clearActiveDraft<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): void {\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n activeDraft: null,\n },\n }));\n}\n\n// =============================================================================\n// Undo/Redo Functions\n// =============================================================================\n\n/**\n * Perform an undo operation on the store.\n * Returns true if an undo was performed, false if undo stack was empty or a draft is active.\n */\nexport function performUndo<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): boolean {\n const state = storeApi.getState();\n const { undoStack, redoStack, activeDraft } = state._commander;\n\n // Undo is disabled while a draft is active\n if (activeDraft) {\n return false;\n }\n\n // Pop the last entry from undo stack\n const entry = undoStack[undoStack.length - 1];\n if (!entry) {\n return false;\n }\n\n const newUndoStack = undoStack.slice(0, -1);\n\n // Create context for the revert function\n const ctx: CommandContext<TStore> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as any),\n dispatch: createDispatchForUndo(storeApi),\n transaction: buildTransaction(storeApi),\n };\n\n // Execute the revert function\n entry.command.revert(ctx, entry.params, entry.result);\n\n // Move entry to redo stack\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n undoStack: newUndoStack,\n redoStack: [...redoStack, entry],\n },\n }));\n\n return true;\n}\n\n/**\n * Perform a redo operation on the store.\n * Returns true if a redo was performed, false if redo stack was empty or a draft is active.\n */\nexport function performRedo<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): boolean {\n const state = storeApi.getState();\n const { undoStack, redoStack, activeDraft } = state._commander;\n\n // Redo is disabled while a draft is active\n if (activeDraft) {\n return false;\n }\n\n // Pop the last entry from redo stack\n const entry = redoStack[redoStack.length - 1];\n if (!entry) {\n return false;\n }\n\n const newRedoStack = redoStack.slice(0, -1);\n\n // Create context for re-executing the command\n const ctx: CommandContext<TStore> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as any),\n dispatch: createDispatchForUndo(storeApi),\n transaction: buildTransaction(storeApi),\n };\n\n // Re-execute the command\n const result = entry.command.fn(ctx, entry.params);\n\n // Create new entry with potentially new result\n const newEntry: UndoEntry = {\n command: entry.command,\n params: entry.params,\n result,\n timestamp: Date.now(),\n };\n\n // Move entry back to undo stack\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n undoStack: [...undoStack, newEntry],\n redoStack: newRedoStack,\n },\n }));\n\n return true;\n}\n\n/**\n * Creates a dispatch function for use during undo/redo operations.\n * This dispatch does NOT add to undo stack (to avoid infinite loops).\n */\nfunction createDispatchForUndo<TStore>(\n storeApi: StoreApi<TStore>\n): CommandDispatch<TStore> {\n return <TParams, TReturn>(command: Command<TStore, TParams, TReturn>) => {\n return (params: TParams): TReturn => {\n const ctx: CommandContext<TStore> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as any),\n dispatch: createDispatchForUndo(storeApi),\n transaction: buildTransaction(storeApi),\n };\n\n // Execute without adding to undo stack\n return command.fn(ctx, params);\n };\n };\n}\n\n/**\n * Clear the undo and redo stacks.\n */\nexport function clearUndoHistory<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): void {\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n undoStack: [],\n redoStack: [],\n },\n }));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"commander.mjs","names":["DEFAULT_OPTIONS: Required<CommanderOptions>","_storeApi: StoreApi<TStore & CommanderSlice> | null","ctx: CommandContext<TStore & CommanderSlice, TSchema>","entry: UndoEntry<TParams, TReturn>","ctx: CommandContext<TStore>","newEntry: UndoEntry"],"sources":["../../src/zustand-commander/commander.ts"],"sourcesContent":["/**\n * @voidhash/mimic-react/zustand-commander\n *\n * Commander creation and command definition.\n *\n * @since 0.0.1\n */\n\nimport type { StoreApi } from \"zustand\";\nimport type { Primitive } from \"@voidhash/mimic\";\nimport type { ClientDocument } from \"@voidhash/mimic/client\";\nimport {\n COMMAND_SYMBOL,\n UNDOABLE_COMMAND_SYMBOL,\n isUndoableCommand,\n type Command,\n type Commander,\n type CommanderOptions,\n type CommanderSlice,\n type CommandContext,\n type CommandDispatch,\n type CommandFn,\n type RevertFn,\n type UndoableCommand,\n type UndoEntry,\n} from \"./types\";\n\n// =============================================================================\n// Default Options\n// =============================================================================\n\nconst DEFAULT_OPTIONS: Required<CommanderOptions> = {\n maxUndoStackSize: 100,\n};\n\n// =============================================================================\n// Transaction Helper\n// =============================================================================\n\n/**\n * Build a transaction function that routes to draft or document.\n */\nfunction buildTransaction<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n storeApi: StoreApi<TStore>\n): (fn: (root: Primitive.InferProxy<TSchema>) => void) => void {\n return (fn) => {\n const state = storeApi.getState();\n const draft = state._commander.activeDraft;\n if (draft) {\n draft.update(fn as (root: unknown) => void);\n } else {\n // Access mimic.document from the store\n const mimic = (state as any).mimic;\n if (!mimic?.document) {\n throw new Error(\n \"Commander: No active draft and no mimic document found on the store.\"\n );\n }\n mimic.document.transaction(fn);\n }\n };\n}\n\n// =============================================================================\n// Commander Implementation\n// =============================================================================\n\n/**\n * Creates a commander instance bound to a specific store type.\n *\n * @example\n * ```ts\n * // Create commander for your store type\n * const commander = createCommander<StoreState>();\n *\n * // Define commands\n * const addItem = commander.action(\n * Schema.Struct({ name: Schema.String }),\n * (ctx, params) => {\n * ctx.transaction(root => {\n * // add item\n * });\n * }\n * );\n *\n * // Create store with middleware\n * const useStore = create(\n * commander.middleware(\n * mimic(document, (set, get) => ({\n * // your state\n * }))\n * )\n * );\n * ```\n */\nexport function createCommander<TStore extends object, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n options: CommanderOptions = {}\n): Commander<TStore & CommanderSlice, TSchema> {\n const { maxUndoStackSize } = { ...DEFAULT_OPTIONS, ...options };\n\n // Track the store API once middleware is applied\n let _storeApi: StoreApi<TStore & CommanderSlice> | null = null;\n\n /**\n * Creates the dispatch function for use within command handlers.\n */\n const createDispatch = (): CommandDispatch<TStore & CommanderSlice, TSchema> => {\n return <TParams, TReturn>(\n command: Command<TStore & CommanderSlice, TParams, TReturn, TSchema>\n ) => {\n return (params: TParams): TReturn => {\n if (!_storeApi) {\n throw new Error(\n \"Commander: Store not initialized. Make sure to use the commander middleware.\"\n );\n }\n\n // Create context for the command\n const ctx: CommandContext<TStore & CommanderSlice, TSchema> = {\n getState: () => _storeApi!.getState(),\n setState: (partial) => _storeApi!.setState(partial as any),\n dispatch: createDispatch(),\n transaction: buildTransaction<TStore & CommanderSlice, TSchema>(_storeApi!),\n };\n\n // Execute the command\n const result = command.fn(ctx, params);\n\n // Skip undo stack when a draft is active\n const hasDraft = _storeApi!.getState()._commander.activeDraft !== null;\n\n // If it's an undoable command and no draft is active, add to undo stack\n if (isUndoableCommand(command) && !hasDraft) {\n const entry: UndoEntry<TParams, TReturn> = {\n command,\n params,\n result,\n timestamp: Date.now(),\n };\n\n _storeApi.setState((state: TStore & CommanderSlice) => {\n const { undoStack, redoStack: _redoStack } = state._commander;\n\n // Add to undo stack, respecting max size\n const newUndoStack = [...undoStack, entry].slice(-maxUndoStackSize);\n\n // Clear redo stack when a new command is executed\n return {\n ...state,\n _commander: {\n ...state._commander,\n undoStack: newUndoStack,\n redoStack: [],\n },\n };\n });\n }\n\n return result;\n };\n };\n };\n\n /**\n * Create a regular command (no undo support).\n */\n function action<TParams, TReturn = void>(\n fn: CommandFn<TStore & CommanderSlice, TParams, TReturn, TSchema>\n ): Command<TStore & CommanderSlice, TParams, TReturn, TSchema> {\n return {\n [COMMAND_SYMBOL]: true,\n fn,\n };\n }\n\n /**\n * Create an undoable command with undo/redo support.\n */\n function undoableAction<TParams, TReturn>(\n fn: CommandFn<TStore & CommanderSlice, TParams, TReturn, TSchema>,\n revert: RevertFn<TStore & CommanderSlice, TParams, TReturn, TSchema>\n ): UndoableCommand<TStore & CommanderSlice, TParams, TReturn, TSchema> {\n return {\n [COMMAND_SYMBOL]: true,\n [UNDOABLE_COMMAND_SYMBOL]: true,\n fn,\n revert,\n };\n }\n\n /**\n * Zustand middleware that adds commander functionality.\n */\n const middleware = <T extends object>(\n config: (\n set: StoreApi<T & CommanderSlice>[\"setState\"],\n get: StoreApi<T & CommanderSlice>[\"getState\"],\n api: StoreApi<T & CommanderSlice>\n ) => T\n ) => {\n return (\n set: StoreApi<T & CommanderSlice>[\"setState\"],\n get: StoreApi<T & CommanderSlice>[\"getState\"],\n api: StoreApi<T & CommanderSlice>\n ): T & CommanderSlice => {\n // Store the API reference for dispatch\n _storeApi = api as unknown as StoreApi<TStore & CommanderSlice>;\n\n // Get user's state\n const userState = config(set, get, api);\n\n // Add commander slice\n return {\n ...userState,\n _commander: {\n undoStack: [],\n redoStack: [],\n activeDraft: null,\n },\n };\n };\n };\n\n return {\n action,\n undoableAction,\n middleware: middleware as Commander<TStore & CommanderSlice, TSchema>[\"middleware\"],\n };\n}\n\n// =============================================================================\n// Draft Helpers\n// =============================================================================\n\n/**\n * Set the active draft on the commander slice.\n * While a draft is active, transactions route through `draft.update()` and undo is disabled.\n */\nexport function setActiveDraft<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>,\n draft: ClientDocument.DraftHandle<any>\n): void {\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n activeDraft: draft,\n },\n }));\n}\n\n/**\n * Clear the active draft from the commander slice.\n */\nexport function clearActiveDraft<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): void {\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n activeDraft: null,\n },\n }));\n}\n\n// =============================================================================\n// Undo/Redo Functions\n// =============================================================================\n\n/**\n * Perform an undo operation on the store.\n * Returns true if an undo was performed, false if undo stack was empty or a draft is active.\n */\nexport function performUndo<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): boolean {\n const state = storeApi.getState();\n const { undoStack, redoStack, activeDraft } = state._commander;\n\n // Undo is disabled while a draft is active\n if (activeDraft) {\n return false;\n }\n\n // Pop the last entry from undo stack\n const entry = undoStack[undoStack.length - 1];\n if (!entry) {\n return false;\n }\n\n const newUndoStack = undoStack.slice(0, -1);\n\n // Create context for the revert function\n const ctx: CommandContext<TStore> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as any),\n dispatch: createDispatchForUndo(storeApi),\n transaction: buildTransaction(storeApi),\n };\n\n // Execute the revert function\n entry.command.revert(ctx, entry.params, entry.result);\n\n // Move entry to redo stack\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n undoStack: newUndoStack,\n redoStack: [...redoStack, entry],\n },\n }));\n\n return true;\n}\n\n/**\n * Perform a redo operation on the store.\n * Returns true if a redo was performed, false if redo stack was empty or a draft is active.\n */\nexport function performRedo<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): boolean {\n const state = storeApi.getState();\n const { undoStack, redoStack, activeDraft } = state._commander;\n\n // Redo is disabled while a draft is active\n if (activeDraft) {\n return false;\n }\n\n // Pop the last entry from redo stack\n const entry = redoStack[redoStack.length - 1];\n if (!entry) {\n return false;\n }\n\n const newRedoStack = redoStack.slice(0, -1);\n\n // Create context for re-executing the command\n const ctx: CommandContext<TStore> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as any),\n dispatch: createDispatchForUndo(storeApi),\n transaction: buildTransaction(storeApi),\n };\n\n // Re-execute the command\n const result = entry.command.fn(ctx, entry.params);\n\n // Create new entry with potentially new result\n const newEntry: UndoEntry = {\n command: entry.command,\n params: entry.params,\n result,\n timestamp: Date.now(),\n };\n\n // Move entry back to undo stack\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n undoStack: [...undoStack, newEntry],\n redoStack: newRedoStack,\n },\n }));\n\n return true;\n}\n\n/**\n * Creates a dispatch function for use during undo/redo operations.\n * This dispatch does NOT add to undo stack (to avoid infinite loops).\n */\nfunction createDispatchForUndo<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): CommandDispatch<TStore> {\n return <TParams, TReturn>(command: Command<TStore, TParams, TReturn>) => {\n return (params: TParams): TReturn => {\n const ctx: CommandContext<TStore> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as any),\n dispatch: createDispatchForUndo(storeApi),\n transaction: buildTransaction(storeApi),\n };\n\n // Execute without adding to undo stack\n return command.fn(ctx, params);\n };\n };\n}\n\n/**\n * Clear the undo and redo stacks.\n */\nexport function clearUndoHistory<TStore extends CommanderSlice>(\n storeApi: StoreApi<TStore>\n): void {\n storeApi.setState((state: TStore) => ({\n ...state,\n _commander: {\n ...state._commander,\n undoStack: [],\n redoStack: [],\n },\n }));\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.mjs","names":["dispatch: CommandDispatch<TStore, TSchema>","ctx: CommandContext<TStore, TSchema>"],"sources":["../../src/zustand-commander/hooks.ts"],"sourcesContent":["/**\n * @voidhash/mimic-react/zustand-commander\n *\n * React hooks for zustand-commander.\n *\n * @since 0.0.1\n */\n\nimport { useCallback, useEffect, useMemo } from \"react\";\nimport { useStore, type StoreApi, type UseBoundStore } from \"zustand\";\nimport type { Primitive } from \"@voidhash/mimic\";\nimport { performRedo, performUndo, clearUndoHistory, setActiveDraft, clearActiveDraft } from \"./commander\";\nimport {\n isUndoableCommand,\n type Command,\n type CommandContext,\n type CommandDispatch,\n type CommanderSlice,\n type ExtractState,\n} from \"./types\";\n\n// =============================================================================\n// useCommander Hook\n// =============================================================================\n\n/**\n * Creates a dispatch function for commands.\n * This is for use outside of React components (e.g., in command handlers).\n */\nfunction buildTransactionFromApi<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n storeApi: StoreApi<TStore>\n): (fn: (root: Primitive.InferProxy<TSchema>) => void) => void {\n return (fn) => {\n const state = storeApi.getState();\n const draft = state._commander.activeDraft;\n if (draft) {\n draft.update(fn);\n } else {\n const mimic = (state as any).mimic;\n if (!mimic?.document) {\n throw new Error(\n \"Commander: No active draft and no mimic document found on the store.\"\n );\n }\n mimic.document.transaction(fn);\n }\n };\n}\n\nfunction createDispatchFromApi<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n storeApi: StoreApi<TStore>,\n maxUndoStackSize = 100\n): CommandDispatch<TStore, TSchema> {\n const dispatch: CommandDispatch<TStore, TSchema> = <TParams, TReturn>(\n command: Command<TStore, TParams, TReturn, TSchema>\n ) => {\n return (params: TParams): TReturn => {\n // Create context for the command\n const ctx: CommandContext<TStore, TSchema> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as Partial<TStore>),\n dispatch,\n transaction: buildTransactionFromApi<TStore, TSchema>(storeApi),\n };\n\n // Execute the command\n const result = command.fn(ctx, params);\n\n // Skip undo stack when a draft is active\n const hasDraft = storeApi.getState()._commander.activeDraft !== null;\n\n // If it's an undoable command and no draft is active, add to undo stack\n if (isUndoableCommand(command) && !hasDraft) {\n storeApi.setState((state: TStore) => {\n const { undoStack } = state._commander;\n\n // Add to undo stack, respecting max size\n const newUndoStack = [\n ...undoStack,\n {\n command,\n params,\n result,\n timestamp: Date.now(),\n },\n ].slice(-maxUndoStackSize);\n\n // Clear redo stack when a new command is executed\n return {\n ...state,\n _commander: {\n ...state._commander,\n undoStack: newUndoStack,\n redoStack: [],\n },\n } as TStore;\n });\n }\n\n return result;\n };\n };\n\n return dispatch;\n}\n\n/**\n * React hook to get a dispatch function for commands.\n * The dispatch function executes commands and manages undo/redo state.\n *\n * @example\n * ```tsx\n * const dispatch = useCommander(useStore);\n *\n * const handleClick = () => {\n * dispatch(addCard)({ columnId: \"col-1\", title: \"New Card\" });\n * };\n * ```\n */\nexport function useCommander<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n store: UseBoundStore<StoreApi<TStore>>\n): CommandDispatch<TStore, TSchema> {\n // Get the store API\n const storeApi = useMemo(() => {\n // UseBoundStore has the StoreApi attached\n return store as unknown as StoreApi<TStore>;\n }, [store]);\n\n // Create a stable dispatch function\n const dispatch = useMemo(\n () => createDispatchFromApi<TStore, TSchema>(storeApi),\n [storeApi]\n );\n\n return dispatch as CommandDispatch<TStore, TSchema>;\n}\n\n// =============================================================================\n// useUndoRedo Hook\n// =============================================================================\n\n/**\n * State and actions for undo/redo functionality.\n */\nexport interface UndoRedoState {\n /** Whether there are actions that can be undone */\n readonly canUndo: boolean;\n /** Whether there are actions that can be redone */\n readonly canRedo: boolean;\n /** Number of items in the undo stack */\n readonly undoCount: number;\n /** Number of items in the redo stack */\n readonly redoCount: number;\n /** Undo the last action */\n readonly undo: () => boolean;\n /** Redo the last undone action */\n readonly redo: () => boolean;\n /** Clear the undo/redo history */\n readonly clear: () => void;\n}\n\n/**\n * React hook for undo/redo functionality.\n * Provides state (canUndo, canRedo) and actions (undo, redo, clear).\n *\n * @example\n * ```tsx\n * const { canUndo, canRedo, undo, redo } = useUndoRedo(useStore);\n *\n * return (\n * <>\n * <button onClick={undo} disabled={!canUndo}>Undo</button>\n * <button onClick={redo} disabled={!canRedo}>Redo</button>\n * </>\n * );\n * ```\n */\nexport function useUndoRedo<TStore extends CommanderSlice>(\n store: UseBoundStore<StoreApi<TStore>>\n): UndoRedoState {\n // Get the store API\n const storeApi = useMemo(() => {\n return store as unknown as StoreApi<TStore>;\n }, [store]);\n\n // Subscribe to commander state\n const commanderState = useStore(\n store,\n (state: TStore) => state._commander\n );\n\n const canUndo = commanderState.undoStack.length > 0;\n const canRedo = commanderState.redoStack.length > 0;\n const undoCount = commanderState.undoStack.length;\n const redoCount = commanderState.redoStack.length;\n\n const undo = useCallback(() => {\n return performUndo(storeApi);\n }, [storeApi]);\n\n const redo = useCallback(() => {\n return performRedo(storeApi);\n }, [storeApi]);\n\n const clear = useCallback(() => {\n clearUndoHistory(storeApi);\n }, [storeApi]);\n\n return {\n canUndo,\n canRedo,\n undoCount,\n redoCount,\n undo,\n redo,\n clear,\n };\n}\n\n// =============================================================================\n// Keyboard Shortcut Hook\n// =============================================================================\n\n/**\n * Options for the keyboard shortcut hook.\n */\nexport interface UseUndoRedoKeyboardOptions {\n /** Enable Ctrl/Cmd+Z for undo (default: true) */\n readonly enableUndo?: boolean;\n /** Enable Ctrl/Cmd+Shift+Z or Ctrl+Y for redo (default: true) */\n readonly enableRedo?: boolean;\n}\n\n/**\n * React hook that adds keyboard shortcuts for undo/redo.\n * Listens for Ctrl/Cmd+Z (undo) and Ctrl/Cmd+Shift+Z or Ctrl+Y (redo).\n *\n * @example\n * ```tsx\n * // In your app component\n * useUndoRedoKeyboard(useStore);\n * ```\n */\nexport function useUndoRedoKeyboard<TStore extends CommanderSlice>(\n store: UseBoundStore<StoreApi<TStore>>,\n options: UseUndoRedoKeyboardOptions = {}\n): void {\n const { enableUndo = true, enableRedo = true } = options;\n\n const storeApi = useMemo(() => {\n return store as unknown as StoreApi<TStore>;\n }, [store]);\n\n // Set up keyboard listener\n useEffect(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n const modKey = isMac ? event.metaKey : event.ctrlKey;\n\n if (!modKey) return;\n\n // Undo: Ctrl/Cmd + Z (without Shift)\n if (enableUndo && event.key === \"z\" && !event.shiftKey) {\n event.preventDefault();\n performUndo(storeApi);\n return;\n }\n\n // Redo: Ctrl/Cmd + Shift + Z or Ctrl + Y\n if (enableRedo) {\n if ((event.key === \"z\" && event.shiftKey) || event.key === \"y\") {\n event.preventDefault();\n performRedo(storeApi);\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [storeApi, enableUndo, enableRedo]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAS,wBACP,UAC6D;AAC7D,SAAQ,OAAO;EACb,MAAM,QAAQ,SAAS,UAAU;EACjC,MAAM,QAAQ,MAAM,WAAW;AAC/B,MAAI,MACF,OAAM,OAAO,GAAG;OACX;GACL,MAAM,QAAS,MAAc;AAC7B,OAAI,gDAAC,MAAO,UACV,OAAM,IAAI,MACR,uEACD;AAEH,SAAM,SAAS,YAAY,GAAG;;;;AAKpC,SAAS,sBACP,UACA,mBAAmB,KACe;CAClC,MAAMA,YACJ,YACG;AACH,UAAQ,WAA6B;GAEnC,MAAMC,MAAuC;IAC3C,gBAAgB,SAAS,UAAU;IACnC,WAAW,YAAY,SAAS,SAAS,QAA2B;IACpE;IACA,aAAa,wBAAyC,SAAS;IAChE;GAGD,MAAM,SAAS,QAAQ,GAAG,KAAK,OAAO;GAGtC,MAAM,WAAW,SAAS,UAAU,CAAC,WAAW,gBAAgB;AAGhE,OAAI,kBAAkB,QAAQ,IAAI,CAAC,SACjC,UAAS,UAAU,UAAkB;IACnC,MAAM,EAAE,cAAc,MAAM;IAG5B,MAAM,eAAe,CACnB,GAAG,WACH;KACE;KACA;KACA;KACA,WAAW,KAAK,KAAK;KACtB,CACF,CAAC,MAAM,CAAC,iBAAiB;AAG1B,6CACK,cACH,8CACK,MAAM;KACT,WAAW;KACX,WAAW,EAAE;;KAGjB;AAGJ,UAAO;;;AAIX,QAAO;;;;;;;;;;;;;;;AAgBT,SAAgB,aACd,OACkC;CAElC,MAAM,WAAW,cAAc;AAE7B,SAAO;IACN,CAAC,MAAM,CAAC;AAQX,QALiB,cACT,sBAAuC,SAAS,EACtD,CAAC,SAAS,CACX;;;;;;;;;;;;;;;;;;AA6CH,SAAgB,YACd,OACe;CAEf,MAAM,WAAW,cAAc;AAC7B,SAAO;IACN,CAAC,MAAM,CAAC;CAGX,MAAM,iBAAiB,SACrB,QACC,UAAkB,MAAM,WAC1B;AAmBD,QAAO;EACL,SAlBc,eAAe,UAAU,SAAS;EAmBhD,SAlBc,eAAe,UAAU,SAAS;EAmBhD,WAlBgB,eAAe,UAAU;EAmBzC,WAlBgB,eAAe,UAAU;EAmBzC,MAjBW,kBAAkB;AAC7B,UAAO,YAAY,SAAS;KAC3B,CAAC,SAAS,CAAC;EAgBZ,MAdW,kBAAkB;AAC7B,UAAO,YAAY,SAAS;KAC3B,CAAC,SAAS,CAAC;EAaZ,OAXY,kBAAkB;AAC9B,oBAAiB,SAAS;KACzB,CAAC,SAAS,CAAC;EAUb;;;;;;;;;;;;AA2BH,SAAgB,oBACd,OACA,UAAsC,EAAE,EAClC;CACN,MAAM,EAAE,aAAa,MAAM,aAAa,SAAS;CAEjD,MAAM,WAAW,cAAc;AAC7B,SAAO;IACN,CAAC,MAAM,CAAC;AAGX,iBAAgB;AACd,MAAI,OAAO,WAAW,YACpB;EAGF,MAAM,iBAAiB,UAAyB;AAI9C,OAAI,EAHU,UAAU,SAAS,aAAa,CAAC,QAAQ,MAAM,IAAI,IAC1C,MAAM,UAAU,MAAM,SAEhC;AAGb,OAAI,cAAc,MAAM,QAAQ,OAAO,CAAC,MAAM,UAAU;AACtD,UAAM,gBAAgB;AACtB,gBAAY,SAAS;AACrB;;AAIF,OAAI,YACF;QAAK,MAAM,QAAQ,OAAO,MAAM,YAAa,MAAM,QAAQ,KAAK;AAC9D,WAAM,gBAAgB;AACtB,iBAAY,SAAS;;;;AAK3B,SAAO,iBAAiB,WAAW,cAAc;AACjD,eAAa,OAAO,oBAAoB,WAAW,cAAc;IAChE;EAAC;EAAU;EAAY;EAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"hooks.mjs","names":["dispatch: CommandDispatch<TStore, TSchema>","ctx: CommandContext<TStore, TSchema>"],"sources":["../../src/zustand-commander/hooks.ts"],"sourcesContent":["/**\n * @voidhash/mimic-react/zustand-commander\n *\n * React hooks for zustand-commander.\n *\n * @since 0.0.1\n */\n\nimport { useCallback, useEffect, useMemo } from \"react\";\nimport { useStore, type StoreApi, type UseBoundStore } from \"zustand\";\nimport type { Primitive } from \"@voidhash/mimic\";\nimport { performRedo, performUndo, clearUndoHistory, setActiveDraft, clearActiveDraft } from \"./commander\";\nimport {\n isUndoableCommand,\n type Command,\n type CommandContext,\n type CommandDispatch,\n type CommanderSlice,\n type ExtractState,\n} from \"./types\";\n\n// =============================================================================\n// useCommander Hook\n// =============================================================================\n\n/**\n * Creates a dispatch function for commands.\n * This is for use outside of React components (e.g., in command handlers).\n */\nfunction buildTransactionFromApi<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n storeApi: StoreApi<TStore>\n): (fn: (root: Primitive.InferProxy<TSchema>) => void) => void {\n return (fn) => {\n const state = storeApi.getState();\n const draft = state._commander.activeDraft;\n if (draft) {\n draft.update(fn as (root: unknown) => void);\n } else {\n const mimic = (state as any).mimic;\n if (!mimic?.document) {\n throw new Error(\n \"Commander: No active draft and no mimic document found on the store.\"\n );\n }\n mimic.document.transaction(fn);\n }\n };\n}\n\nfunction createDispatchFromApi<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n storeApi: StoreApi<TStore>,\n maxUndoStackSize = 100\n): CommandDispatch<TStore, TSchema> {\n const dispatch: CommandDispatch<TStore, TSchema> = <TParams, TReturn>(\n command: Command<TStore, TParams, TReturn, TSchema>\n ) => {\n return (params: TParams): TReturn => {\n // Create context for the command\n const ctx: CommandContext<TStore, TSchema> = {\n getState: () => storeApi.getState(),\n setState: (partial) => storeApi.setState(partial as Partial<TStore>),\n dispatch,\n transaction: buildTransactionFromApi<TStore, TSchema>(storeApi),\n };\n\n // Execute the command\n const result = command.fn(ctx, params);\n\n // Skip undo stack when a draft is active\n const hasDraft = storeApi.getState()._commander.activeDraft !== null;\n\n // If it's an undoable command and no draft is active, add to undo stack\n if (isUndoableCommand(command) && !hasDraft) {\n storeApi.setState((state: TStore) => {\n const { undoStack } = state._commander;\n\n // Add to undo stack, respecting max size\n const newUndoStack = [\n ...undoStack,\n {\n command,\n params,\n result,\n timestamp: Date.now(),\n },\n ].slice(-maxUndoStackSize);\n\n // Clear redo stack when a new command is executed\n return {\n ...state,\n _commander: {\n ...state._commander,\n undoStack: newUndoStack,\n redoStack: [],\n },\n } as TStore;\n });\n }\n\n return result;\n };\n };\n\n return dispatch;\n}\n\n/**\n * React hook to get a dispatch function for commands.\n * The dispatch function executes commands and manages undo/redo state.\n *\n * @example\n * ```tsx\n * const dispatch = useCommander(useStore);\n *\n * const handleClick = () => {\n * dispatch(addCard)({ columnId: \"col-1\", title: \"New Card\" });\n * };\n * ```\n */\nexport function useCommander<TStore extends CommanderSlice, TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive>(\n store: UseBoundStore<StoreApi<TStore>>\n): CommandDispatch<TStore, TSchema> {\n // Get the store API\n const storeApi = useMemo(() => {\n // UseBoundStore has the StoreApi attached\n return store as unknown as StoreApi<TStore>;\n }, [store]);\n\n // Create a stable dispatch function\n const dispatch = useMemo(\n () => createDispatchFromApi<TStore, TSchema>(storeApi),\n [storeApi]\n );\n\n return dispatch as CommandDispatch<TStore, TSchema>;\n}\n\n// =============================================================================\n// useUndoRedo Hook\n// =============================================================================\n\n/**\n * State and actions for undo/redo functionality.\n */\nexport interface UndoRedoState {\n /** Whether there are actions that can be undone */\n readonly canUndo: boolean;\n /** Whether there are actions that can be redone */\n readonly canRedo: boolean;\n /** Number of items in the undo stack */\n readonly undoCount: number;\n /** Number of items in the redo stack */\n readonly redoCount: number;\n /** Undo the last action */\n readonly undo: () => boolean;\n /** Redo the last undone action */\n readonly redo: () => boolean;\n /** Clear the undo/redo history */\n readonly clear: () => void;\n}\n\n/**\n * React hook for undo/redo functionality.\n * Provides state (canUndo, canRedo) and actions (undo, redo, clear).\n *\n * @example\n * ```tsx\n * const { canUndo, canRedo, undo, redo } = useUndoRedo(useStore);\n *\n * return (\n * <>\n * <button onClick={undo} disabled={!canUndo}>Undo</button>\n * <button onClick={redo} disabled={!canRedo}>Redo</button>\n * </>\n * );\n * ```\n */\nexport function useUndoRedo<TStore extends CommanderSlice>(\n store: UseBoundStore<StoreApi<TStore>>\n): UndoRedoState {\n // Get the store API\n const storeApi = useMemo(() => {\n return store as unknown as StoreApi<TStore>;\n }, [store]);\n\n // Subscribe to commander state\n const commanderState = useStore(\n store,\n (state: TStore) => state._commander\n );\n\n const canUndo = commanderState.undoStack.length > 0;\n const canRedo = commanderState.redoStack.length > 0;\n const undoCount = commanderState.undoStack.length;\n const redoCount = commanderState.redoStack.length;\n\n const undo = useCallback(() => {\n return performUndo(storeApi);\n }, [storeApi]);\n\n const redo = useCallback(() => {\n return performRedo(storeApi);\n }, [storeApi]);\n\n const clear = useCallback(() => {\n clearUndoHistory(storeApi);\n }, [storeApi]);\n\n return {\n canUndo,\n canRedo,\n undoCount,\n redoCount,\n undo,\n redo,\n clear,\n };\n}\n\n// =============================================================================\n// Keyboard Shortcut Hook\n// =============================================================================\n\n/**\n * Options for the keyboard shortcut hook.\n */\nexport interface UseUndoRedoKeyboardOptions {\n /** Enable Ctrl/Cmd+Z for undo (default: true) */\n readonly enableUndo?: boolean;\n /** Enable Ctrl/Cmd+Shift+Z or Ctrl+Y for redo (default: true) */\n readonly enableRedo?: boolean;\n}\n\n/**\n * React hook that adds keyboard shortcuts for undo/redo.\n * Listens for Ctrl/Cmd+Z (undo) and Ctrl/Cmd+Shift+Z or Ctrl+Y (redo).\n *\n * @example\n * ```tsx\n * // In your app component\n * useUndoRedoKeyboard(useStore);\n * ```\n */\nexport function useUndoRedoKeyboard<TStore extends CommanderSlice>(\n store: UseBoundStore<StoreApi<TStore>>,\n options: UseUndoRedoKeyboardOptions = {}\n): void {\n const { enableUndo = true, enableRedo = true } = options;\n\n const storeApi = useMemo(() => {\n return store as unknown as StoreApi<TStore>;\n }, [store]);\n\n // Set up keyboard listener\n useEffect(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const handleKeyDown = (event: KeyboardEvent) => {\n const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n const modKey = isMac ? event.metaKey : event.ctrlKey;\n\n if (!modKey) return;\n\n // Undo: Ctrl/Cmd + Z (without Shift)\n if (enableUndo && event.key === \"z\" && !event.shiftKey) {\n event.preventDefault();\n performUndo(storeApi);\n return;\n }\n\n // Redo: Ctrl/Cmd + Shift + Z or Ctrl + Y\n if (enableRedo) {\n if ((event.key === \"z\" && event.shiftKey) || event.key === \"y\") {\n event.preventDefault();\n performRedo(storeApi);\n }\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n return () => window.removeEventListener(\"keydown\", handleKeyDown);\n }, [storeApi, enableUndo, enableRedo]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAS,wBACP,UAC6D;AAC7D,SAAQ,OAAO;EACb,MAAM,QAAQ,SAAS,UAAU;EACjC,MAAM,QAAQ,MAAM,WAAW;AAC/B,MAAI,MACF,OAAM,OAAO,GAA8B;OACtC;GACL,MAAM,QAAS,MAAc;AAC7B,OAAI,gDAAC,MAAO,UACV,OAAM,IAAI,MACR,uEACD;AAEH,SAAM,SAAS,YAAY,GAAG;;;;AAKpC,SAAS,sBACP,UACA,mBAAmB,KACe;CAClC,MAAMA,YACJ,YACG;AACH,UAAQ,WAA6B;GAEnC,MAAMC,MAAuC;IAC3C,gBAAgB,SAAS,UAAU;IACnC,WAAW,YAAY,SAAS,SAAS,QAA2B;IACpE;IACA,aAAa,wBAAyC,SAAS;IAChE;GAGD,MAAM,SAAS,QAAQ,GAAG,KAAK,OAAO;GAGtC,MAAM,WAAW,SAAS,UAAU,CAAC,WAAW,gBAAgB;AAGhE,OAAI,kBAAkB,QAAQ,IAAI,CAAC,SACjC,UAAS,UAAU,UAAkB;IACnC,MAAM,EAAE,cAAc,MAAM;IAG5B,MAAM,eAAe,CACnB,GAAG,WACH;KACE;KACA;KACA;KACA,WAAW,KAAK,KAAK;KACtB,CACF,CAAC,MAAM,CAAC,iBAAiB;AAG1B,6CACK,cACH,8CACK,MAAM;KACT,WAAW;KACX,WAAW,EAAE;;KAGjB;AAGJ,UAAO;;;AAIX,QAAO;;;;;;;;;;;;;;;AAgBT,SAAgB,aACd,OACkC;CAElC,MAAM,WAAW,cAAc;AAE7B,SAAO;IACN,CAAC,MAAM,CAAC;AAQX,QALiB,cACT,sBAAuC,SAAS,EACtD,CAAC,SAAS,CACX;;;;;;;;;;;;;;;;;;AA6CH,SAAgB,YACd,OACe;CAEf,MAAM,WAAW,cAAc;AAC7B,SAAO;IACN,CAAC,MAAM,CAAC;CAGX,MAAM,iBAAiB,SACrB,QACC,UAAkB,MAAM,WAC1B;AAmBD,QAAO;EACL,SAlBc,eAAe,UAAU,SAAS;EAmBhD,SAlBc,eAAe,UAAU,SAAS;EAmBhD,WAlBgB,eAAe,UAAU;EAmBzC,WAlBgB,eAAe,UAAU;EAmBzC,MAjBW,kBAAkB;AAC7B,UAAO,YAAY,SAAS;KAC3B,CAAC,SAAS,CAAC;EAgBZ,MAdW,kBAAkB;AAC7B,UAAO,YAAY,SAAS;KAC3B,CAAC,SAAS,CAAC;EAaZ,OAXY,kBAAkB;AAC9B,oBAAiB,SAAS;KACzB,CAAC,SAAS,CAAC;EAUb;;;;;;;;;;;;AA2BH,SAAgB,oBACd,OACA,UAAsC,EAAE,EAClC;CACN,MAAM,EAAE,aAAa,MAAM,aAAa,SAAS;CAEjD,MAAM,WAAW,cAAc;AAC7B,SAAO;IACN,CAAC,MAAM,CAAC;AAGX,iBAAgB;AACd,MAAI,OAAO,WAAW,YACpB;EAGF,MAAM,iBAAiB,UAAyB;AAI9C,OAAI,EAHU,UAAU,SAAS,aAAa,CAAC,QAAQ,MAAM,IAAI,IAC1C,MAAM,UAAU,MAAM,SAEhC;AAGb,OAAI,cAAc,MAAM,QAAQ,OAAO,CAAC,MAAM,UAAU;AACtD,UAAM,gBAAgB;AACtB,gBAAY,SAAS;AACrB;;AAIF,OAAI,YACF;QAAK,MAAM,QAAQ,OAAO,MAAM,YAAa,MAAM,QAAQ,KAAK;AAC9D,WAAM,gBAAgB;AACtB,iBAAY,SAAS;;;;AAK3B,SAAO,iBAAiB,WAAW,cAAc;AACjD,eAAa,OAAO,oBAAoB,WAAW,cAAc;IAChE;EAAC;EAAU;EAAY;EAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidhash/mimic-react",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"typescript": "5.8.3",
|
|
38
38
|
"vite-tsconfig-paths": "^5.1.4",
|
|
39
39
|
"vitest": "^3.2.4",
|
|
40
|
-
"@voidhash/tsconfig": "1.0.0-beta.
|
|
40
|
+
"@voidhash/tsconfig": "1.0.0-beta.16"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"effect": "^3.16.0",
|
|
44
44
|
"react": "^18.0.0 || ^19.0.0",
|
|
45
|
-
"@voidhash/mimic": "1.0.0-beta.
|
|
45
|
+
"@voidhash/mimic": "1.0.0-beta.16"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsdown",
|
|
@@ -47,7 +47,7 @@ function buildTransaction<TStore extends CommanderSlice, TSchema extends Primiti
|
|
|
47
47
|
const state = storeApi.getState();
|
|
48
48
|
const draft = state._commander.activeDraft;
|
|
49
49
|
if (draft) {
|
|
50
|
-
draft.update(fn);
|
|
50
|
+
draft.update(fn as (root: unknown) => void);
|
|
51
51
|
} else {
|
|
52
52
|
// Access mimic.document from the store
|
|
53
53
|
const mimic = (state as any).mimic;
|
|
@@ -374,7 +374,7 @@ export function performRedo<TStore extends CommanderSlice>(
|
|
|
374
374
|
* Creates a dispatch function for use during undo/redo operations.
|
|
375
375
|
* This dispatch does NOT add to undo stack (to avoid infinite loops).
|
|
376
376
|
*/
|
|
377
|
-
function createDispatchForUndo<TStore>(
|
|
377
|
+
function createDispatchForUndo<TStore extends CommanderSlice>(
|
|
378
378
|
storeApi: StoreApi<TStore>
|
|
379
379
|
): CommandDispatch<TStore> {
|
|
380
380
|
return <TParams, TReturn>(command: Command<TStore, TParams, TReturn>) => {
|
|
@@ -34,7 +34,7 @@ function buildTransactionFromApi<TStore extends CommanderSlice, TSchema extends
|
|
|
34
34
|
const state = storeApi.getState();
|
|
35
35
|
const draft = state._commander.activeDraft;
|
|
36
36
|
if (draft) {
|
|
37
|
-
draft.update(fn);
|
|
37
|
+
draft.update(fn as (root: unknown) => void);
|
|
38
38
|
} else {
|
|
39
39
|
const mimic = (state as any).mimic;
|
|
40
40
|
if (!mimic?.document) {
|
|
@@ -668,6 +668,327 @@ describe("Command Dispatch in Context", () => {
|
|
|
668
668
|
});
|
|
669
669
|
});
|
|
670
670
|
|
|
671
|
+
describe("ctx.transaction routing (draft vs document)", () => {
|
|
672
|
+
it("should route ctx.transaction to document.transaction when no draft is active", () => {
|
|
673
|
+
const commander = createCommander<TestState>();
|
|
674
|
+
|
|
675
|
+
// Track calls to document.transaction
|
|
676
|
+
const transactionCalls: Array<(root: any) => void> = [];
|
|
677
|
+
const mockDocument = {
|
|
678
|
+
transaction: (fn: (root: any) => void) => {
|
|
679
|
+
transactionCalls.push(fn);
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Create store with mimic slice containing the mock document
|
|
684
|
+
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
685
|
+
commander.middleware(() => ({
|
|
686
|
+
count: 0,
|
|
687
|
+
items: [],
|
|
688
|
+
selectedId: null,
|
|
689
|
+
mimic: { document: mockDocument },
|
|
690
|
+
}))
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
// Create a command that uses ctx.transaction
|
|
694
|
+
const updateViaTransaction = commander.action<{ value: number }>(
|
|
695
|
+
(ctx, params) => {
|
|
696
|
+
ctx.transaction((root: any) => {
|
|
697
|
+
root.count.set(params.value);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
703
|
+
|
|
704
|
+
// Build a proper context with transaction routing
|
|
705
|
+
const ctx: CommandContext<TestStore> = {
|
|
706
|
+
getState: () => storeApi.getState(),
|
|
707
|
+
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
708
|
+
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
709
|
+
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
710
|
+
},
|
|
711
|
+
transaction: (fn: (root: any) => void) => {
|
|
712
|
+
const state = storeApi.getState();
|
|
713
|
+
const draft = state._commander.activeDraft;
|
|
714
|
+
if (draft) {
|
|
715
|
+
draft.update(fn);
|
|
716
|
+
} else {
|
|
717
|
+
(state as any).mimic.document.transaction(fn);
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
// Execute command - should route to document.transaction
|
|
723
|
+
updateViaTransaction.fn(ctx, { value: 42 });
|
|
724
|
+
|
|
725
|
+
expect(transactionCalls.length).toBe(1);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it("should route ctx.transaction to draft.update when draft is active", () => {
|
|
729
|
+
const commander = createCommander<TestState>();
|
|
730
|
+
|
|
731
|
+
// Track calls to both document.transaction and draft.update
|
|
732
|
+
const documentTransactionCalls: Array<(root: any) => void> = [];
|
|
733
|
+
const draftUpdateCalls: Array<(root: any) => void> = [];
|
|
734
|
+
|
|
735
|
+
const mockDocument = {
|
|
736
|
+
transaction: (fn: (root: any) => void) => {
|
|
737
|
+
documentTransactionCalls.push(fn);
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const mockDraft = {
|
|
742
|
+
update: (fn: (root: any) => void) => {
|
|
743
|
+
draftUpdateCalls.push(fn);
|
|
744
|
+
},
|
|
745
|
+
commit: () => {},
|
|
746
|
+
discard: () => {},
|
|
747
|
+
id: "mock-draft-id",
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// Create store with mimic slice
|
|
751
|
+
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
752
|
+
commander.middleware(() => ({
|
|
753
|
+
count: 0,
|
|
754
|
+
items: [],
|
|
755
|
+
selectedId: null,
|
|
756
|
+
mimic: { document: mockDocument },
|
|
757
|
+
}))
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
// Set the active draft
|
|
761
|
+
store.setState((state) => ({
|
|
762
|
+
...state,
|
|
763
|
+
_commander: {
|
|
764
|
+
...state._commander,
|
|
765
|
+
activeDraft: mockDraft as any,
|
|
766
|
+
},
|
|
767
|
+
}));
|
|
768
|
+
|
|
769
|
+
// Create a command that uses ctx.transaction
|
|
770
|
+
const updateViaTransaction = commander.action<{ value: number }>(
|
|
771
|
+
(ctx, params) => {
|
|
772
|
+
ctx.transaction((root: any) => {
|
|
773
|
+
root.count.set(params.value);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
779
|
+
|
|
780
|
+
// Build a proper context with transaction routing
|
|
781
|
+
const ctx: CommandContext<TestStore> = {
|
|
782
|
+
getState: () => storeApi.getState(),
|
|
783
|
+
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
784
|
+
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
785
|
+
return (params: TParams): TReturn => cmd.fn(ctx, params);
|
|
786
|
+
},
|
|
787
|
+
transaction: (fn: (root: any) => void) => {
|
|
788
|
+
const state = storeApi.getState();
|
|
789
|
+
const draft = state._commander.activeDraft;
|
|
790
|
+
if (draft) {
|
|
791
|
+
draft.update(fn);
|
|
792
|
+
} else {
|
|
793
|
+
(state as any).mimic.document.transaction(fn);
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// Execute command - should route to draft.update, NOT document.transaction
|
|
799
|
+
updateViaTransaction.fn(ctx, { value: 42 });
|
|
800
|
+
|
|
801
|
+
expect(draftUpdateCalls.length).toBe(1);
|
|
802
|
+
expect(documentTransactionCalls.length).toBe(0);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it("should never call document.transaction while draft is active (explicit verification)", () => {
|
|
806
|
+
const commander = createCommander<TestState>();
|
|
807
|
+
|
|
808
|
+
// Track ALL calls
|
|
809
|
+
const documentTransactionCalls: Array<{ fn: (root: any) => void; timestamp: number }> = [];
|
|
810
|
+
const draftUpdateCalls: Array<{ fn: (root: any) => void; timestamp: number }> = [];
|
|
811
|
+
|
|
812
|
+
const mockDocument = {
|
|
813
|
+
transaction: (fn: (root: any) => void) => {
|
|
814
|
+
documentTransactionCalls.push({ fn, timestamp: Date.now() });
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const mockDraft = {
|
|
819
|
+
update: (fn: (root: any) => void) => {
|
|
820
|
+
draftUpdateCalls.push({ fn, timestamp: Date.now() });
|
|
821
|
+
},
|
|
822
|
+
commit: () => {},
|
|
823
|
+
discard: () => {},
|
|
824
|
+
id: "mock-draft-id",
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// Create store
|
|
828
|
+
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
829
|
+
commander.middleware(() => ({
|
|
830
|
+
count: 0,
|
|
831
|
+
items: [],
|
|
832
|
+
selectedId: null,
|
|
833
|
+
mimic: { document: mockDocument },
|
|
834
|
+
}))
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
838
|
+
|
|
839
|
+
// Helper to build context
|
|
840
|
+
const buildCtx = (): CommandContext<TestStore> => ({
|
|
841
|
+
getState: () => storeApi.getState(),
|
|
842
|
+
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
843
|
+
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
844
|
+
return (params: TParams): TReturn => cmd.fn(buildCtx(), params);
|
|
845
|
+
},
|
|
846
|
+
transaction: (fn: (root: any) => void) => {
|
|
847
|
+
const state = storeApi.getState();
|
|
848
|
+
const draft = state._commander.activeDraft;
|
|
849
|
+
if (draft) {
|
|
850
|
+
draft.update(fn);
|
|
851
|
+
} else {
|
|
852
|
+
(state as any).mimic.document.transaction(fn);
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// Command that uses transaction
|
|
858
|
+
const doUpdate = commander.action<{ value: number }>(
|
|
859
|
+
(ctx, params) => {
|
|
860
|
+
ctx.transaction((root: any) => {
|
|
861
|
+
root.count.set(params.value);
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
// Test 1: No draft - should go to document.transaction
|
|
867
|
+
doUpdate.fn(buildCtx(), { value: 1 });
|
|
868
|
+
expect(documentTransactionCalls.length).toBe(1);
|
|
869
|
+
expect(draftUpdateCalls.length).toBe(0);
|
|
870
|
+
|
|
871
|
+
// Test 2: Set active draft
|
|
872
|
+
store.setState((state) => ({
|
|
873
|
+
...state,
|
|
874
|
+
_commander: {
|
|
875
|
+
...state._commander,
|
|
876
|
+
activeDraft: mockDraft as any,
|
|
877
|
+
},
|
|
878
|
+
}));
|
|
879
|
+
|
|
880
|
+
// Test 3: With draft active - should go to draft.update
|
|
881
|
+
doUpdate.fn(buildCtx(), { value: 2 });
|
|
882
|
+
expect(documentTransactionCalls.length).toBe(1); // Still 1 - no new calls
|
|
883
|
+
expect(draftUpdateCalls.length).toBe(1);
|
|
884
|
+
|
|
885
|
+
// Test 4: Multiple updates while draft is active
|
|
886
|
+
doUpdate.fn(buildCtx(), { value: 3 });
|
|
887
|
+
doUpdate.fn(buildCtx(), { value: 4 });
|
|
888
|
+
doUpdate.fn(buildCtx(), { value: 5 });
|
|
889
|
+
|
|
890
|
+
expect(documentTransactionCalls.length).toBe(1); // Still 1 - no new calls
|
|
891
|
+
expect(draftUpdateCalls.length).toBe(4); // 4 draft updates
|
|
892
|
+
|
|
893
|
+
// Test 5: Clear draft
|
|
894
|
+
store.setState((state) => ({
|
|
895
|
+
...state,
|
|
896
|
+
_commander: {
|
|
897
|
+
...state._commander,
|
|
898
|
+
activeDraft: null,
|
|
899
|
+
},
|
|
900
|
+
}));
|
|
901
|
+
|
|
902
|
+
// Test 6: Without draft - should go back to document.transaction
|
|
903
|
+
doUpdate.fn(buildCtx(), { value: 6 });
|
|
904
|
+
expect(documentTransactionCalls.length).toBe(2);
|
|
905
|
+
expect(draftUpdateCalls.length).toBe(4);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it("should route nested command dispatches to draft when active", () => {
|
|
909
|
+
const commander = createCommander<TestState>();
|
|
910
|
+
|
|
911
|
+
const documentTransactionCalls: Array<(root: any) => void> = [];
|
|
912
|
+
const draftUpdateCalls: Array<(root: any) => void> = [];
|
|
913
|
+
|
|
914
|
+
const mockDocument = {
|
|
915
|
+
transaction: (fn: (root: any) => void) => {
|
|
916
|
+
documentTransactionCalls.push(fn);
|
|
917
|
+
},
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
const mockDraft = {
|
|
921
|
+
update: (fn: (root: any) => void) => {
|
|
922
|
+
draftUpdateCalls.push(fn);
|
|
923
|
+
},
|
|
924
|
+
commit: () => {},
|
|
925
|
+
discard: () => {},
|
|
926
|
+
id: "mock-draft-id",
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const store = createStore<TestStore & { mimic: { document: typeof mockDocument } }>(
|
|
930
|
+
commander.middleware(() => ({
|
|
931
|
+
count: 0,
|
|
932
|
+
items: [],
|
|
933
|
+
selectedId: null,
|
|
934
|
+
mimic: { document: mockDocument },
|
|
935
|
+
}))
|
|
936
|
+
);
|
|
937
|
+
|
|
938
|
+
// Set active draft
|
|
939
|
+
store.setState((state) => ({
|
|
940
|
+
...state,
|
|
941
|
+
_commander: {
|
|
942
|
+
...state._commander,
|
|
943
|
+
activeDraft: mockDraft as any,
|
|
944
|
+
},
|
|
945
|
+
}));
|
|
946
|
+
|
|
947
|
+
const storeApi = store as unknown as StoreApi<TestStore & { mimic: { document: typeof mockDocument } }>;
|
|
948
|
+
|
|
949
|
+
const buildCtx = (): CommandContext<TestStore> => ({
|
|
950
|
+
getState: () => storeApi.getState(),
|
|
951
|
+
setState: (partial: Partial<TestStore>) => storeApi.setState(partial as any),
|
|
952
|
+
dispatch: <TParams, TReturn>(cmd: Command<TestStore, TParams, TReturn>) => {
|
|
953
|
+
return (params: TParams): TReturn => cmd.fn(buildCtx(), params);
|
|
954
|
+
},
|
|
955
|
+
transaction: (fn: (root: any) => void) => {
|
|
956
|
+
const state = storeApi.getState();
|
|
957
|
+
const draft = state._commander.activeDraft;
|
|
958
|
+
if (draft) {
|
|
959
|
+
draft.update(fn);
|
|
960
|
+
} else {
|
|
961
|
+
(state as any).mimic.document.transaction(fn);
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// Inner command that uses transaction
|
|
967
|
+
const setCount = commander.action<{ value: number }>(
|
|
968
|
+
(ctx, params) => {
|
|
969
|
+
ctx.transaction((root: any) => {
|
|
970
|
+
root.count.set(params.value);
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
// Outer command that dispatches inner command
|
|
976
|
+
const setMultiple = commander.action<{ values: number[] }>(
|
|
977
|
+
(ctx, params) => {
|
|
978
|
+
for (const value of params.values) {
|
|
979
|
+
ctx.dispatch(setCount)({ value });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
// Execute outer command - all nested transactions should go to draft
|
|
985
|
+
setMultiple.fn(buildCtx(), { values: [1, 2, 3, 4, 5] });
|
|
986
|
+
|
|
987
|
+
expect(draftUpdateCalls.length).toBe(5);
|
|
988
|
+
expect(documentTransactionCalls.length).toBe(0);
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
|
|
671
992
|
describe("Undo/Redo Integration", () => {
|
|
672
993
|
it("should handle multiple undo operations", () => {
|
|
673
994
|
const commander = createCommander<TestState>();
|