From b5d44f9a69138b99107cebddc5088de61c312450 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Wed, 15 Oct 2025 13:30:36 +0200 Subject: [PATCH 01/12] add support for blocked and allowed Command props --- packages/vue/src/experimental/commander.ts | 40 +++++++++++++++++++--- packages/vue/src/makeClient.ts | 2 ++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/vue/src/experimental/commander.ts b/packages/vue/src/experimental/commander.ts index ed020c3de9..f714058515 100644 --- a/packages/vue/src/experimental/commander.ts +++ b/packages/vue/src/experimental/commander.ts @@ -26,6 +26,8 @@ type FnOptions | (() => State) disableSharedWaiting?: boolean + blockKey?: string + waitKey?: string } type FnOptionsInternal = { @@ -143,7 +145,11 @@ export declare namespace Commander { /** reactive */ waiting: boolean /** reactive */ - state: ComputedRef + blocked: boolean + /** reactive */ + allowed: boolean + /** reactive */ + state: State } export interface CommandOut< @@ -1418,6 +1424,8 @@ export class CommanderImpl { Effect.sync(() => makeContext_()) ) ) + const waitId = options?.waitKey ? `${id}.${options.waitKey}` : id + const blockId = options?.blockKey ? `${id}.${options.blockKey}` : id const [result, exec_] = asResult(theHandler) // probably could be nice to use a namespaced, computable wait key instead not unlike query invalidation? @@ -1425,15 +1433,33 @@ export class CommanderImpl { const exec = options?.disableSharedWaiting ? exec_ : Effect - .fnUntraced(function*(...args: [any, any]) { - registerWait(id) - return yield* exec_(...args) - }, Effect.onExit(() => Effect.sync(() => unregisterWait(id)))) + .fnUntraced( + function*(...args: [any, any]) { + registerWait(waitId) + if (blockId !== waitId) { + registerWait(blockId) + } + return yield* exec_(...args) + }, + Effect.onExit(() => + Effect.sync(() => { + unregisterWait(waitId) + if (blockId !== waitId) { + unregisterWait(blockId) + } + }) + ) + ) const waiting = options?.disableSharedWaiting ? computed(() => result.value.waiting) : computed(() => result.value.waiting || (waitState.value[id] ?? 0) > 0) + const blocked = computed(() => waiting.value || (waitState.value[blockId] ?? 0) > 0) + + // TODO: allow to influence e.g via role check + const allowed = true + const handle = Object.assign((arg: Arg) => { // we capture the call site stack here const limit = Error.stackTraceLimit @@ -1503,6 +1529,10 @@ export class CommanderImpl { /** reactive */ waiting, /** reactive */ + blocked, + /** reactive */ + allowed, + /** reactive */ action, /** reactive */ label, diff --git a/packages/vue/src/makeClient.ts b/packages/vue/src/makeClient.ts index ad7df1a148..38bfe8cafe 100644 --- a/packages/vue/src/makeClient.ts +++ b/packages/vue/src/makeClient.ts @@ -1481,6 +1481,8 @@ export type ToCamel = S extends string export interface CommandBase { handle: (input: I) => A waiting: boolean + blocked: boolean + allowed: boolean action: string label: string } From 5febc314b58686feebb0621705816ce38873c537 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Wed, 15 Oct 2025 13:30:51 +0200 Subject: [PATCH 02/12] add CommandButton --- .../src/components/CommandButton.vue | 74 +++++++++++++++++++ .../vue-components/src/components/index.ts | 1 + 2 files changed, 75 insertions(+) create mode 100644 packages/vue-components/src/components/CommandButton.vue diff --git a/packages/vue-components/src/components/CommandButton.vue b/packages/vue-components/src/components/CommandButton.vue new file mode 100644 index 0000000000..1e25edfc4f --- /dev/null +++ b/packages/vue-components/src/components/CommandButton.vue @@ -0,0 +1,74 @@ + + + diff --git a/packages/vue-components/src/components/index.ts b/packages/vue-components/src/components/index.ts index 478b846263..d773b893e6 100644 --- a/packages/vue-components/src/components/index.ts +++ b/packages/vue-components/src/components/index.ts @@ -1,2 +1,3 @@ +export { default as CommandButton } from "./CommandButton.vue" export { default as Dialog } from "./Dialog.vue" export * from "./OmegaForm" From 8e6a4945e9ae047b071a618f5f3d5538508078fa Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Wed, 15 Oct 2025 14:50:09 +0200 Subject: [PATCH 03/12] setup storybook --- packages/vue-components/package.json | 1 + .../src/components/CommandButton.vue | 4 +- .../stories/Commands.stories.ts | 41 ++++++++++++ .../vue-components/stories/Commands/One.vue | 28 +++++++++ .../stories/Commands/components.ts | 1 + .../stories/Commands/helpers.ts | 54 ++++++++++++++++ .../stories/Commands/useEffectRouter.ts | 63 +++++++++++++++++++ packages/vue/src/experimental/commander.ts | 13 +++- pnpm-lock.yaml | 12 ++++ 9 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 packages/vue-components/stories/Commands.stories.ts create mode 100644 packages/vue-components/stories/Commands/One.vue create mode 100644 packages/vue-components/stories/Commands/components.ts create mode 100644 packages/vue-components/stories/Commands/helpers.ts create mode 100644 packages/vue-components/stories/Commands/useEffectRouter.ts diff --git a/packages/vue-components/package.json b/packages/vue-components/package.json index 115c3aa56a..d1e8bf14d5 100644 --- a/packages/vue-components/package.json +++ b/packages/vue-components/package.json @@ -47,6 +47,7 @@ "vitepress": "^1.6.4", "vitest": "^3.2.4", "vue-router": "^4.5.1", + "vue-toastification": "^2.0.0-rc.5", "vue-tsc": "^3.1.0" }, "files": [ diff --git a/packages/vue-components/src/components/CommandButton.vue b/packages/vue-components/src/components/CommandButton.vue index 1e25edfc4f..1b4c2aa635 100644 --- a/packages/vue-components/src/components/CommandButton.vue +++ b/packages/vue-components/src/components/CommandButton.vue @@ -39,7 +39,7 @@ export default { diff --git a/packages/vue/src/experimental/commander.ts b/packages/vue/src/experimental/commander.ts index bdea05549f..9011d0d8f6 100644 --- a/packages/vue/src/experimental/commander.ts +++ b/packages/vue/src/experimental/commander.ts @@ -10,7 +10,7 @@ import { id, type RuntimeFiber } from "effect/Fiber" import { type NoInfer } from "effect/Types" import { isGeneratorFunction, type YieldWrap } from "effect/Utils" import { type FormatXMLElementFn, type PrimitiveType } from "intl-messageformat" -import { computed, type ComputedRef, reactive, ref, watch } from "vue" +import { computed, type ComputedRef, reactive, ref } from "vue" import { Confirm } from "./confirm.js" import { I18n } from "./intl.js" import { WithToast } from "./withToast.js" @@ -25,9 +25,8 @@ type FnOptions | (() => State) - disableSharedWaiting?: boolean - blockKey?: string - waitKey?: string + blockKey?: (id: string) => string | undefined + waitKey?: (id: string) => string | undefined } type FnOptionsInternal = { @@ -1424,56 +1423,38 @@ export class CommanderImpl { Effect.sync(() => makeContext_()) ) ) - const waitId = options?.waitKey ? `${id}.${options.waitKey}` : id - const blockId = options?.blockKey ? `${id}.${options.blockKey}` : id + const waitId = options?.waitKey ? options.waitKey(id) : undefined + const blockId = options?.blockKey ? options.blockKey(id) : undefined const [result, exec_] = asResult(theHandler) // probably could be nice to use a namespaced, computable wait key instead not unlike query invalidation? // ["Something.Update", { id }] for instance - const exec = options?.disableSharedWaiting - ? Effect - .fnUntraced( - function*(...args: [any, any]) { + const exec = Effect + .fnUntraced( + function*(...args: [any, any]) { + if (waitId) registerWait(waitId) + if (blockId && blockId !== waitId) { registerWait(blockId) - return yield* exec_(...args) - }, - Effect.onExit(() => - Effect.sync(() => { + } + return yield* exec_(...args) + }, + Effect.onExit(() => + Effect.sync(() => { + if (waitId) unregisterWait(waitId) + if (blockId && blockId !== waitId) { unregisterWait(blockId) - }) - ) - ) - : Effect - .fnUntraced( - function*(...args: [any, any]) { - registerWait(waitId) - if (blockId !== waitId) { - registerWait(blockId) } - return yield* exec_(...args) - }, - Effect.onExit(() => - Effect.sync(() => { - unregisterWait(waitId) - if (blockId !== waitId) { - unregisterWait(blockId) - } - }) - ) + }) ) + ) - const waiting = options?.disableSharedWaiting - ? computed(() => result.value.waiting) - : computed(() => result.value.waiting || (waitState.value[id] ?? 0) > 0) - - const blocked = computed(() => waiting.value || (waitState.value[blockId] ?? 0) > 0) - watch(waitState, (waitState) => { - console.log(waitState) - }, { deep: true }) + const waiting = waitId + ? computed(() => result.value.waiting || (waitState.value[waitId] ?? 0) > 0) + : computed(() => result.value.waiting) - watch(blocked, (blocked) => { - console.log(blocked) - }, { deep: true }) + const blocked = blockId + ? computed(() => waiting.value || (waitState.value[blockId] ?? 0) > 0) + : computed(() => waiting.value) // TODO: allow to influence e.g via role check const allowed = true From c8be0198db05e3f3abe48e13ada18c97fa83d1c4 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Wed, 15 Oct 2025 15:41:33 +0200 Subject: [PATCH 06/12] noice --- packages/vue-components/stories/Commands/One.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vue-components/stories/Commands/One.vue b/packages/vue-components/stories/Commands/One.vue index 0f6dfc9644..79b958a86f 100644 --- a/packages/vue-components/stories/Commands/One.vue +++ b/packages/vue-components/stories/Commands/One.vue @@ -56,7 +56,7 @@ const removeMutation = Object.assign( const updateName = makeFamily((item: string) => Command.fn(updateMutation, { state: () => ({ item, field: "name" }), - waitKey: () => `update_thing.${item}.name`, + waitKey: (id) => `${id}.${item}.name`, blockKey: () => `modify_thing.${item}` })( function*() { @@ -69,7 +69,7 @@ const updateName = makeFamily((item: string) => const updateName2 = makeFamily((item: string) => Command.fn(updateMutation, { state: () => ({ item, field: "name" }), - waitKey: () => `update_thing.${item}.name`, + waitKey: (id) => `${id}.${item}.name`, blockKey: () => `modify_thing.${item}` })( function*() { @@ -82,7 +82,7 @@ const updateName2 = makeFamily((item: string) => const updateState = makeFamily((item: string) => Command.fn(updateMutation, { state: () => ({ item, field: "state" }), - waitKey: () => `update_thing.${item}.state`, + waitKey: (id) => `${id}.${item}.state`, blockKey: () => `modify_thing.${item}` })( function*() { From 271c2202d3c601eacc9d4cbe7d5f4790602cdcf3 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Wed, 15 Oct 2025 17:26:31 +0200 Subject: [PATCH 07/12] const id --- packages/vue/src/experimental/commander.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/vue/src/experimental/commander.ts b/packages/vue/src/experimental/commander.ts index 9011d0d8f6..3a49fc4028 100644 --- a/packages/vue/src/experimental/commander.ts +++ b/packages/vue/src/experimental/commander.ts @@ -16,7 +16,7 @@ import { I18n } from "./intl.js" import { WithToast } from "./withToast.js" type IntlRecord = Record> -type FnOptions = { +type FnOptions = { i18nCustomKey?: I18nCustomKey /** * passed to the i18n formatMessage calls so you can use it in translation messagee @@ -25,8 +25,8 @@ type FnOptions | (() => State) - blockKey?: (id: string) => string | undefined - waitKey?: (id: string) => string | undefined + blockKey?: (id: Id) => string | undefined + waitKey?: (id: Id) => string | undefined } type FnOptionsInternal = { @@ -1287,8 +1287,8 @@ const unregisterWait = (id: string) => { } } -const getStateValues = ( - options?: FnOptions +const getStateValues = ( + options?: FnOptions ): ComputedRef => { const state_ = options?.state const state = !state_ ? computed(() => undefined as State) : typeof state_ === "function" @@ -1344,7 +1344,7 @@ export class CommanderImpl { const I18nKey extends string = Id >( id_: Id | { id: Id }, - options?: FnOptions, + options?: FnOptions, errorDef?: Error ) => { const id = typeof id_ === "string" ? id_ : id_.id @@ -1629,7 +1629,7 @@ export class CommanderImpl { const I18nKey extends string = Id >( id: Id | { id: Id }, - options?: FnOptions + options?: FnOptions ): Commander.Gen & Commander.NonGen & { state: Context.Tag<`Commander.Command.${Id}.state`, State> } => @@ -1676,7 +1676,7 @@ export class CommanderImpl { | Id | { id: Id; mutate: (arg: MutArg) => Effect.Effect } | ((arg: MutArg) => Effect.Effect) & { id: Id }, - options?: FnOptions + options?: FnOptions ) => & Commander.CommandContextLocal & (( @@ -1767,7 +1767,7 @@ export class CommanderImpl { mutation: | { mutate: (arg: Arg) => Effect.Effect; id: Id } | ((arg: Arg) => Effect.Effect) & { id: Id }, - options?: FnOptions + options?: FnOptions ): Commander.CommanderWrap => Object.assign( ( From 0264cdc595c307f54126fb12a9b1b2005ee8762c Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Thu, 16 Oct 2025 08:52:05 +0200 Subject: [PATCH 08/12] add `allowed` option --- packages/vue/src/experimental/commander.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/vue/src/experimental/commander.ts b/packages/vue/src/experimental/commander.ts index 3a49fc4028..4ba4c91bfb 100644 --- a/packages/vue/src/experimental/commander.ts +++ b/packages/vue/src/experimental/commander.ts @@ -27,6 +27,7 @@ type FnOptions | (() => State) blockKey?: (id: Id) => string | undefined waitKey?: (id: Id) => string | undefined + allowed?: (id: Id, state: ComputedRef) => boolean } type FnOptionsInternal = { @@ -1427,37 +1428,36 @@ export class CommanderImpl { const blockId = options?.blockKey ? options.blockKey(id) : undefined const [result, exec_] = asResult(theHandler) - // probably could be nice to use a namespaced, computable wait key instead not unlike query invalidation? - // ["Something.Update", { id }] for instance + const exec = Effect .fnUntraced( function*(...args: [any, any]) { - if (waitId) registerWait(waitId) - if (blockId && blockId !== waitId) { + if (waitId !== undefined) registerWait(waitId) + if (blockId !== undefined && blockId !== waitId) { registerWait(blockId) } return yield* exec_(...args) }, Effect.onExit(() => Effect.sync(() => { - if (waitId) unregisterWait(waitId) - if (blockId && blockId !== waitId) { + if (waitId !== undefined) unregisterWait(waitId) + if (blockId !== undefined && blockId !== waitId) { unregisterWait(blockId) } }) ) ) - const waiting = waitId + const waiting = waitId !== undefined ? computed(() => result.value.waiting || (waitState.value[waitId] ?? 0) > 0) : computed(() => result.value.waiting) - const blocked = blockId + const blocked = blockId !== undefined ? computed(() => waiting.value || (waitState.value[blockId] ?? 0) > 0) : computed(() => waiting.value) - // TODO: allow to influence e.g via role check - const allowed = true + const computeAllowed = options?.allowed + const allowed = computeAllowed ? computed(() => computeAllowed(id, state)) : true const handle = Object.assign((arg: Arg) => { // we capture the call site stack here From 0b8ceecb4e720da3f56a7f1176070faa3b66d610 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Thu, 16 Oct 2025 08:55:43 +0200 Subject: [PATCH 09/12] add role demo --- packages/vue-components/stories/Commands/One.vue | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/vue-components/stories/Commands/One.vue b/packages/vue-components/stories/Commands/One.vue index 79b958a86f..941501d14c 100644 --- a/packages/vue-components/stories/Commands/One.vue +++ b/packages/vue-components/stories/Commands/One.vue @@ -1,4 +1,7 @@