@mmapp/react 0.1.0-alpha.1 → 0.1.0-alpha.3
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/README.md +112 -0
- package/dist/index.d.mts +27 -2
- package/dist/index.d.ts +27 -2
- package/dist/index.js +70 -3
- package/dist/index.mjs +74 -12
- package/package.json +4 -3
- package/package.json.backup +0 -41
- package/src/Blueprint.ts +0 -216
- package/src/__tests__/Blueprint.test.ts +0 -106
- package/src/__tests__/action-context.test.ts +0 -166
- package/src/__tests__/actionCreators.test.ts +0 -179
- package/src/__tests__/builders.test.ts +0 -336
- package/src/__tests__/defineBlueprint-composition.test.ts +0 -106
- package/src/__tests__/factories.test.ts +0 -229
- package/src/__tests__/loader.test.ts +0 -159
- package/src/__tests__/logger.test.ts +0 -70
- package/src/__tests__/type-inference.test.ts +0 -160
- package/src/__tests__/typed-transitions.test.ts +0 -126
- package/src/__tests__/useModuleConfig.test.ts +0 -61
- package/src/actionCreators.ts +0 -132
- package/src/actions.ts +0 -547
- package/src/atoms/index.ts +0 -600
- package/src/authoring.ts +0 -92
- package/src/browser-player.ts +0 -783
- package/src/builders.ts +0 -1342
- package/src/components/ExperienceWorkflowBridge.tsx +0 -123
- package/src/components/PlayerProvider.tsx +0 -43
- package/src/components/atoms/index.tsx +0 -269
- package/src/components/index.ts +0 -36
- package/src/conditions.ts +0 -692
- package/src/config/defineBlueprint.ts +0 -329
- package/src/config/defineModel.ts +0 -753
- package/src/config/defineWorkspace.ts +0 -24
- package/src/core/WorkflowRuntime.ts +0 -153
- package/src/factories.ts +0 -425
- package/src/grammar/index.ts +0 -173
- package/src/hooks/index.ts +0 -106
- package/src/hooks/useAuth.ts +0 -288
- package/src/hooks/useChannel.ts +0 -304
- package/src/hooks/useComputed.ts +0 -154
- package/src/hooks/useDomainSubscription.ts +0 -110
- package/src/hooks/useDuringAction.ts +0 -99
- package/src/hooks/useExperienceState.ts +0 -59
- package/src/hooks/useExpressionLibrary.ts +0 -129
- package/src/hooks/useForm.ts +0 -352
- package/src/hooks/useGeolocation.ts +0 -207
- package/src/hooks/useMapView.ts +0 -259
- package/src/hooks/useMiddleware.ts +0 -291
- package/src/hooks/useModel.ts +0 -363
- package/src/hooks/useModule.ts +0 -59
- package/src/hooks/useModuleConfig.ts +0 -61
- package/src/hooks/useMutation.ts +0 -237
- package/src/hooks/useNotification.ts +0 -151
- package/src/hooks/useOnChange.ts +0 -30
- package/src/hooks/useOnEnter.ts +0 -59
- package/src/hooks/useOnEvent.ts +0 -37
- package/src/hooks/useOnExit.ts +0 -27
- package/src/hooks/useOnTransition.ts +0 -30
- package/src/hooks/usePackage.ts +0 -128
- package/src/hooks/useParams.ts +0 -33
- package/src/hooks/usePlayer.ts +0 -308
- package/src/hooks/useQuery.ts +0 -184
- package/src/hooks/useRealtimeQuery.ts +0 -222
- package/src/hooks/useRole.ts +0 -191
- package/src/hooks/useRouteParams.ts +0 -100
- package/src/hooks/useRouter.ts +0 -347
- package/src/hooks/useServerAction.ts +0 -178
- package/src/hooks/useServerState.ts +0 -284
- package/src/hooks/useToast.ts +0 -164
- package/src/hooks/useTransition.ts +0 -39
- package/src/hooks/useView.ts +0 -102
- package/src/hooks/useWhileIn.ts +0 -48
- package/src/hooks/useWorkflow.ts +0 -63
- package/src/index.ts +0 -465
- package/src/loader/experience-workflow-loader.ts +0 -192
- package/src/loader/index.ts +0 -6
- package/src/local/LocalEngine.ts +0 -388
- package/src/local/LocalEngineAdapter.ts +0 -175
- package/src/local/LocalEngineContext.ts +0 -30
- package/src/logger.ts +0 -37
- package/src/mixins.ts +0 -1160
- package/src/providers/RuntimeContext.ts +0 -20
- package/src/providers/WorkflowProvider.tsx +0 -28
- package/src/routing/instance-key.ts +0 -107
- package/src/server/transition-context.ts +0 -172
- package/src/testing/index.ts +0 -9
- package/src/testing/useBlueprintTestRunner.ts +0 -91
- package/src/testing/useGraphAnalysis.ts +0 -18
- package/src/testing/useTestRunner.ts +0 -77
- package/src/testing.ts +0 -995
- package/src/types/workflow-inference.ts +0 -158
- package/src/types.ts +0 -114
- package/tsconfig.json +0 -27
- 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
|
-
}
|
package/src/hooks/useModel.ts
DELETED
|
@@ -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
|
-
}
|