@softize/opus 8.6.6
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/CHANGELOG.md +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/ui/react — React driver
|
|
3
|
+
*
|
|
4
|
+
* Hooks + Provider pra invocar actions client-side.
|
|
5
|
+
* - `TbdlibProvider` — injeta ClientAdapter no contexto
|
|
6
|
+
* - `useAction(action)` — invoca actions simple/form/view
|
|
7
|
+
* - `useLookupAction(action)` — variante pra search (output Paginated)
|
|
8
|
+
*
|
|
9
|
+
* Compatível com React 18 e 19. Não dona estado global de cache —
|
|
10
|
+
* cada hook mantém o seu (suficiente pra v0). Integração com
|
|
11
|
+
* TanStack Query fica em `@softize/opus/client/tanstack` no futuro.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createContext,
|
|
16
|
+
useCallback,
|
|
17
|
+
useContext,
|
|
18
|
+
useMemo,
|
|
19
|
+
useState,
|
|
20
|
+
type ReactNode,
|
|
21
|
+
} from 'react'
|
|
22
|
+
import {
|
|
23
|
+
useForm,
|
|
24
|
+
type DefaultValues,
|
|
25
|
+
type FieldValues,
|
|
26
|
+
type UseFormReturn,
|
|
27
|
+
} from 'react-hook-form'
|
|
28
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
29
|
+
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
30
|
+
import type { ZodType } from 'zod'
|
|
31
|
+
import type {
|
|
32
|
+
ActionContract,
|
|
33
|
+
ActionDef,
|
|
34
|
+
ActionError,
|
|
35
|
+
ClientAdapter,
|
|
36
|
+
FormContract,
|
|
37
|
+
Paginated,
|
|
38
|
+
} from '../../core/index.ts'
|
|
39
|
+
import {
|
|
40
|
+
type ActionState,
|
|
41
|
+
type ListState,
|
|
42
|
+
idleAction,
|
|
43
|
+
idleLookup,
|
|
44
|
+
} from '../index.ts'
|
|
45
|
+
import { zodErrorMapPtBr } from '../lib/zod-pt-br.ts'
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Provider
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Dicionário estrutural pro resolvedor de `options: { kind: 'dictionary', ref }`:
|
|
53
|
+
* qualquer coisa com `options()` → `{ value, label }`. O `DictType` do `t.dict`
|
|
54
|
+
* satisfaz direto — sem este driver importar o driver de schema.
|
|
55
|
+
*/
|
|
56
|
+
export interface DictLike {
|
|
57
|
+
options(): Array<{ value: string; label: string }>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TbdlibContextValue {
|
|
61
|
+
client: ClientAdapter
|
|
62
|
+
dicts: Record<string, DictLike>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const TbdlibContext = createContext<TbdlibContextValue | null>(null)
|
|
66
|
+
|
|
67
|
+
export interface TbdlibProviderProps {
|
|
68
|
+
client: ClientAdapter
|
|
69
|
+
/** Dicionários por ref — resolvem `options: { kind: 'dictionary', ref }` de
|
|
70
|
+
* filters/fields (ActionList/ActionForm). Campo de dict sem registro aqui ainda
|
|
71
|
+
* resolve pelo fallback (a meta do `t.dict` viaja no schema do contrato). */
|
|
72
|
+
dicts?: Record<string, DictLike>
|
|
73
|
+
children: ReactNode
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function TbdlibProvider({
|
|
77
|
+
client,
|
|
78
|
+
dicts,
|
|
79
|
+
children,
|
|
80
|
+
}: TbdlibProviderProps): ReactNode {
|
|
81
|
+
const value = useMemo<TbdlibContextValue>(() => ({ client, dicts: dicts ?? {} }), [client, dicts])
|
|
82
|
+
return <TbdlibContext.Provider value={value}>{children}</TbdlibContext.Provider>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function useClient(): ClientAdapter {
|
|
86
|
+
const ctx = useContext(TbdlibContext)
|
|
87
|
+
if (ctx === null) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'useAction/useLookupAction must be used inside <TbdlibProvider>',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return ctx.client
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const NO_DICTS: Record<string, DictLike> = {}
|
|
96
|
+
|
|
97
|
+
/** Dicts do provider. Default {} — inclusive fora do provider: o resolvedor cai
|
|
98
|
+
* nos fallbacks (estáticas do spec, meta do `t.dict`) sem exigir registry. */
|
|
99
|
+
export function useDicts(): Record<string, DictLike> {
|
|
100
|
+
return useContext(TbdlibContext)?.dicts ?? NO_DICTS
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// useAction
|
|
105
|
+
// =============================================================================
|
|
106
|
+
|
|
107
|
+
export interface UseActionResult<TInput, TData> extends ActionState<TData> {
|
|
108
|
+
run: (input: TInput) => Promise<ActionState<TData>>
|
|
109
|
+
reset: () => void
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function useAction<TInput = unknown, TData = unknown>(
|
|
113
|
+
action: ActionDef,
|
|
114
|
+
): UseActionResult<TInput, TData> {
|
|
115
|
+
const client = useClient()
|
|
116
|
+
const [state, setState] = useState<ActionState<TData>>(() => idleAction<TData>())
|
|
117
|
+
|
|
118
|
+
const run = useCallback(
|
|
119
|
+
async (input: TInput): Promise<ActionState<TData>> => {
|
|
120
|
+
setState({
|
|
121
|
+
data: undefined,
|
|
122
|
+
error: undefined,
|
|
123
|
+
isIdle: false,
|
|
124
|
+
isLoading: true,
|
|
125
|
+
isSuccess: false,
|
|
126
|
+
isError: false,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const result = await client.run<TData>(action, input)
|
|
130
|
+
|
|
131
|
+
const next: ActionState<TData> = result.ok
|
|
132
|
+
? {
|
|
133
|
+
data: result.data,
|
|
134
|
+
error: undefined,
|
|
135
|
+
isIdle: false,
|
|
136
|
+
isLoading: false,
|
|
137
|
+
isSuccess: true,
|
|
138
|
+
isError: false,
|
|
139
|
+
}
|
|
140
|
+
: {
|
|
141
|
+
data: undefined,
|
|
142
|
+
error: result.error,
|
|
143
|
+
isIdle: false,
|
|
144
|
+
isLoading: false,
|
|
145
|
+
isSuccess: false,
|
|
146
|
+
isError: true,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setState(next)
|
|
150
|
+
return next
|
|
151
|
+
},
|
|
152
|
+
[client, action],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const reset = useCallback(() => {
|
|
156
|
+
setState(idleAction<TData>())
|
|
157
|
+
}, [])
|
|
158
|
+
|
|
159
|
+
return { ...state, run, reset }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =============================================================================
|
|
163
|
+
// useLookupAction
|
|
164
|
+
// =============================================================================
|
|
165
|
+
|
|
166
|
+
export interface UseLookupActionResult<TInput, TItem>
|
|
167
|
+
extends ListState<TItem> {
|
|
168
|
+
run: (input: TInput) => Promise<ListState<TItem>>
|
|
169
|
+
reset: () => void
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function useLookupAction<TInput = unknown, TItem = unknown>(
|
|
173
|
+
action: ActionDef,
|
|
174
|
+
): UseLookupActionResult<TInput, TItem> {
|
|
175
|
+
const client = useClient()
|
|
176
|
+
const [state, setState] = useState<ListState<TItem>>(() => idleLookup<TItem>())
|
|
177
|
+
|
|
178
|
+
const run = useCallback(
|
|
179
|
+
async (input: TInput): Promise<ListState<TItem>> => {
|
|
180
|
+
setState((prev) => ({
|
|
181
|
+
...prev,
|
|
182
|
+
error: undefined,
|
|
183
|
+
isIdle: false,
|
|
184
|
+
isLoading: true,
|
|
185
|
+
isSuccess: false,
|
|
186
|
+
isError: false,
|
|
187
|
+
}))
|
|
188
|
+
|
|
189
|
+
const result = await client.run<Paginated<TItem>>(action, input)
|
|
190
|
+
|
|
191
|
+
const next: ListState<TItem> = result.ok
|
|
192
|
+
? {
|
|
193
|
+
items: result.data.items,
|
|
194
|
+
cursor: result.data.cursor,
|
|
195
|
+
total: result.data.total,
|
|
196
|
+
error: undefined,
|
|
197
|
+
isIdle: false,
|
|
198
|
+
isLoading: false,
|
|
199
|
+
isSuccess: true,
|
|
200
|
+
isError: false,
|
|
201
|
+
}
|
|
202
|
+
: {
|
|
203
|
+
items: [],
|
|
204
|
+
cursor: { next: null },
|
|
205
|
+
total: undefined,
|
|
206
|
+
error: result.error,
|
|
207
|
+
isIdle: false,
|
|
208
|
+
isLoading: false,
|
|
209
|
+
isSuccess: false,
|
|
210
|
+
isError: true,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
setState(next)
|
|
214
|
+
return next
|
|
215
|
+
},
|
|
216
|
+
[client, action],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const reset = useCallback(() => {
|
|
220
|
+
setState(idleLookup<TItem>())
|
|
221
|
+
}, [])
|
|
222
|
+
|
|
223
|
+
return { ...state, run, reset }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// =============================================================================
|
|
227
|
+
// useListAction
|
|
228
|
+
// =============================================================================
|
|
229
|
+
//
|
|
230
|
+
// Lista QUERY-BACKED de uma search action: busca no mount e cacheia em
|
|
231
|
+
// queryKey [action.name, input]. É o par do `action.invalidates` — quando um
|
|
232
|
+
// form/trigger invalida 'x.list', toda lista montada com este hook refaz
|
|
233
|
+
// sozinha (invalidateQueries casa por prefixo do queryKey).
|
|
234
|
+
//
|
|
235
|
+
// Difere do useLookupAction (imperativo, run() sob demanda, pro ActionList):
|
|
236
|
+
// aqui o fetch é declarativo — montou, buscou. Pre-requisito: <QueryClientProvider>.
|
|
237
|
+
|
|
238
|
+
export interface UseListActionResult<TItem> {
|
|
239
|
+
items: TItem[]
|
|
240
|
+
total: number | undefined
|
|
241
|
+
error: ActionError | undefined
|
|
242
|
+
/** Primeiro load (sem dado nenhum ainda) — o skeleton. */
|
|
243
|
+
isLoading: boolean
|
|
244
|
+
/** Qualquer busca em andamento (inclui refetch com dados na tela). */
|
|
245
|
+
isFetching: boolean
|
|
246
|
+
isSuccess: boolean
|
|
247
|
+
isError: boolean
|
|
248
|
+
/** Força re-fetch (além da invalidação declarativa via action.invalidates). */
|
|
249
|
+
refetch: () => Promise<void>
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// O param é estrutural (name + kind) de propósito: ListContract é invariante no
|
|
253
|
+
// input, o que quebraria a chamada `useListAction<Row>(contract)` quando o contrato
|
|
254
|
+
// tem filtros opcionais. O hook só usa name/kind; o item é tipado pelo caller.
|
|
255
|
+
export function useListAction<TItem = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
|
|
256
|
+
action: { name: string; kind: 'list' },
|
|
257
|
+
input?: TInput,
|
|
258
|
+
): UseListActionResult<TItem> {
|
|
259
|
+
const client = useClient()
|
|
260
|
+
|
|
261
|
+
const query = useQuery<Paginated<TItem>, ActionError>({
|
|
262
|
+
queryKey: [action.name, input ?? {}],
|
|
263
|
+
queryFn: async () => {
|
|
264
|
+
const result = await client.run<Paginated<TItem>>(action as unknown as ActionDef, input ?? {})
|
|
265
|
+
if (!result.ok) throw result.error
|
|
266
|
+
return result.data
|
|
267
|
+
},
|
|
268
|
+
// Input mudou (filtro/página) → mantém os itens na tela enquanto busca: o
|
|
269
|
+
// ActionList esmaece em vez de piscar skeleton. isPending = só o 1º load.
|
|
270
|
+
placeholderData: keepPreviousData,
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
const refetch = useCallback(async () => {
|
|
274
|
+
await query.refetch()
|
|
275
|
+
}, [query])
|
|
276
|
+
|
|
277
|
+
const isLoading = query.isPending
|
|
278
|
+
return {
|
|
279
|
+
items: query.data?.items ?? [],
|
|
280
|
+
total: query.data?.total,
|
|
281
|
+
error: query.error ?? undefined,
|
|
282
|
+
isLoading,
|
|
283
|
+
// Busca em andamento COM dados na tela (troca de recorte, invalidates, refetch).
|
|
284
|
+
isFetching: query.isFetching,
|
|
285
|
+
isSuccess: query.isSuccess,
|
|
286
|
+
isError: query.isError,
|
|
287
|
+
refetch,
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// useFormAction
|
|
293
|
+
// =============================================================================
|
|
294
|
+
//
|
|
295
|
+
// Pre-requisito: app está envolvido em `<QueryClientProvider>` do TanStack
|
|
296
|
+
// Query. Sem ele, useFormAction lança no primeiro render.
|
|
297
|
+
//
|
|
298
|
+
// Usa react-hook-form internamente. Se `action.input` é um Zod schema,
|
|
299
|
+
// validation client-side roda automaticamente via zodResolver. Submit
|
|
300
|
+
// invoca `client.run(action, input)`; em sucesso, invalida queryKeys
|
|
301
|
+
// derivadas de `action.invalidates` automaticamente.
|
|
302
|
+
|
|
303
|
+
export interface UseFormActionOptions<TInput extends FieldValues, TData> {
|
|
304
|
+
/** Valores iniciais do form. Mesmo shape do input da action. */
|
|
305
|
+
defaultValues?: DefaultValues<TInput>
|
|
306
|
+
/** Callback chamado ao sucesso. Cache invalidation acontece antes do callback. */
|
|
307
|
+
onSuccess?: (data: TData) => void | Promise<void>
|
|
308
|
+
/** Callback chamado em erro (validation client-side passou mas servidor rejeitou). */
|
|
309
|
+
onError?: (error: ActionError) => void
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface UseFormActionResult<TInput extends FieldValues, TData>
|
|
313
|
+
extends ActionState<TData> {
|
|
314
|
+
/** RHF form methods — passe `{...form.formMethods}` no `<Form>` do shadcn. */
|
|
315
|
+
form: UseFormReturn<TInput>
|
|
316
|
+
/** Handler pronto pra usar em `<form onSubmit={submit}>`. */
|
|
317
|
+
submit: (event?: React.BaseSyntheticEvent) => Promise<void>
|
|
318
|
+
/** Reseta form + estado de execução. */
|
|
319
|
+
reset: () => void
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Normaliza strings vazias do form conforme o schema — reproduz o `trim() || null`
|
|
324
|
+
* dos forms à mão: campo NULLABLE → null (limpa no servidor); só OPTIONAL → undefined
|
|
325
|
+
* (omite, não mexe); obrigatório → mantém '' (o zod acusa). Anda pelos wrappers via
|
|
326
|
+
* _def.typeName (string, sem instanceof — atravessa zod v3/v4; se não reconhecer,
|
|
327
|
+
* passa reto, fail-open).
|
|
328
|
+
*/
|
|
329
|
+
function normalizeEmptyStrings(schema: unknown, values: Record<string, unknown>): Record<string, unknown> {
|
|
330
|
+
const shape = (schema as { shape?: Record<string, unknown> } | null)?.shape
|
|
331
|
+
if (shape === undefined || shape === null) return values
|
|
332
|
+
const out: Record<string, unknown> = { ...values }
|
|
333
|
+
for (const [key, value] of Object.entries(out)) {
|
|
334
|
+
if (value !== '') continue
|
|
335
|
+
let node = shape[key] as { _def?: { typeName?: string; innerType?: unknown } } | undefined
|
|
336
|
+
let nullable = false
|
|
337
|
+
let optional = false
|
|
338
|
+
while (
|
|
339
|
+
node?._def?.typeName === 'ZodOptional' ||
|
|
340
|
+
node?._def?.typeName === 'ZodNullable' ||
|
|
341
|
+
node?._def?.typeName === 'ZodDefault'
|
|
342
|
+
) {
|
|
343
|
+
if (node._def.typeName === 'ZodNullable') nullable = true
|
|
344
|
+
else optional = true
|
|
345
|
+
node = node._def.innerType as typeof node
|
|
346
|
+
}
|
|
347
|
+
if (nullable) out[key] = null
|
|
348
|
+
else if (optional) delete out[key]
|
|
349
|
+
}
|
|
350
|
+
return out
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function useFormAction<
|
|
354
|
+
TInput extends FieldValues = FieldValues,
|
|
355
|
+
TData = unknown,
|
|
356
|
+
>(
|
|
357
|
+
action: FormContract<TInput, TData>,
|
|
358
|
+
options: UseFormActionOptions<TInput, TData> = {},
|
|
359
|
+
): UseFormActionResult<TInput, TData> {
|
|
360
|
+
const client = useClient()
|
|
361
|
+
const queryClient = useQueryClient()
|
|
362
|
+
|
|
363
|
+
const resolver = useMemo(() => {
|
|
364
|
+
// action.input é Schema<TInput>. Se for Zod, embrulhamos com zodResolver.
|
|
365
|
+
// Caso seja outra implementação de StandardSchemaV1, fica sem resolver
|
|
366
|
+
// (servidor ainda valida no fail-closed pipeline).
|
|
367
|
+
const schema = action.input as unknown as ZodType<TInput>
|
|
368
|
+
if (
|
|
369
|
+
schema !== null &&
|
|
370
|
+
typeof schema === 'object' &&
|
|
371
|
+
'_def' in schema &&
|
|
372
|
+
typeof (schema as { parse?: unknown }).parse === 'function'
|
|
373
|
+
) {
|
|
374
|
+
// Cast version-agnostic: há zod v3 e v4 no monorepo e qual o Opus resolve varia
|
|
375
|
+
// por install (passava local, quebrava na VPS). O guard de runtime acima já
|
|
376
|
+
// garante que é um schema Zod; o tipo exato não importa pro zodResolver.
|
|
377
|
+
// errorMap = dicionário pt-BR (mensagem específica no schema tem precedência).
|
|
378
|
+
return zodResolver(
|
|
379
|
+
schema as unknown as Parameters<typeof zodResolver>[0],
|
|
380
|
+
{ errorMap: zodErrorMapPtBr } as never,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
return undefined
|
|
384
|
+
}, [action.input])
|
|
385
|
+
|
|
386
|
+
const form = useForm<TInput>({
|
|
387
|
+
...(resolver !== undefined ? { resolver } : {}),
|
|
388
|
+
...(options.defaultValues !== undefined
|
|
389
|
+
? { defaultValues: options.defaultValues }
|
|
390
|
+
: {}),
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
const [state, setState] = useState<ActionState<TData>>(() =>
|
|
394
|
+
idleAction<TData>(),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
const submit = useMemo(
|
|
398
|
+
() =>
|
|
399
|
+
form.handleSubmit(async (input: TInput) => {
|
|
400
|
+
setState({
|
|
401
|
+
data: undefined,
|
|
402
|
+
error: undefined,
|
|
403
|
+
isIdle: false,
|
|
404
|
+
isLoading: true,
|
|
405
|
+
isSuccess: false,
|
|
406
|
+
isError: false,
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// O ClientAdapter tipa `run` com ActionDef (action ligado, com handler), mas o
|
|
410
|
+
// cliente só tem o FormContract — e em runtime o adapter só usa `action.name`.
|
|
411
|
+
const payload = normalizeEmptyStrings(action.input, input as Record<string, unknown>) as TInput
|
|
412
|
+
const result = await client.run<TData>(action as unknown as ActionDef, payload)
|
|
413
|
+
|
|
414
|
+
if (result.ok) {
|
|
415
|
+
// Invalidação cache declarativa via action.invalidates
|
|
416
|
+
const invalidates =
|
|
417
|
+
typeof action.invalidates === 'function'
|
|
418
|
+
? action.invalidates(input)
|
|
419
|
+
: action.invalidates
|
|
420
|
+
if (invalidates !== undefined && invalidates.length > 0) {
|
|
421
|
+
await Promise.all(
|
|
422
|
+
invalidates.map((key) =>
|
|
423
|
+
queryClient.invalidateQueries({ queryKey: [key] }),
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const next: ActionState<TData> = {
|
|
429
|
+
data: result.data,
|
|
430
|
+
error: undefined,
|
|
431
|
+
isIdle: false,
|
|
432
|
+
isLoading: false,
|
|
433
|
+
isSuccess: true,
|
|
434
|
+
isError: false,
|
|
435
|
+
}
|
|
436
|
+
setState(next)
|
|
437
|
+
await options.onSuccess?.(result.data)
|
|
438
|
+
} else {
|
|
439
|
+
const next: ActionState<TData> = {
|
|
440
|
+
data: undefined,
|
|
441
|
+
error: result.error,
|
|
442
|
+
isIdle: false,
|
|
443
|
+
isLoading: false,
|
|
444
|
+
isSuccess: false,
|
|
445
|
+
isError: true,
|
|
446
|
+
}
|
|
447
|
+
setState(next)
|
|
448
|
+
options.onError?.(result.error)
|
|
449
|
+
}
|
|
450
|
+
}),
|
|
451
|
+
[form, client, action, queryClient, options],
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
const reset = useCallback(() => {
|
|
455
|
+
form.reset()
|
|
456
|
+
setState(idleAction<TData>())
|
|
457
|
+
}, [form])
|
|
458
|
+
|
|
459
|
+
return { ...state, form, submit, reset }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// =============================================================================
|
|
463
|
+
// useViewAction
|
|
464
|
+
// =============================================================================
|
|
465
|
+
//
|
|
466
|
+
// Auto-roda action.run(input) no mount + quando input muda. Pensado pra
|
|
467
|
+
// view actions (kind=view) que carregam um recurso por id, mas funciona
|
|
468
|
+
// pra qualquer action determinística por input.
|
|
469
|
+
//
|
|
470
|
+
// Pre-requisito: app está envolvido em `<QueryClientProvider>` — cache é
|
|
471
|
+
// armazenado em queryKey [action.name, input]. Múltiplos componentes que
|
|
472
|
+
// usam mesmo action+input compartilham o cache automaticamente.
|
|
473
|
+
|
|
474
|
+
export interface UseViewActionResult<TData> extends ActionState<TData> {
|
|
475
|
+
/** Força re-fetch (invalida o cache desta queryKey + refetch). */
|
|
476
|
+
refetch: () => Promise<void>
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// O param é estrutural (name + kind) de propósito, como no useListAction: no
|
|
480
|
+
// client só existe o CONTRATO (sem handler) — exigir ViewAction barraria todo
|
|
481
|
+
// callsite `useViewAction(contract, input)`. O hook só usa name/kind.
|
|
482
|
+
export function useViewAction<TInput = unknown, TData = unknown>(
|
|
483
|
+
action: { name: string; kind: 'view' },
|
|
484
|
+
input: TInput,
|
|
485
|
+
): UseViewActionResult<TData> {
|
|
486
|
+
const client = useClient()
|
|
487
|
+
|
|
488
|
+
// TanStack Query serializa queryKey → dedupe automático mesmo com input
|
|
489
|
+
// inline (referência nova a cada render). Sem isso entraria em loop.
|
|
490
|
+
const query = useQuery<TData, ActionError>({
|
|
491
|
+
queryKey: [action.name, input],
|
|
492
|
+
queryFn: async () => {
|
|
493
|
+
const result = await client.run<TData>(action as unknown as ActionDef, input)
|
|
494
|
+
if (!result.ok) {
|
|
495
|
+
throw result.error
|
|
496
|
+
}
|
|
497
|
+
return result.data
|
|
498
|
+
},
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
const refetch = useCallback(async () => {
|
|
502
|
+
await query.refetch()
|
|
503
|
+
}, [query])
|
|
504
|
+
|
|
505
|
+
// Mapeia 4 estados do TanStack Query (pending/success/error/idle) pros
|
|
506
|
+
// 5 flags do ActionState. Pending pré-data = loading; sem fetch ainda = idle.
|
|
507
|
+
const isLoading = query.isPending || query.isFetching
|
|
508
|
+
const isSuccess = query.isSuccess && !isLoading
|
|
509
|
+
const isError = query.isError
|
|
510
|
+
const isIdle = !isLoading && !isSuccess && !isError
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
data: query.data,
|
|
514
|
+
error: query.error ?? undefined,
|
|
515
|
+
isIdle,
|
|
516
|
+
isLoading,
|
|
517
|
+
isSuccess,
|
|
518
|
+
isError,
|
|
519
|
+
refetch,
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// =============================================================================
|
|
524
|
+
// useTriggerAction
|
|
525
|
+
// =============================================================================
|
|
526
|
+
//
|
|
527
|
+
// Dispara QUALQUER contrato via `trigger(input)` — o caso típico é simple
|
|
528
|
+
// (kind=simple), mas um FormContract submetido fora de form (modal composto)
|
|
529
|
+
// roda igual: o hook só usa name/kind e lê `action.invalidates`.
|
|
530
|
+
//
|
|
531
|
+
// Diferença pra useAction (genérico):
|
|
532
|
+
// - Auto-invalidação via action.invalidates
|
|
533
|
+
// - Callbacks declarativos (onSuccess/onError)
|
|
534
|
+
// - Foco em mutation sem form
|
|
535
|
+
//
|
|
536
|
+
// Pre-requisito: <QueryClientProvider> no app.
|
|
537
|
+
|
|
538
|
+
export interface UseTriggerActionOptions<TInput, TData> {
|
|
539
|
+
onSuccess?: (data: TData, input: TInput) => void | Promise<void>
|
|
540
|
+
onError?: (error: ActionError, input: TInput) => void
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export interface UseTriggerActionResult<TInput, TData>
|
|
544
|
+
extends ActionState<TData> {
|
|
545
|
+
trigger: (input: TInput) => Promise<ActionState<TData>>
|
|
546
|
+
reset: () => void
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function useTriggerAction<TInput = unknown, TData = unknown>(
|
|
550
|
+
action: ActionContract<TInput, TData>,
|
|
551
|
+
options: UseTriggerActionOptions<TInput, TData> = {},
|
|
552
|
+
): UseTriggerActionResult<TInput, TData> {
|
|
553
|
+
const client = useClient()
|
|
554
|
+
const queryClient = useQueryClient()
|
|
555
|
+
const [state, setState] = useState<ActionState<TData>>(() =>
|
|
556
|
+
idleAction<TData>(),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
const trigger = useCallback(
|
|
560
|
+
async (input: TInput): Promise<ActionState<TData>> => {
|
|
561
|
+
setState({
|
|
562
|
+
data: undefined,
|
|
563
|
+
error: undefined,
|
|
564
|
+
isIdle: false,
|
|
565
|
+
isLoading: true,
|
|
566
|
+
isSuccess: false,
|
|
567
|
+
isError: false,
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
// Contrato (sem handler) no cliente; o adapter só usa `action.name`/`kind`.
|
|
571
|
+
const result = await client.run<TData>(action as unknown as ActionDef, input)
|
|
572
|
+
|
|
573
|
+
if (result.ok) {
|
|
574
|
+
// Cast: o braço FormContract do union restringe o input a `& Record<...>`,
|
|
575
|
+
// mas o TInput do chamador JÁ é o In do contrato passado.
|
|
576
|
+
const invalidates =
|
|
577
|
+
typeof action.invalidates === 'function'
|
|
578
|
+
? (action.invalidates as (input: TInput) => string[])(input)
|
|
579
|
+
: action.invalidates
|
|
580
|
+
if (invalidates !== undefined && invalidates.length > 0) {
|
|
581
|
+
await Promise.all(
|
|
582
|
+
invalidates.map((key) =>
|
|
583
|
+
queryClient.invalidateQueries({ queryKey: [key] }),
|
|
584
|
+
),
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const next: ActionState<TData> = {
|
|
589
|
+
data: result.data,
|
|
590
|
+
error: undefined,
|
|
591
|
+
isIdle: false,
|
|
592
|
+
isLoading: false,
|
|
593
|
+
isSuccess: true,
|
|
594
|
+
isError: false,
|
|
595
|
+
}
|
|
596
|
+
setState(next)
|
|
597
|
+
await options.onSuccess?.(result.data, input)
|
|
598
|
+
return next
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const next: ActionState<TData> = {
|
|
602
|
+
data: undefined,
|
|
603
|
+
error: result.error,
|
|
604
|
+
isIdle: false,
|
|
605
|
+
isLoading: false,
|
|
606
|
+
isSuccess: false,
|
|
607
|
+
isError: true,
|
|
608
|
+
}
|
|
609
|
+
setState(next)
|
|
610
|
+
options.onError?.(result.error, input)
|
|
611
|
+
return next
|
|
612
|
+
},
|
|
613
|
+
[client, action, queryClient, options],
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
const reset = useCallback(() => {
|
|
617
|
+
setState(idleAction<TData>())
|
|
618
|
+
}, [])
|
|
619
|
+
|
|
620
|
+
return { ...state, trigger, reset }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// =============================================================================
|
|
624
|
+
// Re-exports
|
|
625
|
+
// =============================================================================
|
|
626
|
+
|
|
627
|
+
export type { ActionError, ActionState, ListState }
|