@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.4

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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.mts +1378 -94
  3. package/dist/index.d.ts +1378 -94
  4. package/dist/index.js +1094 -1309
  5. package/dist/index.mjs +1038 -1296
  6. package/package.json +4 -3
  7. package/package.json.backup +0 -41
  8. package/src/Blueprint.ts +0 -216
  9. package/src/__tests__/Blueprint.test.ts +0 -106
  10. package/src/__tests__/action-context.test.ts +0 -166
  11. package/src/__tests__/actionCreators.test.ts +0 -179
  12. package/src/__tests__/builders.test.ts +0 -336
  13. package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
  14. package/src/__tests__/factories.test.ts +0 -229
  15. package/src/__tests__/loader.test.ts +0 -159
  16. package/src/__tests__/logger.test.ts +0 -70
  17. package/src/__tests__/type-inference.test.ts +0 -160
  18. package/src/__tests__/typed-transitions.test.ts +0 -126
  19. package/src/__tests__/useModuleConfig.test.ts +0 -61
  20. package/src/actionCreators.ts +0 -132
  21. package/src/actions.ts +0 -547
  22. package/src/atoms/index.ts +0 -600
  23. package/src/authoring.ts +0 -92
  24. package/src/browser-player.ts +0 -783
  25. package/src/builders.ts +0 -1342
  26. package/src/components/ExperienceWorkflowBridge.tsx +0 -123
  27. package/src/components/PlayerProvider.tsx +0 -43
  28. package/src/components/atoms/index.tsx +0 -269
  29. package/src/components/index.ts +0 -36
  30. package/src/conditions.ts +0 -692
  31. package/src/config/defineBlueprint.ts +0 -329
  32. package/src/config/defineModel.ts +0 -753
  33. package/src/config/defineWorkspace.ts +0 -24
  34. package/src/core/WorkflowRuntime.ts +0 -153
  35. package/src/factories.ts +0 -425
  36. package/src/grammar/index.ts +0 -173
  37. package/src/hooks/index.ts +0 -106
  38. package/src/hooks/useAuth.ts +0 -288
  39. package/src/hooks/useChannel.ts +0 -304
  40. package/src/hooks/useComputed.ts +0 -154
  41. package/src/hooks/useDomainSubscription.ts +0 -110
  42. package/src/hooks/useDuringAction.ts +0 -99
  43. package/src/hooks/useExperienceState.ts +0 -59
  44. package/src/hooks/useExpressionLibrary.ts +0 -129
  45. package/src/hooks/useForm.ts +0 -352
  46. package/src/hooks/useGeolocation.ts +0 -207
  47. package/src/hooks/useMapView.ts +0 -259
  48. package/src/hooks/useMiddleware.ts +0 -291
  49. package/src/hooks/useModel.ts +0 -363
  50. package/src/hooks/useModule.ts +0 -59
  51. package/src/hooks/useModuleConfig.ts +0 -61
  52. package/src/hooks/useMutation.ts +0 -237
  53. package/src/hooks/useNotification.ts +0 -151
  54. package/src/hooks/useOnChange.ts +0 -30
  55. package/src/hooks/useOnEnter.ts +0 -59
  56. package/src/hooks/useOnEvent.ts +0 -37
  57. package/src/hooks/useOnExit.ts +0 -27
  58. package/src/hooks/useOnTransition.ts +0 -30
  59. package/src/hooks/usePackage.ts +0 -128
  60. package/src/hooks/useParams.ts +0 -33
  61. package/src/hooks/usePlayer.ts +0 -308
  62. package/src/hooks/useQuery.ts +0 -184
  63. package/src/hooks/useRealtimeQuery.ts +0 -222
  64. package/src/hooks/useRole.ts +0 -191
  65. package/src/hooks/useRouteParams.ts +0 -100
  66. package/src/hooks/useRouter.ts +0 -347
  67. package/src/hooks/useServerAction.ts +0 -178
  68. package/src/hooks/useServerState.ts +0 -284
  69. package/src/hooks/useToast.ts +0 -164
  70. package/src/hooks/useTransition.ts +0 -39
  71. package/src/hooks/useView.ts +0 -102
  72. package/src/hooks/useWhileIn.ts +0 -48
  73. package/src/hooks/useWorkflow.ts +0 -63
  74. package/src/index.ts +0 -465
  75. package/src/loader/experience-workflow-loader.ts +0 -192
  76. package/src/loader/index.ts +0 -6
  77. package/src/local/LocalEngine.ts +0 -388
  78. package/src/local/LocalEngineAdapter.ts +0 -175
  79. package/src/local/LocalEngineContext.ts +0 -30
  80. package/src/logger.ts +0 -37
  81. package/src/mixins.ts +0 -1160
  82. package/src/providers/RuntimeContext.ts +0 -20
  83. package/src/providers/WorkflowProvider.tsx +0 -28
  84. package/src/routing/instance-key.ts +0 -107
  85. package/src/server/transition-context.ts +0 -172
  86. package/src/testing/index.ts +0 -9
  87. package/src/testing/useBlueprintTestRunner.ts +0 -91
  88. package/src/testing/useGraphAnalysis.ts +0 -18
  89. package/src/testing/useTestRunner.ts +0 -77
  90. package/src/testing.ts +0 -995
  91. package/src/types/workflow-inference.ts +0 -158
  92. package/src/types.ts +0 -114
  93. package/tsconfig.json +0 -27
  94. package/vitest.config.ts +0 -8
