@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.
- package/README.md +112 -0
- package/dist/index.d.mts +1378 -94
- package/dist/index.d.ts +1378 -94
- package/dist/index.js +1094 -1309
- package/dist/index.mjs +1038 -1296
- 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,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useRealtimeQuery — Reactive data fetching with SSE live updates.
|
|
3
|
-
*
|
|
4
|
-
* Wraps useQuery with a Server-Sent Events subscription for real-time
|
|
5
|
-
* data updates. When the server pushes an event matching the slug,
|
|
6
|
-
* the query automatically refetches.
|
|
7
|
-
*
|
|
8
|
-
* Critical for ride tracking, driver location, order status, etc.
|
|
9
|
-
*
|
|
10
|
-
* Usage in .workflow.tsx:
|
|
11
|
-
* const { data: rides, loading, connected } = useRealtimeQuery('ride', {
|
|
12
|
-
* state: 'in_progress',
|
|
13
|
-
* filter: { driver_id: currentUser.id },
|
|
14
|
-
* });
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
18
|
-
import type { QueryParams, QueryResult, WorkflowInstance } from './useQuery';
|
|
19
|
-
|
|
20
|
-
// =============================================================================
|
|
21
|
-
// Types
|
|
22
|
-
// =============================================================================
|
|
23
|
-
|
|
24
|
-
export interface RealtimeQueryParams extends QueryParams {
|
|
25
|
-
/** SSE event channel to listen on (defaults to slug). */
|
|
26
|
-
channel?: string;
|
|
27
|
-
/** SSE endpoint URL (defaults to /api/v1/workflow/events). */
|
|
28
|
-
sseUrl?: string;
|
|
29
|
-
/** Auto-reconnect on disconnect. */
|
|
30
|
-
reconnect?: boolean;
|
|
31
|
-
/** Reconnect delay in ms (default: 3000). */
|
|
32
|
-
reconnectDelay?: number;
|
|
33
|
-
/** Debounce refetch on rapid events (ms, default: 100). */
|
|
34
|
-
debounce?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface RealtimeQueryResult<T> extends QueryResult<T> {
|
|
38
|
-
/** Whether the SSE connection is active. */
|
|
39
|
-
connected: boolean;
|
|
40
|
-
/** Force reconnect the SSE stream. */
|
|
41
|
-
reconnect: () => void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Re-use the global query resolver from useQuery
|
|
45
|
-
interface InternalQueryResolver {
|
|
46
|
-
query: <T>(slug: string, params: QueryParams) => Promise<{ data: T[]; total?: number }>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let _queryResolver: InternalQueryResolver | null = null;
|
|
50
|
-
|
|
51
|
-
export function setRealtimeQueryResolver(resolver: InternalQueryResolver | null): void {
|
|
52
|
-
_queryResolver = resolver;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// =============================================================================
|
|
56
|
-
// Internal
|
|
57
|
-
// =============================================================================
|
|
58
|
-
|
|
59
|
-
function getToken(): string | null {
|
|
60
|
-
return typeof localStorage !== 'undefined' ? localStorage.getItem('auth_token') : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// =============================================================================
|
|
64
|
-
// Hook
|
|
65
|
-
// =============================================================================
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Fetches workflow instances with real-time SSE updates.
|
|
69
|
-
*
|
|
70
|
-
* @param slug - Workflow definition slug to query.
|
|
71
|
-
* @param params - Query params + SSE configuration.
|
|
72
|
-
* @returns Query result with connected status and reconnect.
|
|
73
|
-
*/
|
|
74
|
-
export function useRealtimeQuery<T = Record<string, unknown>>(
|
|
75
|
-
slug: string,
|
|
76
|
-
params: RealtimeQueryParams = {},
|
|
77
|
-
): RealtimeQueryResult<T> {
|
|
78
|
-
const {
|
|
79
|
-
channel,
|
|
80
|
-
sseUrl,
|
|
81
|
-
reconnect: autoReconnect = true,
|
|
82
|
-
reconnectDelay = 3000,
|
|
83
|
-
debounce = 100,
|
|
84
|
-
enabled,
|
|
85
|
-
...queryParams
|
|
86
|
-
} = params;
|
|
87
|
-
|
|
88
|
-
const [data, setData] = useState<WorkflowInstance<T>[]>([]);
|
|
89
|
-
const [loading, setLoading] = useState(true);
|
|
90
|
-
const [error, setError] = useState<Error | null>(null);
|
|
91
|
-
const [total, setTotal] = useState<number | undefined>(undefined);
|
|
92
|
-
const [connected, setConnected] = useState(false);
|
|
93
|
-
const [reconnectKey, setReconnectKey] = useState(0);
|
|
94
|
-
|
|
95
|
-
const queryParamsRef = useRef(queryParams);
|
|
96
|
-
queryParamsRef.current = queryParams;
|
|
97
|
-
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
98
|
-
|
|
99
|
-
// Fetch data from resolver
|
|
100
|
-
const fetchData = useCallback(async () => {
|
|
101
|
-
if (enabled === false) {
|
|
102
|
-
setLoading(false);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const resolver = _queryResolver;
|
|
107
|
-
if (!resolver) {
|
|
108
|
-
setError(new Error('useRealtimeQuery: No query resolver configured.'));
|
|
109
|
-
setLoading(false);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
setLoading(true);
|
|
115
|
-
setError(null);
|
|
116
|
-
const result = await resolver.query<WorkflowInstance<T>>(slug, queryParamsRef.current);
|
|
117
|
-
setData(result.data);
|
|
118
|
-
setTotal(result.total);
|
|
119
|
-
} catch (err) {
|
|
120
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
121
|
-
} finally {
|
|
122
|
-
setLoading(false);
|
|
123
|
-
}
|
|
124
|
-
}, [slug, enabled]);
|
|
125
|
-
|
|
126
|
-
// Initial fetch
|
|
127
|
-
useEffect(() => {
|
|
128
|
-
fetchData();
|
|
129
|
-
}, [fetchData]);
|
|
130
|
-
|
|
131
|
-
// SSE subscription for live updates
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
if (enabled === false) return;
|
|
134
|
-
|
|
135
|
-
const token = getToken();
|
|
136
|
-
if (!token) return;
|
|
137
|
-
|
|
138
|
-
const eventChannel = channel ?? slug;
|
|
139
|
-
const url = sseUrl ?? `/api/v1/workflow/events?token=${encodeURIComponent(token)}`;
|
|
140
|
-
|
|
141
|
-
let es: EventSource | null = null;
|
|
142
|
-
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
143
|
-
|
|
144
|
-
const connect = () => {
|
|
145
|
-
es = new EventSource(url);
|
|
146
|
-
|
|
147
|
-
es.addEventListener('connected', () => {
|
|
148
|
-
setConnected(true);
|
|
149
|
-
setError(null);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Listen for events that match our channel/slug
|
|
153
|
-
const relevantEvents = [
|
|
154
|
-
'workflow:transitioned',
|
|
155
|
-
'workflow:data_updated',
|
|
156
|
-
'workflow:instance_created',
|
|
157
|
-
'workflow:instance_deleted',
|
|
158
|
-
'workflow:state_changed',
|
|
159
|
-
];
|
|
160
|
-
|
|
161
|
-
for (const eventType of relevantEvents) {
|
|
162
|
-
es.addEventListener(eventType, (e: Event) => {
|
|
163
|
-
const messageEvent = e as MessageEvent;
|
|
164
|
-
try {
|
|
165
|
-
const eventData = JSON.parse(messageEvent.data);
|
|
166
|
-
// Filter: only process events matching our slug/channel
|
|
167
|
-
if (
|
|
168
|
-
eventData.definition_slug === eventChannel ||
|
|
169
|
-
eventData.slug === eventChannel ||
|
|
170
|
-
eventData.channel === eventChannel
|
|
171
|
-
) {
|
|
172
|
-
// Debounced refetch
|
|
173
|
-
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
174
|
-
debounceRef.current = setTimeout(() => {
|
|
175
|
-
fetchData();
|
|
176
|
-
}, debounce);
|
|
177
|
-
}
|
|
178
|
-
} catch {
|
|
179
|
-
// Ignore parse errors
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
es.onerror = () => {
|
|
185
|
-
setConnected(false);
|
|
186
|
-
es?.close();
|
|
187
|
-
es = null;
|
|
188
|
-
|
|
189
|
-
if (autoReconnect) {
|
|
190
|
-
reconnectTimer = setTimeout(connect, reconnectDelay);
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
connect();
|
|
196
|
-
|
|
197
|
-
return () => {
|
|
198
|
-
es?.close();
|
|
199
|
-
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
200
|
-
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
201
|
-
setConnected(false);
|
|
202
|
-
};
|
|
203
|
-
}, [slug, channel, sseUrl, enabled, autoReconnect, reconnectDelay, debounce, fetchData, reconnectKey]);
|
|
204
|
-
|
|
205
|
-
const forceReconnect = useCallback(() => {
|
|
206
|
-
setReconnectKey((k) => k + 1);
|
|
207
|
-
}, []);
|
|
208
|
-
|
|
209
|
-
const hasMore = total !== undefined && data.length < total;
|
|
210
|
-
|
|
211
|
-
const handle: RealtimeQueryResult<T> = {
|
|
212
|
-
data,
|
|
213
|
-
loading,
|
|
214
|
-
error,
|
|
215
|
-
refetch: fetchData,
|
|
216
|
-
total,
|
|
217
|
-
hasMore,
|
|
218
|
-
connected,
|
|
219
|
-
reconnect: forceReconnect,
|
|
220
|
-
};
|
|
221
|
-
return handle;
|
|
222
|
-
}
|
package/src/hooks/useRole.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useRole — Check if the current user has a given role (with hierarchy support).
|
|
3
|
-
*
|
|
4
|
-
* Reads the runtime's current state to determine the user's roles
|
|
5
|
-
* and permissions. Supports role hierarchies where higher roles
|
|
6
|
-
* automatically include lower role permissions.
|
|
7
|
-
*
|
|
8
|
-
* Usage in .workflow.tsx:
|
|
9
|
-
* const { hasRole, permissions } = useRole('admin');
|
|
10
|
-
* if (hasRole) { ... }
|
|
11
|
-
*
|
|
12
|
-
* // With role hierarchy: admin > manager > driver > rider
|
|
13
|
-
* const { hasRole, isAbove, isBelow } = useRole('driver', {
|
|
14
|
-
* hierarchy: ['admin', 'manager', 'driver', 'rider'],
|
|
15
|
-
* });
|
|
16
|
-
* isAbove('rider') // true if user is driver or higher
|
|
17
|
-
* isBelow('manager') // true if user is driver or lower
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { useMemo, useCallback } from 'react';
|
|
21
|
-
import { useRuntimeContext } from '../providers/RuntimeContext';
|
|
22
|
-
|
|
23
|
-
// =============================================================================
|
|
24
|
-
// Types
|
|
25
|
-
// =============================================================================
|
|
26
|
-
|
|
27
|
-
/** Options for role hierarchy configuration. */
|
|
28
|
-
export interface RoleOptions {
|
|
29
|
-
/** Role hierarchy from highest to lowest (e.g., ['admin', 'manager', 'user']). */
|
|
30
|
-
hierarchy?: string[];
|
|
31
|
-
/** Include permissions from all roles below in the hierarchy. */
|
|
32
|
-
inheritPermissions?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface RoleResult {
|
|
36
|
-
/** Whether the current user has the specified role. */
|
|
37
|
-
hasRole: boolean;
|
|
38
|
-
/** Permissions granted by this role (from definition.roles). */
|
|
39
|
-
permissions: string[];
|
|
40
|
-
/** All roles the current user has. */
|
|
41
|
-
roles: string[];
|
|
42
|
-
/** Check if user's highest role is above the given role in the hierarchy. */
|
|
43
|
-
isAbove: (role: string) => boolean;
|
|
44
|
-
/** Check if user's highest role is below the given role in the hierarchy. */
|
|
45
|
-
isBelow: (role: string) => boolean;
|
|
46
|
-
/** Check if user has a specific permission. */
|
|
47
|
-
hasPermission: (permission: string) => boolean;
|
|
48
|
-
/** The user's highest role in the hierarchy (or first role if no hierarchy). */
|
|
49
|
-
highestRole: string | null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Global role hierarchy (set by provider)
|
|
53
|
-
let _globalHierarchy: string[] | null = null;
|
|
54
|
-
|
|
55
|
-
export function setRoleHierarchy(hierarchy: string[] | null): void {
|
|
56
|
-
_globalHierarchy = hierarchy;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// =============================================================================
|
|
60
|
-
// Hook
|
|
61
|
-
// =============================================================================
|
|
62
|
-
|
|
63
|
-
export function useRole(roleName: string, options: RoleOptions = {}): RoleResult {
|
|
64
|
-
const runtime = useRuntimeContext();
|
|
65
|
-
const hierarchy = options.hierarchy ?? _globalHierarchy ?? [];
|
|
66
|
-
const inheritPermissions = options.inheritPermissions ?? true;
|
|
67
|
-
|
|
68
|
-
const result = useMemo(() => {
|
|
69
|
-
const snapshot = runtime.getSnapshot();
|
|
70
|
-
const definition = (runtime as any).config?.definition;
|
|
71
|
-
|
|
72
|
-
// Resolve user roles from state_data or context
|
|
73
|
-
const userRoles: string[] =
|
|
74
|
-
(snapshot.stateData as any)?._userRoles ??
|
|
75
|
-
(snapshot.stateData as any)?.user_roles ??
|
|
76
|
-
[];
|
|
77
|
-
|
|
78
|
-
// Direct role check
|
|
79
|
-
let hasRole = userRoles.includes(roleName);
|
|
80
|
-
|
|
81
|
-
// Hierarchy-based role check: if user has a higher role, they also "have" lower roles
|
|
82
|
-
if (!hasRole && hierarchy.length > 0) {
|
|
83
|
-
const targetIdx = hierarchy.indexOf(roleName);
|
|
84
|
-
if (targetIdx !== -1) {
|
|
85
|
-
for (const userRole of userRoles) {
|
|
86
|
-
const userIdx = hierarchy.indexOf(userRole);
|
|
87
|
-
if (userIdx !== -1 && userIdx <= targetIdx) {
|
|
88
|
-
// User has a role at same level or higher (lower index = higher rank)
|
|
89
|
-
hasRole = true;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Find permissions for this role from definition.roles
|
|
97
|
-
let permissions: string[] = [];
|
|
98
|
-
if (definition?.roles && Array.isArray(definition.roles)) {
|
|
99
|
-
if (inheritPermissions && hierarchy.length > 0) {
|
|
100
|
-
// Collect permissions from this role and all roles below in hierarchy
|
|
101
|
-
const targetIdx = hierarchy.indexOf(roleName);
|
|
102
|
-
const rolesToCheck = targetIdx !== -1
|
|
103
|
-
? hierarchy.slice(targetIdx)
|
|
104
|
-
: [roleName];
|
|
105
|
-
|
|
106
|
-
for (const r of rolesToCheck) {
|
|
107
|
-
const roleDef = definition.roles.find((rd: any) => rd.name === r);
|
|
108
|
-
if (roleDef?.permissions && Array.isArray(roleDef.permissions)) {
|
|
109
|
-
permissions = [...permissions, ...roleDef.permissions];
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Deduplicate
|
|
113
|
-
permissions = [...new Set(permissions)];
|
|
114
|
-
} else {
|
|
115
|
-
const roleDef = definition.roles.find(
|
|
116
|
-
(r: any) => r.name === roleName,
|
|
117
|
-
);
|
|
118
|
-
if (roleDef?.permissions && Array.isArray(roleDef.permissions)) {
|
|
119
|
-
permissions = roleDef.permissions;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Find highest role
|
|
125
|
-
let highestRole: string | null = userRoles[0] ?? null;
|
|
126
|
-
if (hierarchy.length > 0) {
|
|
127
|
-
let bestIdx = Infinity;
|
|
128
|
-
for (const role of userRoles) {
|
|
129
|
-
const idx = hierarchy.indexOf(role);
|
|
130
|
-
if (idx !== -1 && idx < bestIdx) {
|
|
131
|
-
bestIdx = idx;
|
|
132
|
-
highestRole = role;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { hasRole, permissions, roles: userRoles, highestRole };
|
|
138
|
-
}, [runtime, roleName, hierarchy, inheritPermissions]);
|
|
139
|
-
|
|
140
|
-
const isAbove = useCallback(
|
|
141
|
-
(role: string): boolean => {
|
|
142
|
-
if (hierarchy.length === 0) return false;
|
|
143
|
-
const targetIdx = hierarchy.indexOf(role);
|
|
144
|
-
if (targetIdx === -1) return false;
|
|
145
|
-
|
|
146
|
-
for (const userRole of result.roles) {
|
|
147
|
-
const userIdx = hierarchy.indexOf(userRole);
|
|
148
|
-
if (userIdx !== -1 && userIdx < targetIdx) return true;
|
|
149
|
-
}
|
|
150
|
-
return false;
|
|
151
|
-
},
|
|
152
|
-
[hierarchy, result.roles],
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const isBelow = useCallback(
|
|
156
|
-
(role: string): boolean => {
|
|
157
|
-
if (hierarchy.length === 0) return false;
|
|
158
|
-
const targetIdx = hierarchy.indexOf(role);
|
|
159
|
-
if (targetIdx === -1) return false;
|
|
160
|
-
|
|
161
|
-
// User is below if ALL their roles are lower
|
|
162
|
-
let hasAnyHierarchyRole = false;
|
|
163
|
-
for (const userRole of result.roles) {
|
|
164
|
-
const userIdx = hierarchy.indexOf(userRole);
|
|
165
|
-
if (userIdx !== -1) {
|
|
166
|
-
hasAnyHierarchyRole = true;
|
|
167
|
-
if (userIdx <= targetIdx) return false; // Has same or higher role
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return hasAnyHierarchyRole;
|
|
171
|
-
},
|
|
172
|
-
[hierarchy, result.roles],
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
const hasPermission = useCallback(
|
|
176
|
-
(permission: string): boolean => {
|
|
177
|
-
return result.permissions.includes(permission);
|
|
178
|
-
},
|
|
179
|
-
[result.permissions],
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
return useMemo(
|
|
183
|
-
(): RoleResult => ({
|
|
184
|
-
...result,
|
|
185
|
-
isAbove,
|
|
186
|
-
isBelow,
|
|
187
|
-
hasPermission,
|
|
188
|
-
}),
|
|
189
|
-
[result, isAbove, isBelow, hasPermission],
|
|
190
|
-
);
|
|
191
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useRouteParams — Access dynamic URL route parameters.
|
|
3
|
-
*
|
|
4
|
-
* Parses the current URL pathname against a route pattern and
|
|
5
|
-
* returns extracted dynamic segments. Like Next.js useParams().
|
|
6
|
-
*
|
|
7
|
-
* Note: The existing useParams() hook reads workflow invocation params
|
|
8
|
-
* from stateData. This hook reads URL route params.
|
|
9
|
-
*
|
|
10
|
-
* Usage in .workflow.tsx:
|
|
11
|
-
* // Given URL: /rides/abc123/tracking
|
|
12
|
-
* // Pattern: /rides/:rideId/:tab
|
|
13
|
-
* const { rideId, tab } = useRouteParams<{ rideId: string; tab: string }>();
|
|
14
|
-
* // rideId = 'abc123', tab = 'tracking'
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
18
|
-
|
|
19
|
-
// =============================================================================
|
|
20
|
-
// Types
|
|
21
|
-
// =============================================================================
|
|
22
|
-
|
|
23
|
-
export interface RouteParamsOptions {
|
|
24
|
-
/** Route pattern to match against (e.g., '/rides/:id'). If omitted, extracts all :segments from path. */
|
|
25
|
-
pattern?: string;
|
|
26
|
-
/** Base path to strip before matching. */
|
|
27
|
-
basePath?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// Internal
|
|
32
|
-
// =============================================================================
|
|
33
|
-
|
|
34
|
-
function extractParams(pathname: string, pattern: string): Record<string, string> {
|
|
35
|
-
const pathParts = pathname.split('/').filter(Boolean);
|
|
36
|
-
const patternParts = pattern.split('/').filter(Boolean);
|
|
37
|
-
|
|
38
|
-
const params: Record<string, string> = {};
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < patternParts.length && i < pathParts.length; i++) {
|
|
41
|
-
if (patternParts[i].startsWith(':')) {
|
|
42
|
-
params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return params;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Simple segment extraction — treats every path segment as a positional param. */
|
|
50
|
-
function extractAllSegments(pathname: string): Record<string, string> {
|
|
51
|
-
const parts = pathname.split('/').filter(Boolean);
|
|
52
|
-
const params: Record<string, string> = {};
|
|
53
|
-
for (let i = 0; i < parts.length; i++) {
|
|
54
|
-
params[`$${i}`] = decodeURIComponent(parts[i]);
|
|
55
|
-
}
|
|
56
|
-
return params;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// =============================================================================
|
|
60
|
-
// Hook
|
|
61
|
-
// =============================================================================
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Reads dynamic URL route parameters from the current pathname.
|
|
65
|
-
*
|
|
66
|
-
* @param options - Optional pattern and basePath configuration.
|
|
67
|
-
* @returns Parsed route parameters.
|
|
68
|
-
*/
|
|
69
|
-
export function useRouteParams<T extends Record<string, string> = Record<string, string>>(
|
|
70
|
-
options: RouteParamsOptions = {},
|
|
71
|
-
): T {
|
|
72
|
-
const { pattern, basePath = '' } = options;
|
|
73
|
-
const patternRef = useRef(pattern);
|
|
74
|
-
patternRef.current = pattern;
|
|
75
|
-
|
|
76
|
-
const getPathname = () => {
|
|
77
|
-
if (typeof window === 'undefined') return '/';
|
|
78
|
-
let path = window.location.pathname;
|
|
79
|
-
if (basePath && path.startsWith(basePath)) {
|
|
80
|
-
path = path.slice(basePath.length) || '/';
|
|
81
|
-
}
|
|
82
|
-
return path;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const [pathname, setPathname] = useState(getPathname);
|
|
86
|
-
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
const handler = () => setPathname(getPathname());
|
|
89
|
-
window.addEventListener('popstate', handler);
|
|
90
|
-
return () => window.removeEventListener('popstate', handler);
|
|
91
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
92
|
-
}, [basePath]);
|
|
93
|
-
|
|
94
|
-
return useMemo(() => {
|
|
95
|
-
if (patternRef.current) {
|
|
96
|
-
return extractParams(pathname, patternRef.current) as T;
|
|
97
|
-
}
|
|
98
|
-
return extractAllSegments(pathname) as T;
|
|
99
|
-
}, [pathname]);
|
|
100
|
-
}
|