@@ -1,291 +0,0 @@
1
- /**
2
- * useMiddleware — Pre-render middleware pipeline for workflow pages.
3
- *
4
- * Runs a sequence of middleware functions before a page/component renders.
5
- * Similar to Next.js middleware — used for auth checks, data prefetch,
6
- * feature flags, A/B testing, etc.
7
- *
8
- * Each middleware can:
9
- * - Allow (continue to next middleware)
10
- * - Redirect (navigate to a different path)
11
- * - Block (prevent rendering, show loading/error)
12
- *
13
- * Usage in .workflow.tsx:
14
- * const { ready, redirect, error } = useMiddleware([
15
- * requireAuth('/login'),
16
- * requireRole('driver', '/unauthorized'),
17
- * prefetchData('ride', { state: 'active' }),
18
- * ]);
19
- *
20
- * if (!ready) return <LoadingSpinner />;
21
- * // ... render page
22
- *
23
- * Factory functions for common middleware:
24
- * import { requireAuth, requireRole, prefetchData } from '@mindmatrix/react';
25
- */
26
-
27
- import { useState, useEffect, useRef, useMemo } from 'react';
28
-
29
- // =============================================================================
30
- // Types
31
- // =============================================================================
32
-
33
- /** Result of a middleware function. */
34
- export interface MiddlewareResult {
35
- /** Whether to continue to the next middleware. */
36
- ok: boolean;
37
- /** Redirect path (if ok is false and redirect is needed). */
38
- redirect?: string;
39
- /** Error message (if ok is false and no redirect). */
40
- error?: string;
41
- /** Data to pass to the next middleware or component. */
42
- data?: Record<string, unknown>;
43
- }
44
-
45
- /** Middleware context available to each middleware function. */
46
- export interface MiddlewareContext {
47
- /** Current URL pathname. */
48
- pathname: string;
49
- /** Current query params. */
50
- query: Record<string, string>;
51
- /** Data accumulated from previous middleware. */
52
- data: Record<string, unknown>;
53
- /** Auth token from localStorage (if available). */
54
- token: string | null;
55
- }
56
-
57
- /** A middleware function. */
58
- export type MiddlewareFn = (
59
- ctx: MiddlewareContext,
60
- ) => MiddlewareResult | Promise<MiddlewareResult>;
61
-
62
- /** Options for useMiddleware. */
63
- export interface MiddlewareOptions {
64
- /** Enable/disable the middleware pipeline (default: true). */
65
- enabled?: boolean;
66
- /** Re-run middleware when pathname changes (default: true). */
67
- watchPathname?: boolean;
68
- /** Called when middleware redirects. */
69
- onRedirect?: (path: string) => void;
70
- }
71
-
72
- /** Middleware result returned by useMiddleware. */
73
- export interface MiddlewareHandle {
74
- /** Whether all middleware have passed and the page is ready to render. */
75
- ready: boolean;
76
- /** Whether middleware are still running. */
77
- loading: boolean;
78
- /** Redirect path if any middleware requested a redirect. */
79
- redirect: string | null;
80
- /** Error message if any middleware failed. */
81
- error: string | null;
82
- /** Accumulated data from all middleware. */
83
- data: Record<string, unknown>;
84
- /** Re-run the middleware pipeline. */
85
- rerun: () => void;
86
- }
87
-
88
- // =============================================================================
89
- // Middleware Factories
90
- // =============================================================================
91
-
92
- /**
93
- * Factory: Require authentication. Redirects to loginPath if no token.
94
- */
95
- export function requireAuth(loginPath: string = '/login'): MiddlewareFn {
96
- return (ctx) => {
97
- if (!ctx.token) {
98
- return { ok: false, redirect: loginPath };
99
- }
100
- return { ok: true };
101
- };
102
- }
103
-
104
- /**
105
- * Factory: Require a specific role. Checks auth token for role claim.
106
- */
107
- export function requireRole(role: string, redirectPath: string = '/unauthorized'): MiddlewareFn {
108
- return async (ctx) => {
109
- if (!ctx.token) {
110
- return { ok: false, redirect: '/login' };
111
- }
112
-
113
- try {
114
- // Decode JWT payload (no verification — server validates)
115
- const parts = ctx.token.split('.');
116
- if (parts.length !== 3) {
117
- return { ok: false, redirect: redirectPath };
118
- }
119
-
120
- const payload = JSON.parse(atob(parts[1]));
121
- const roles: string[] = payload.roles ?? payload.role ?? [];
122
- const roleList = Array.isArray(roles) ? roles : [roles];
123
-
124
- if (!roleList.includes(role)) {
125
- return { ok: false, redirect: redirectPath };
126
- }
127
-
128
- return { ok: true, data: { userRoles: roleList } };
129
- } catch {
130
- return { ok: false, redirect: redirectPath };
131
- }
132
- };
133
- }
134
-
135
- /**
136
- * Factory: Prefetch data before rendering.
137
- */
138
- export function prefetchData(slug: string, params: Record<string, unknown> = {}): MiddlewareFn {
139
- return async (ctx) => {
140
- try {
141
- const queryString = new URLSearchParams(
142
- Object.entries(params).map(([k, v]) => [k, String(v)]),
143
- ).toString();
144
-
145
- const url = `/api/v1/data/${encodeURIComponent(slug)}${queryString ? `?${queryString}` : ''}`;
146
- const res = await fetch(url, {
147
- headers: ctx.token ? { Authorization: `Bearer ${ctx.token}` } : {},
148
- });
149
-
150
- if (!res.ok) {
151
- return { ok: false, error: `Failed to prefetch ${slug}: ${res.status}` };
152
- }
153
-
154
- const data = await res.json();
155
- return { ok: true, data: { [slug]: data } };
156
- } catch (err) {
157
- return { ok: false, error: `Prefetch failed: ${err instanceof Error ? err.message : String(err)}` };
158
- }
159
- };
160
- }
161
-
162
- // =============================================================================
163
- // Hook
164
- // =============================================================================
165
-
166
- function getPathname(): string {
167
- return typeof window !== 'undefined' ? window.location.pathname : '/';
168
- }
169
-
170
- function getQuery(): Record<string, string> {
171
- if (typeof window === 'undefined') return {};
172
- const params: Record<string, string> = {};
173
- const search = new URLSearchParams(window.location.search);
174
- search.forEach((value, key) => {
175
- params[key] = value;
176
- });
177
- return params;
178
- }
179
-
180
- function getToken(): string | null {
181
- return typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
182
- }
183
-
184
- /**
185
- * Pre-render middleware pipeline.
186
- *
187
- * @param middlewares - Array of middleware functions to run in order.
188
- * @param options - Configuration options.
189
- * @returns Middleware result with ready/loading/redirect/error states.
190
- */
191
- export function useMiddleware(
192
- middlewares: MiddlewareFn[],
193
- options: MiddlewareOptions = {},
194
- ): MiddlewareHandle {
195
- const {
196
- enabled = true,
197
- watchPathname = true,
198
- onRedirect,
199
- } = options;
200
-
201
- const [ready, setReady] = useState(false);
202
- const [loading, setLoading] = useState(true);
203
- const [redirect, setRedirect] = useState<string | null>(null);
204
- const [error, setError] = useState<string | null>(null);
205
- const [data, setData] = useState<Record<string, unknown>>({});
206
- const [runKey, setRunKey] = useState(0);
207
-
208
- const middlewaresRef = useRef(middlewares);
209
- middlewaresRef.current = middlewares;
210
- const onRedirectRef = useRef(onRedirect);
211
- onRedirectRef.current = onRedirect;
212
-
213
- // Track pathname for re-running
214
- const [pathname, setPathname] = useState(getPathname);
215
-
216
- useEffect(() => {
217
- if (!watchPathname) return;
218
- const handler = () => setPathname(getPathname());
219
- window.addEventListener('popstate', handler);
220
- return () => window.removeEventListener('popstate', handler);
221
- }, [watchPathname]);
222
-
223
- // Run middleware pipeline
224
- useEffect(() => {
225
- if (!enabled) {
226
- setReady(true);
227
- setLoading(false);
228
- return;
229
- }
230
-
231
- let cancelled = false;
232
- setLoading(true);
233
- setReady(false);
234
- setRedirect(null);
235
- setError(null);
236
-
237
- const run = async () => {
238
- const ctx: MiddlewareContext = {
239
- pathname,
240
- query: getQuery(),
241
- data: {},
242
- token: getToken(),
243
- };
244
-
245
- for (const mw of middlewaresRef.current) {
246
- if (cancelled) return;
247
-
248
- const result = await mw(ctx);
249
-
250
- if (result.data) {
251
- Object.assign(ctx.data, result.data);
252
- }
253
-
254
- if (!result.ok) {
255
- if (cancelled) return;
256
-
257
- if (result.redirect) {
258
- setRedirect(result.redirect);
259
- onRedirectRef.current?.(result.redirect);
260
- }
261
- if (result.error) {
262
- setError(result.error);
263
- }
264
- setLoading(false);
265
- return;
266
- }
267
- }
268
-
269
- if (cancelled) return;
270
- setData(ctx.data);
271
- setReady(true);
272
- setLoading(false);
273
- };
274
-
275
- run();
276
-
277
- return () => {
278
- cancelled = true;
279
- };
280
- }, [enabled, pathname, runKey]);
281
-
282
- const rerun = useMemo(
283
- () => () => setRunKey((k) => k + 1),
284
- [],
285
- );
286
-
287
- return useMemo(
288
- (): MiddlewareHandle => ({ ready, loading, redirect, error, data, rerun }),
289
- [ready, loading, redirect, error, data, rerun],
290
- );
291
- }
@@ -1,363 +0,0 @@
1
- /**
2
- * useModel — unified hook for connecting a view to a workflow model.
3
- *
4
- * Combines useQuery + useMutation into a single, ergonomic, fully-typed handle.
5
- * This is the recommended API for views that interact with a single model.
6
- *
7
- * For multi-instance patterns (collections), use useCollection().
8
- *
9
- * @example
10
- * ```tsx
11
- * import auth from '../models/authentication.workflow';
12
- * import { useModel } from '@mindmatrix/react';
13
- *
14
- * export default function LoginPage() {
15
- * const { fields, state, trigger, loading } = useModel(auth);
16
- *
17
- * fields.appName; // string — typed, autocomplete
18
- * fields.errorMessage; // string — typed
19
- * trigger('login', { email }); // 'login' autocompletes, typos are TS errors
20
- * state === 'authenticating'; // 'authenticating' autocompletes
21
- * }
22
- * ```
23
- */
24
-
25
- import { useCallback, useRef, useMemo } from 'react';
26
- import type { ModelDefinition } from '../config/defineModel';
27
- import type { InferFields, InferStates, InferTransitions } from '../types/workflow-inference';
28
- import type { QueryParams } from './useQuery';
29
- import { useQuery } from './useQuery';
30
- import { useMutation } from './useMutation';
31
-
32
- // =============================================================================
33
- // Types
34
- // =============================================================================
35
-
36
- /** Options for useModel(). */
37
- export interface UseModelOptions {
38
- /** Specific instance ID to load (instead of limit: 1). */
39
- instanceId?: string;
40
- /** Additional query filters. */
41
- filter?: Record<string, unknown>;
42
- /** Filter by state. */
43
- state?: string | string[];
44
- /** Disable auto-fetching. */
45
- enabled?: boolean;
46
- /** Refetch interval in milliseconds. */
47
- refetchInterval?: number;
48
- }
49
-
50
- /**
51
- * Handle returned by `useModel()`.
52
- *
53
- * Provides typed access to a single workflow instance's fields, current state,
54
- * and transition triggers. All field names, state names, and transition names
55
- * are inferred from the model definition passed to `useModel()`.
56
- *
57
- * @example
58
- * ```tsx
59
- * const auth = useModel(authModel);
60
- * auth.fields.appName; // string — autocomplete works
61
- * auth.state; // 'unauthenticated' | 'authenticating' | ...
62
- * auth.trigger('login', { email }); // transition names autocomplete
63
- * ```
64
- */
65
- export interface ModelHandle<
66
- Fields,
67
- States extends string,
68
- Transitions extends string,
69
- > {
70
- /**
71
- * Current field values, typed from the model definition.
72
- *
73
- * Each property corresponds to a field declared in `defineModel({ fields: { ... } })`.
74
- * Values are typed based on the field's `type` property (e.g., `'string'` → `string`).
75
- */
76
- fields: Fields;
77
- /**
78
- * Current workflow state name.
79
- *
80
- * One of the state names declared in `defineModel({ states: { ... } })`,
81
- * or `''` if no instance is loaded yet.
82
- */
83
- state: States | '';
84
- /** The loaded instance ID, or `null` if no instance is loaded. */
85
- instanceId: string | null;
86
- /**
87
- * Fire a named transition on the current instance.
88
- *
89
- * @param name - Transition name (autocompletes from the model's transitions).
90
- * @param input - Optional input data passed to the transition's actions/conditions.
91
- * @throws If no instance is loaded.
92
- *
93
- * @example
94
- * ```tsx
95
- * auth.trigger('login', { email: 'user@example.com', password: '...' });
96
- * auth.trigger('logout');
97
- * ```
98
- */
99
- trigger: (name: Transitions, input?: Record<string, unknown>) => Promise<void>;
100
- /** Alias for `trigger` — same behavior, alternate name. */
101
- transition: (name: Transitions, input?: Record<string, unknown>) => Promise<void>;
102
- /**
103
- * Create a new workflow instance with optional initial field values.
104
- * @returns The ID of the newly created instance.
105
- */
106
- create: (input?: Record<string, unknown>) => Promise<string>;
107
- /**
108
- * Update fields on the current instance (partial update).
109
- * @param fields - Object with field names and new values.
110
- */
111
- update: (fields: Partial<Fields>) => Promise<void>;
112
- /** `true` while the initial query is loading. */
113
- loading: boolean;
114
- /** The last error from query or mutation, or `null`. */
115
- error: Error | null;
116
- /** Refetch data from the backend. */
117
- refetch: () => Promise<void>;
118
- /** `true` while a mutation (trigger/create/update) is in progress. */
119
- isPending: boolean;
120
- /** Reset the error state to `null`. */
121
- reset: () => void;
122
- /** Raw query result — use for advanced scenarios (pagination, counts). */
123
- raw: { data: unknown[]; total?: number };
124
- }
125
-
126
- // =============================================================================
127
- // Default Fields Helper
128
- // =============================================================================
129
-
130
- function getDefaultFields(definition: ModelDefinition): Record<string, unknown> {
131
- const defaults: Record<string, unknown> = {};
132
- for (const [key, field] of Object.entries(definition.fields)) {
133
- if (field.default !== undefined) {
134
- defaults[key] = field.default;
135
- } else {
136
- // Sensible defaults by type
137
- switch (field.type) {
138
- case 'string': case 'text': case 'email': case 'url': case 'phone':
139
- case 'color': case 'select': case 'rich_text':
140
- defaults[key] = '';
141
- break;
142
- case 'number': case 'integer': case 'float': case 'currency':
143
- case 'percentage': case 'rating': case 'duration':
144
- defaults[key] = 0;
145
- break;
146
- case 'boolean':
147
- defaults[key] = false;
148
- break;
149
- case 'array':
150
- defaults[key] = [];
151
- break;
152
- case 'object': case 'json':
153
- defaults[key] = {};
154
- break;
155
- default:
156
- defaults[key] = undefined;
157
- }
158
- }
159
- }
160
- return defaults;
161
- }
162
-
163
- // =============================================================================
164
- // Hook Implementation
165
- // =============================================================================
166
-
167
- /**
168
- * Connect a view to a workflow model with full type inference.
169
- *
170
- * @param definition - The model definition (from defineModel() or import).
171
- * @param options - Optional configuration (instanceId, filter, etc.).
172
- * @returns Fully typed model handle with fields, state, trigger, etc.
173
- */
174
- export function useModel<D extends ModelDefinition>(
175
- definition: D,
176
- options: UseModelOptions = {},
177
- ): ModelHandle<InferFields<D>, InferStates<D> & string, InferTransitions<D> & string> {
178
- const slug = definition.slug;
179
-
180
- // Build query params
181
- const queryParams: QueryParams = {
182
- limit: 1,
183
- enabled: options.enabled,
184
- refetchInterval: options.refetchInterval,
185
- };
186
- if (options.filter) queryParams.filter = options.filter;
187
- if (options.state) queryParams.state = options.state;
188
-
189
- // Query + mutation
190
- const query = useQuery(slug, queryParams);
191
- const mutation = useMutation(slug);
192
-
193
- // Extract instance data
194
- const instance = query.data?.[0] as Record<string, unknown> | undefined;
195
- const instanceId = (instance?.id as string) ?? null;
196
- const currentState = (instance?.currentState as string) ?? '';
197
- const instanceFields = (instance?.fields as Record<string, unknown>) ?? null;
198
-
199
- // Compute defaults once
200
- const defaultFields = useMemo(() => getDefaultFields(definition), [definition]);
201
-
202
- // Merge instance fields with defaults
203
- const fields = useMemo(() => {
204
- if (!instanceFields) return defaultFields;
205
- return { ...defaultFields, ...instanceFields };
206
- }, [instanceFields, defaultFields]);
207
-
208
- // Trigger function
209
- const instanceIdRef = useRef(instanceId);
210
- instanceIdRef.current = instanceId;
211
-
212
- const trigger = useCallback(async (name: string, input?: Record<string, unknown>) => {
213
- const id = instanceIdRef.current;
214
- if (!id) {
215
- throw new Error(`useModel(${slug}): No instance loaded. Cannot trigger '${name}'.`);
216
- }
217
- await mutation.transition(id, name, input);
218
- // Refetch after transition to get updated state
219
- await query.refetch();
220
- }, [slug, mutation, query]);
221
-
222
- // Create function
223
- const create = useCallback(async (input?: Record<string, unknown>) => {
224
- const id = await mutation.create(input);
225
- await query.refetch();
226
- return id;
227
- }, [mutation, query]);
228
-
229
- // Update function
230
- const update = useCallback(async (fieldUpdates: Record<string, unknown>) => {
231
- const id = instanceIdRef.current;
232
- if (!id) {
233
- throw new Error(`useModel(${slug}): No instance loaded. Cannot update.`);
234
- }
235
- await mutation.update(id, fieldUpdates);
236
- await query.refetch();
237
- }, [slug, mutation, query]);
238
-
239
- type Handle = ModelHandle<InferFields<D>, InferStates<D> & string, InferTransitions<D> & string>;
240
-
241
- const handle: Handle = {
242
- fields: fields as Handle['fields'],
243
- state: currentState as Handle['state'],
244
- instanceId,
245
- trigger: trigger as Handle['trigger'],
246
- transition: trigger as Handle['transition'],
247
- create,
248
- update: update as Handle['update'],
249
- loading: query.loading,
250
- error: query.error ?? mutation.error,
251
- refetch: query.refetch,
252
- isPending: mutation.isPending,
253
- reset: mutation.reset,
254
- raw: { data: query.data, total: query.total },
255
- };
256
-
257
- return handle;
258
- }
259
-
260
- // =============================================================================
261
- // useCollection — multi-instance variant
262
- // =============================================================================
263
-
264
- /** Handle returned by useCollection(). */
265
- export interface CollectionHandle<
266
- Fields,
267
- States extends string,
268
- Transitions extends string,
269
- > {
270
- /** Array of instances with typed fields. */
271
- items: Array<{ id: string; fields: Fields; state: States }>;
272
- /** Total count of matching instances. */
273
- total: number;
274
- /** Whether data is loading. */
275
- loading: boolean;
276
- /** Last error. */
277
- error: Error | null;
278
- /** Refetch data. */
279
- refetch: () => Promise<void>;
280
- /** Whether there are more results. */
281
- hasMore: boolean;
282
- /** Fire a transition on a specific instance. */
283
- trigger: (instanceId: string, name: Transitions, input?: Record<string, unknown>) => Promise<void>;
284
- /** Create a new instance. */
285
- create: (input?: Record<string, unknown>) => Promise<string>;
286
- /** Update fields on a specific instance. */
287
- update: (instanceId: string, fields: Partial<Fields>) => Promise<void>;
288
- /** Remove an instance. */
289
- remove: (instanceId: string) => Promise<void>;
290
- /** Whether a mutation is in progress. */
291
- isPending: boolean;
292
- }
293
-
294
- /** Options for useCollection(). */
295
- export interface UseCollectionOptions extends QueryParams {}
296
-
297
- /**
298
- * Connect a view to a collection of workflow model instances.
299
- *
300
- * @example
301
- * ```tsx
302
- * import channelModel from '../models/channel';
303
- * const { items, trigger, create } = useCollection(channelModel, { state: 'active' });
304
- *
305
- * items.map(ch => ch.fields.name); // string[] — typed
306
- * trigger(ch.id, 'archive'); // transition name typed
307
- * ```
308
- */
309
- export function useCollection<D extends ModelDefinition>(
310
- definition: D,
311
- options: UseCollectionOptions = {},
312
- ): CollectionHandle<InferFields<D>, InferStates<D> & string, InferTransitions<D> & string> {
313
- const slug = definition.slug;
314
- const query = useQuery(slug, options);
315
- const mutation = useMutation(slug);
316
-
317
- const items = useMemo(() => {
318
- return (query.data || []).map((instance: any) => ({
319
- id: instance.id as string,
320
- fields: (instance.fields ?? {}) as InferFields<D>,
321
- state: (instance.currentState ?? '') as InferStates<D> & string,
322
- }));
323
- }, [query.data]);
324
-
325
- const trigger = useCallback(async (instanceId: string, name: string, input?: Record<string, unknown>) => {
326
- await mutation.transition(instanceId, name, input);
327
- await query.refetch();
328
- }, [mutation, query]);
329
-
330
- const create = useCallback(async (input?: Record<string, unknown>) => {
331
- const id = await mutation.create(input);
332
- await query.refetch();
333
- return id;
334
- }, [mutation, query]);
335
-
336
- const update = useCallback(async (instanceId: string, fieldUpdates: Record<string, unknown>) => {
337
- await mutation.update(instanceId, fieldUpdates);
338
- await query.refetch();
339
- }, [mutation, query]);
340
-
341
- const remove = useCallback(async (instanceId: string) => {
342
- await mutation.remove(instanceId);
343
- await query.refetch();
344
- }, [mutation, query]);
345
-
346
- type CHandle = CollectionHandle<InferFields<D>, InferStates<D> & string, InferTransitions<D> & string>;
347
-
348
- const handle: CHandle = {
349
- items: items as CHandle['items'],
350
- total: query.total ?? items.length,
351
- loading: query.loading,
352
- error: query.error ?? mutation.error,
353
- refetch: query.refetch,
354
- hasMore: query.hasMore ?? false,
355
- trigger: trigger as CHandle['trigger'],
356
- create,
357
- update: update as CHandle['update'],
358
- remove,
359
- isPending: mutation.isPending,
360
- };
361
-
362
- return handle;
363
- }