@uploadista/react-native-core 0.0.20-beta.6 → 0.0.20-beta.7
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/dist/index.d.mts +747 -222
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/flow-primitives.tsx +840 -0
- package/src/components/index.ts +32 -0
- package/src/index.ts +29 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FlowUploadState,
|
|
3
|
+
FlowUploadStatus,
|
|
4
|
+
InputExecutionState,
|
|
5
|
+
} from "@uploadista/client-core";
|
|
6
|
+
import type { TypedOutput } from "@uploadista/core/flow";
|
|
7
|
+
import { createContext, type ReactNode, useCallback, useContext } from "react";
|
|
8
|
+
import {
|
|
9
|
+
type FlowInputMetadata,
|
|
10
|
+
type UseFlowOptions,
|
|
11
|
+
useFlow,
|
|
12
|
+
} from "../hooks/use-flow";
|
|
13
|
+
import { useUploadistaContext } from "../hooks/use-uploadista-context";
|
|
14
|
+
import type { FilePickResult } from "../types";
|
|
15
|
+
|
|
16
|
+
// Re-export types for convenience
|
|
17
|
+
export type {
|
|
18
|
+
FlowUploadState,
|
|
19
|
+
FlowUploadStatus,
|
|
20
|
+
InputExecutionState,
|
|
21
|
+
FlowInputMetadata,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ============ FLOW CONTEXT ============
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Context value provided by the Flow component root.
|
|
28
|
+
* Contains all flow state and actions.
|
|
29
|
+
*/
|
|
30
|
+
export interface FlowContextValue {
|
|
31
|
+
/** Current upload state */
|
|
32
|
+
state: FlowUploadState;
|
|
33
|
+
/** Discovered input nodes metadata (null until discovery completes) */
|
|
34
|
+
inputMetadata: FlowInputMetadata[] | null;
|
|
35
|
+
/** Current input values set via setInput() */
|
|
36
|
+
inputs: Record<string, unknown>;
|
|
37
|
+
/** Per-input execution state for multi-input flows */
|
|
38
|
+
inputStates: ReadonlyMap<string, InputExecutionState>;
|
|
39
|
+
|
|
40
|
+
/** Set an input value for a specific node */
|
|
41
|
+
setInput: (nodeId: string, value: unknown) => void;
|
|
42
|
+
/** Execute the flow with current inputs */
|
|
43
|
+
execute: () => Promise<void>;
|
|
44
|
+
/** Upload a single file through the flow */
|
|
45
|
+
upload: (file: FilePickResult) => Promise<void>;
|
|
46
|
+
/** Abort the current upload */
|
|
47
|
+
abort: () => void;
|
|
48
|
+
/** Reset the upload state and clear all inputs */
|
|
49
|
+
reset: () => void;
|
|
50
|
+
|
|
51
|
+
/** Whether an upload or flow execution is in progress */
|
|
52
|
+
isActive: boolean;
|
|
53
|
+
/** Whether the file is currently being uploaded */
|
|
54
|
+
isUploadingFile: boolean;
|
|
55
|
+
/** Whether the flow is currently processing */
|
|
56
|
+
isProcessing: boolean;
|
|
57
|
+
/** Whether the hook is discovering flow inputs */
|
|
58
|
+
isDiscoveringInputs: boolean;
|
|
59
|
+
|
|
60
|
+
/** Pick a file and set it for a specific input node */
|
|
61
|
+
pickFileForInput: (nodeId: string) => Promise<void>;
|
|
62
|
+
/** Pick a file and start upload immediately (single-file flows) */
|
|
63
|
+
pickAndUpload: () => Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const FlowContext = createContext<FlowContextValue | null>(null);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook to access flow context from within a Flow component.
|
|
70
|
+
* @throws Error if used outside of a Flow component
|
|
71
|
+
*/
|
|
72
|
+
export function useFlowContext(): FlowContextValue {
|
|
73
|
+
const context = useContext(FlowContext);
|
|
74
|
+
if (!context) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"useFlowContext must be used within a <Flow> component. " +
|
|
77
|
+
'Wrap your component tree with <Flow flowId="..." storageId="...">',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return context;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============ FLOW INPUT CONTEXT ============
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Context value for a specific input node within a Flow.
|
|
87
|
+
*/
|
|
88
|
+
export interface FlowInputContextValue {
|
|
89
|
+
/** Input node ID */
|
|
90
|
+
nodeId: string;
|
|
91
|
+
/** Input metadata from flow discovery */
|
|
92
|
+
metadata: FlowInputMetadata;
|
|
93
|
+
/** Current value for this input */
|
|
94
|
+
value: unknown;
|
|
95
|
+
/** Set the value for this input */
|
|
96
|
+
setValue: (value: unknown) => void;
|
|
97
|
+
/** Per-input execution state (if available) */
|
|
98
|
+
state: InputExecutionState | undefined;
|
|
99
|
+
/** Pick a file for this input */
|
|
100
|
+
pickFile: () => Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const FlowInputContext = createContext<FlowInputContextValue | null>(null);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Hook to access flow input context from within a Flow.Input component.
|
|
107
|
+
* @throws Error if used outside of a Flow.Input component
|
|
108
|
+
*/
|
|
109
|
+
export function useFlowInputContext(): FlowInputContextValue {
|
|
110
|
+
const context = useContext(FlowInputContext);
|
|
111
|
+
if (!context) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"useFlowInputContext must be used within a <Flow.Input> component. " +
|
|
114
|
+
'Wrap your component with <Flow.Input nodeId="...">',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return context;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============ FLOW ROOT COMPONENT ============
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Render props for the Flow root component.
|
|
124
|
+
*/
|
|
125
|
+
export interface FlowRenderProps extends FlowContextValue {
|
|
126
|
+
/** Alias for execute() */
|
|
127
|
+
submit: () => Promise<void>;
|
|
128
|
+
/** Alias for abort() */
|
|
129
|
+
cancel: () => void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Props for the Flow root component.
|
|
134
|
+
*/
|
|
135
|
+
export interface FlowProps {
|
|
136
|
+
/** Flow ID to execute */
|
|
137
|
+
flowId: string;
|
|
138
|
+
/** Storage ID for file uploads */
|
|
139
|
+
storageId: string;
|
|
140
|
+
/** Optional output node ID to wait for */
|
|
141
|
+
outputNodeId?: string;
|
|
142
|
+
/** Optional metadata to include with the flow execution */
|
|
143
|
+
metadata?: Record<string, string>;
|
|
144
|
+
/** Called when flow completes successfully */
|
|
145
|
+
onSuccess?: (outputs: TypedOutput[]) => void;
|
|
146
|
+
/** Called when flow fails */
|
|
147
|
+
onError?: (error: Error) => void;
|
|
148
|
+
/** Called on upload progress */
|
|
149
|
+
onProgress?: (
|
|
150
|
+
progress: number,
|
|
151
|
+
bytesUploaded: number,
|
|
152
|
+
totalBytes: number | null,
|
|
153
|
+
) => void;
|
|
154
|
+
/** Called when flow completes with all outputs */
|
|
155
|
+
onFlowComplete?: (outputs: TypedOutput[]) => void;
|
|
156
|
+
/** Children to render (can be render function or ReactNode) */
|
|
157
|
+
children: ReactNode | ((props: FlowRenderProps) => ReactNode);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Root component for flow-based uploads on React Native.
|
|
162
|
+
* Provides context for all Flow sub-components.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```tsx
|
|
166
|
+
* <Flow flowId="image-optimizer" storageId="s3" onSuccess={handleSuccess}>
|
|
167
|
+
* <Flow.Inputs>
|
|
168
|
+
* {({ inputs, isLoading }) => (
|
|
169
|
+
* inputs.map(input => (
|
|
170
|
+
* <Flow.Input key={input.nodeId} nodeId={input.nodeId}>
|
|
171
|
+
* {({ metadata, pickFile }) => (
|
|
172
|
+
* <Button onPress={pickFile} title={metadata.nodeName} />
|
|
173
|
+
* )}
|
|
174
|
+
* </Flow.Input>
|
|
175
|
+
* ))
|
|
176
|
+
* )}
|
|
177
|
+
* </Flow.Inputs>
|
|
178
|
+
* <Flow.Submit>
|
|
179
|
+
* <Text>Process</Text>
|
|
180
|
+
* </Flow.Submit>
|
|
181
|
+
* </Flow>
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
function FlowRoot({
|
|
185
|
+
flowId,
|
|
186
|
+
storageId,
|
|
187
|
+
outputNodeId,
|
|
188
|
+
metadata,
|
|
189
|
+
onSuccess,
|
|
190
|
+
onError,
|
|
191
|
+
onProgress,
|
|
192
|
+
onFlowComplete,
|
|
193
|
+
children,
|
|
194
|
+
}: FlowProps) {
|
|
195
|
+
const { fileSystemProvider } = useUploadistaContext();
|
|
196
|
+
|
|
197
|
+
const options: UseFlowOptions = {
|
|
198
|
+
flowId,
|
|
199
|
+
storageId,
|
|
200
|
+
outputNodeId,
|
|
201
|
+
metadata,
|
|
202
|
+
onSuccess,
|
|
203
|
+
onError,
|
|
204
|
+
onProgress,
|
|
205
|
+
onFlowComplete,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const flow = useFlow(options);
|
|
209
|
+
|
|
210
|
+
// Pick a file for a specific input node
|
|
211
|
+
const pickFileForInput = useCallback(
|
|
212
|
+
async (nodeId: string) => {
|
|
213
|
+
if (!fileSystemProvider?.pickDocument) {
|
|
214
|
+
throw new Error("File picker not available");
|
|
215
|
+
}
|
|
216
|
+
const result = await fileSystemProvider.pickDocument();
|
|
217
|
+
if (result.status === "success") {
|
|
218
|
+
flow.setInput(nodeId, result);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
[fileSystemProvider, flow],
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Pick a file and start upload immediately
|
|
225
|
+
const pickAndUpload = useCallback(async () => {
|
|
226
|
+
if (!fileSystemProvider?.pickDocument) {
|
|
227
|
+
throw new Error("File picker not available");
|
|
228
|
+
}
|
|
229
|
+
const result = await fileSystemProvider.pickDocument();
|
|
230
|
+
if (result.status === "success") {
|
|
231
|
+
await flow.upload(result);
|
|
232
|
+
}
|
|
233
|
+
}, [fileSystemProvider, flow]);
|
|
234
|
+
|
|
235
|
+
const contextValue: FlowContextValue = {
|
|
236
|
+
state: flow.state,
|
|
237
|
+
inputMetadata: flow.inputMetadata,
|
|
238
|
+
inputs: flow.inputs,
|
|
239
|
+
inputStates: flow.inputStates,
|
|
240
|
+
setInput: flow.setInput,
|
|
241
|
+
execute: flow.execute,
|
|
242
|
+
upload: flow.upload,
|
|
243
|
+
abort: flow.abort,
|
|
244
|
+
reset: flow.reset,
|
|
245
|
+
isActive: flow.isActive,
|
|
246
|
+
isUploadingFile: flow.isUploadingFile,
|
|
247
|
+
isProcessing: flow.isProcessing,
|
|
248
|
+
isDiscoveringInputs: flow.isDiscoveringInputs,
|
|
249
|
+
pickFileForInput,
|
|
250
|
+
pickAndUpload,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const renderProps: FlowRenderProps = {
|
|
254
|
+
...contextValue,
|
|
255
|
+
submit: flow.execute,
|
|
256
|
+
cancel: flow.abort,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<FlowContext.Provider value={contextValue}>
|
|
261
|
+
{typeof children === "function" ? children(renderProps) : children}
|
|
262
|
+
</FlowContext.Provider>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============ INPUTS DISCOVERY PRIMITIVE ============
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Render props for Flow.Inputs component.
|
|
270
|
+
*/
|
|
271
|
+
export interface FlowInputsRenderProps {
|
|
272
|
+
/** Discovered input metadata */
|
|
273
|
+
inputs: FlowInputMetadata[];
|
|
274
|
+
/** Whether inputs are still being discovered */
|
|
275
|
+
isLoading: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Props for Flow.Inputs component.
|
|
280
|
+
*/
|
|
281
|
+
export interface FlowInputsProps {
|
|
282
|
+
/** Render function receiving discovered inputs */
|
|
283
|
+
children: (props: FlowInputsRenderProps) => ReactNode;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Auto-discovers flow input nodes and provides them via render props.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```tsx
|
|
291
|
+
* <Flow.Inputs>
|
|
292
|
+
* {({ inputs, isLoading }) => (
|
|
293
|
+
* isLoading ? <ActivityIndicator /> : (
|
|
294
|
+
* inputs.map(input => (
|
|
295
|
+
* <Flow.Input key={input.nodeId} nodeId={input.nodeId}>
|
|
296
|
+
* ...
|
|
297
|
+
* </Flow.Input>
|
|
298
|
+
* ))
|
|
299
|
+
* )
|
|
300
|
+
* )}
|
|
301
|
+
* </Flow.Inputs>
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
function FlowInputs({ children }: FlowInputsProps) {
|
|
305
|
+
const flow = useFlowContext();
|
|
306
|
+
|
|
307
|
+
const renderProps: FlowInputsRenderProps = {
|
|
308
|
+
inputs: flow.inputMetadata ?? [],
|
|
309
|
+
isLoading: flow.isDiscoveringInputs,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return <>{children(renderProps)}</>;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ============ INPUT PRIMITIVE ============
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Props for Flow.Input component.
|
|
319
|
+
*/
|
|
320
|
+
export interface FlowInputProps {
|
|
321
|
+
/** Input node ID */
|
|
322
|
+
nodeId: string;
|
|
323
|
+
/** Children (can be render function or regular children) */
|
|
324
|
+
children: ReactNode | ((props: FlowInputContextValue) => ReactNode);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Scoped input context provider for a specific input node.
|
|
329
|
+
* Children can access input-specific state via useFlowInputContext().
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```tsx
|
|
333
|
+
* <Flow.Input nodeId="video-input">
|
|
334
|
+
* {({ metadata, value, pickFile }) => (
|
|
335
|
+
* <View>
|
|
336
|
+
* <Text>{metadata.nodeName}</Text>
|
|
337
|
+
* <Button onPress={pickFile} title="Select File" />
|
|
338
|
+
* {value && <Text>Selected: {value.data?.name}</Text>}
|
|
339
|
+
* </View>
|
|
340
|
+
* )}
|
|
341
|
+
* </Flow.Input>
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
function FlowInput({ nodeId, children }: FlowInputProps) {
|
|
345
|
+
const flow = useFlowContext();
|
|
346
|
+
|
|
347
|
+
const metadata = flow.inputMetadata?.find((m) => m.nodeId === nodeId);
|
|
348
|
+
|
|
349
|
+
if (!metadata) {
|
|
350
|
+
// Input not yet discovered or doesn't exist
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const pickFile = async () => {
|
|
355
|
+
await flow.pickFileForInput(nodeId);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const contextValue: FlowInputContextValue = {
|
|
359
|
+
nodeId,
|
|
360
|
+
metadata,
|
|
361
|
+
value: flow.inputs[nodeId],
|
|
362
|
+
setValue: (value) => flow.setInput(nodeId, value),
|
|
363
|
+
state: flow.inputStates.get(nodeId),
|
|
364
|
+
pickFile,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<FlowInputContext.Provider value={contextValue}>
|
|
369
|
+
{typeof children === "function" ? children(contextValue) : children}
|
|
370
|
+
</FlowInputContext.Provider>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============ INPUT FILE PICKER PRIMITIVE ============
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Render props for Flow.Input.FilePicker component.
|
|
378
|
+
*/
|
|
379
|
+
export interface FlowInputFilePickerRenderProps {
|
|
380
|
+
/** Current value for this input */
|
|
381
|
+
value: unknown;
|
|
382
|
+
/** Whether a file is selected */
|
|
383
|
+
hasFile: boolean;
|
|
384
|
+
/** File name (if value is FilePickResult) */
|
|
385
|
+
fileName: string | null;
|
|
386
|
+
/** File size in bytes (if value is FilePickResult) */
|
|
387
|
+
fileSize: number | null;
|
|
388
|
+
/** Per-input progress (if available) */
|
|
389
|
+
progress: number;
|
|
390
|
+
/** Per-input status (if available) */
|
|
391
|
+
status: string;
|
|
392
|
+
/** Open file picker */
|
|
393
|
+
pickFile: () => Promise<void>;
|
|
394
|
+
/** Clear the input value */
|
|
395
|
+
clear: () => void;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Props for Flow.Input.FilePicker component.
|
|
400
|
+
*/
|
|
401
|
+
export interface FlowInputFilePickerProps {
|
|
402
|
+
/** Render function receiving file picker state */
|
|
403
|
+
children: (props: FlowInputFilePickerRenderProps) => ReactNode;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* File picker for a specific input within a Flow.Input.
|
|
408
|
+
* Sets the input value but does NOT trigger upload until Flow.Submit is pressed.
|
|
409
|
+
*/
|
|
410
|
+
function FlowInputFilePicker({ children }: FlowInputFilePickerProps) {
|
|
411
|
+
const input = useFlowInputContext();
|
|
412
|
+
|
|
413
|
+
// Check if value is a FilePickResult
|
|
414
|
+
const fileResult = input.value as FilePickResult | undefined;
|
|
415
|
+
const hasFile = fileResult?.status === "success";
|
|
416
|
+
const fileName = hasFile ? (fileResult?.data?.name ?? null) : null;
|
|
417
|
+
const fileSize = hasFile ? (fileResult?.data?.size ?? null) : null;
|
|
418
|
+
|
|
419
|
+
const renderProps: FlowInputFilePickerRenderProps = {
|
|
420
|
+
value: input.value,
|
|
421
|
+
hasFile,
|
|
422
|
+
fileName,
|
|
423
|
+
fileSize,
|
|
424
|
+
progress: input.state?.progress ?? 0,
|
|
425
|
+
status: input.state?.status ?? "idle",
|
|
426
|
+
pickFile: input.pickFile,
|
|
427
|
+
clear: () => input.setValue(undefined),
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
return <>{children(renderProps)}</>;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============ INPUT PREVIEW PRIMITIVE ============
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Render props for Flow.Input.Preview component.
|
|
437
|
+
*/
|
|
438
|
+
export interface FlowInputPreviewRenderProps {
|
|
439
|
+
/** Current value */
|
|
440
|
+
value: unknown;
|
|
441
|
+
/** Whether a file is selected */
|
|
442
|
+
hasFile: boolean;
|
|
443
|
+
/** Whether value is a URL string */
|
|
444
|
+
isUrl: boolean;
|
|
445
|
+
/** File name (if value is FilePickResult) */
|
|
446
|
+
fileName: string | null;
|
|
447
|
+
/** File size in bytes (if value is FilePickResult) */
|
|
448
|
+
fileSize: number | null;
|
|
449
|
+
/** File URI (if value is FilePickResult) */
|
|
450
|
+
fileUri: string | null;
|
|
451
|
+
/** Clear the input value */
|
|
452
|
+
clear: () => void;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Props for Flow.Input.Preview component.
|
|
457
|
+
*/
|
|
458
|
+
export interface FlowInputPreviewProps {
|
|
459
|
+
/** Render function receiving preview state */
|
|
460
|
+
children: (props: FlowInputPreviewRenderProps) => ReactNode;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Preview component for showing the selected value within a Flow.Input.
|
|
465
|
+
*/
|
|
466
|
+
function FlowInputPreview({ children }: FlowInputPreviewProps) {
|
|
467
|
+
const input = useFlowInputContext();
|
|
468
|
+
|
|
469
|
+
// Check if value is a FilePickResult
|
|
470
|
+
const fileResult = input.value as FilePickResult | undefined;
|
|
471
|
+
const hasFile = fileResult?.status === "success";
|
|
472
|
+
const isUrl =
|
|
473
|
+
typeof input.value === "string" && (input.value as string).length > 0;
|
|
474
|
+
|
|
475
|
+
const renderProps: FlowInputPreviewRenderProps = {
|
|
476
|
+
value: input.value,
|
|
477
|
+
hasFile,
|
|
478
|
+
isUrl,
|
|
479
|
+
fileName: hasFile ? (fileResult?.data?.name ?? null) : null,
|
|
480
|
+
fileSize: hasFile ? (fileResult?.data?.size ?? null) : null,
|
|
481
|
+
fileUri: hasFile ? (fileResult?.data?.uri ?? null) : null,
|
|
482
|
+
clear: () => input.setValue(undefined),
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
return <>{children(renderProps)}</>;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ============ PROGRESS PRIMITIVE ============
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Render props for Flow.Progress component.
|
|
492
|
+
*/
|
|
493
|
+
export interface FlowProgressRenderProps {
|
|
494
|
+
/** Progress percentage (0-100) */
|
|
495
|
+
progress: number;
|
|
496
|
+
/** Bytes uploaded so far */
|
|
497
|
+
bytesUploaded: number;
|
|
498
|
+
/** Total bytes to upload (null if unknown) */
|
|
499
|
+
totalBytes: number | null;
|
|
500
|
+
/** Current status */
|
|
501
|
+
status: FlowUploadStatus;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Props for Flow.Progress component.
|
|
506
|
+
*/
|
|
507
|
+
export interface FlowProgressProps {
|
|
508
|
+
/** Render function receiving progress state */
|
|
509
|
+
children: (props: FlowProgressRenderProps) => ReactNode;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Progress display component within a Flow.
|
|
514
|
+
*/
|
|
515
|
+
function FlowProgress({ children }: FlowProgressProps) {
|
|
516
|
+
const flow = useFlowContext();
|
|
517
|
+
|
|
518
|
+
const renderProps: FlowProgressRenderProps = {
|
|
519
|
+
progress: flow.state.progress,
|
|
520
|
+
bytesUploaded: flow.state.bytesUploaded,
|
|
521
|
+
totalBytes: flow.state.totalBytes,
|
|
522
|
+
status: flow.state.status,
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
return <>{children(renderProps)}</>;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ============ STATUS PRIMITIVE ============
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Render props for Flow.Status component.
|
|
532
|
+
*/
|
|
533
|
+
export interface FlowStatusRenderProps {
|
|
534
|
+
/** Current status */
|
|
535
|
+
status: FlowUploadStatus;
|
|
536
|
+
/** Current node being processed (if any) */
|
|
537
|
+
currentNodeName: string | null;
|
|
538
|
+
/** Current node type (if any) */
|
|
539
|
+
currentNodeType: string | null;
|
|
540
|
+
/** Error (if status is error) */
|
|
541
|
+
error: Error | null;
|
|
542
|
+
/** Job ID (if started) */
|
|
543
|
+
jobId: string | null;
|
|
544
|
+
/** Whether flow has started */
|
|
545
|
+
flowStarted: boolean;
|
|
546
|
+
/** Flow outputs (if completed) */
|
|
547
|
+
flowOutputs: TypedOutput[] | null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Props for Flow.Status component.
|
|
552
|
+
*/
|
|
553
|
+
export interface FlowStatusProps {
|
|
554
|
+
/** Render function receiving status state */
|
|
555
|
+
children: (props: FlowStatusRenderProps) => ReactNode;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Status display component within a Flow.
|
|
560
|
+
*/
|
|
561
|
+
function FlowStatus({ children }: FlowStatusProps) {
|
|
562
|
+
const flow = useFlowContext();
|
|
563
|
+
|
|
564
|
+
const renderProps: FlowStatusRenderProps = {
|
|
565
|
+
status: flow.state.status,
|
|
566
|
+
currentNodeName: flow.state.currentNodeName,
|
|
567
|
+
currentNodeType: flow.state.currentNodeType,
|
|
568
|
+
error: flow.state.error,
|
|
569
|
+
jobId: flow.state.jobId,
|
|
570
|
+
flowStarted: flow.state.flowStarted,
|
|
571
|
+
flowOutputs: flow.state.flowOutputs,
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
return <>{children(renderProps)}</>;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ============ ERROR PRIMITIVE ============
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Render props for Flow.Error component.
|
|
581
|
+
*/
|
|
582
|
+
export interface FlowErrorRenderProps {
|
|
583
|
+
/** Error object (null if no error) */
|
|
584
|
+
error: Error | null;
|
|
585
|
+
/** Whether there is an error */
|
|
586
|
+
hasError: boolean;
|
|
587
|
+
/** Error message */
|
|
588
|
+
message: string | null;
|
|
589
|
+
/** Reset the flow */
|
|
590
|
+
reset: () => void;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Props for Flow.Error component.
|
|
595
|
+
*/
|
|
596
|
+
export interface FlowErrorProps {
|
|
597
|
+
/** Render function receiving error state */
|
|
598
|
+
children: (props: FlowErrorRenderProps) => ReactNode;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Error display component within a Flow.
|
|
603
|
+
*/
|
|
604
|
+
function FlowError({ children }: FlowErrorProps) {
|
|
605
|
+
const flow = useFlowContext();
|
|
606
|
+
|
|
607
|
+
const renderProps: FlowErrorRenderProps = {
|
|
608
|
+
error: flow.state.error,
|
|
609
|
+
hasError: flow.state.status === "error",
|
|
610
|
+
message: flow.state.error?.message ?? null,
|
|
611
|
+
reset: flow.reset,
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
return <>{children(renderProps)}</>;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ============ ACTION PRIMITIVES ============
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Render props for Flow.Submit component.
|
|
621
|
+
*/
|
|
622
|
+
export interface FlowSubmitRenderProps {
|
|
623
|
+
/** Execute the flow */
|
|
624
|
+
submit: () => Promise<void>;
|
|
625
|
+
/** Whether the button should be disabled */
|
|
626
|
+
isDisabled: boolean;
|
|
627
|
+
/** Whether currently submitting */
|
|
628
|
+
isSubmitting: boolean;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Props for Flow.Submit component.
|
|
633
|
+
*/
|
|
634
|
+
export interface FlowSubmitProps {
|
|
635
|
+
/** Render function receiving submit state */
|
|
636
|
+
children: ReactNode | ((props: FlowSubmitRenderProps) => ReactNode);
|
|
637
|
+
/** Additional disabled state */
|
|
638
|
+
disabled?: boolean;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Submit primitive that executes the flow with current inputs.
|
|
643
|
+
* Provides render props for building custom submit buttons.
|
|
644
|
+
* Automatically disabled when uploading.
|
|
645
|
+
*/
|
|
646
|
+
function FlowSubmit({ children, disabled }: FlowSubmitProps) {
|
|
647
|
+
const flow = useFlowContext();
|
|
648
|
+
|
|
649
|
+
const renderProps: FlowSubmitRenderProps = {
|
|
650
|
+
submit: flow.execute,
|
|
651
|
+
isDisabled: disabled || flow.isActive || Object.keys(flow.inputs).length === 0,
|
|
652
|
+
isSubmitting: flow.isActive,
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
return <>{typeof children === "function" ? children(renderProps) : children}</>;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Render props for Flow.Cancel component.
|
|
660
|
+
*/
|
|
661
|
+
export interface FlowCancelRenderProps {
|
|
662
|
+
/** Cancel the flow */
|
|
663
|
+
cancel: () => void;
|
|
664
|
+
/** Whether the button should be disabled */
|
|
665
|
+
isDisabled: boolean;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Props for Flow.Cancel component.
|
|
670
|
+
*/
|
|
671
|
+
export interface FlowCancelProps {
|
|
672
|
+
/** Render function receiving cancel state */
|
|
673
|
+
children: ReactNode | ((props: FlowCancelRenderProps) => ReactNode);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Cancel primitive that aborts the current upload.
|
|
678
|
+
*/
|
|
679
|
+
function FlowCancel({ children }: FlowCancelProps) {
|
|
680
|
+
const flow = useFlowContext();
|
|
681
|
+
|
|
682
|
+
const renderProps: FlowCancelRenderProps = {
|
|
683
|
+
cancel: flow.abort,
|
|
684
|
+
isDisabled: !flow.isActive,
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
return <>{typeof children === "function" ? children(renderProps) : children}</>;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Render props for Flow.Reset component.
|
|
692
|
+
*/
|
|
693
|
+
export interface FlowResetRenderProps {
|
|
694
|
+
/** Reset the flow */
|
|
695
|
+
reset: () => void;
|
|
696
|
+
/** Whether the button should be disabled */
|
|
697
|
+
isDisabled: boolean;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Props for Flow.Reset component.
|
|
702
|
+
*/
|
|
703
|
+
export interface FlowResetProps {
|
|
704
|
+
/** Render function receiving reset state */
|
|
705
|
+
children: ReactNode | ((props: FlowResetRenderProps) => ReactNode);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Reset primitive that clears all inputs and resets to idle state.
|
|
710
|
+
*/
|
|
711
|
+
function FlowReset({ children }: FlowResetProps) {
|
|
712
|
+
const flow = useFlowContext();
|
|
713
|
+
|
|
714
|
+
const renderProps: FlowResetRenderProps = {
|
|
715
|
+
reset: flow.reset,
|
|
716
|
+
isDisabled: flow.isActive,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
return <>{typeof children === "function" ? children(renderProps) : children}</>;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ============ QUICK UPLOAD PRIMITIVE ============
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Render props for Flow.QuickUpload component.
|
|
726
|
+
*/
|
|
727
|
+
export interface FlowQuickUploadRenderProps {
|
|
728
|
+
/** Whether currently uploading */
|
|
729
|
+
isUploading: boolean;
|
|
730
|
+
/** Progress percentage (0-100) */
|
|
731
|
+
progress: number;
|
|
732
|
+
/** Current status */
|
|
733
|
+
status: FlowUploadStatus;
|
|
734
|
+
/** Pick a file and start upload immediately */
|
|
735
|
+
pickAndUpload: () => Promise<void>;
|
|
736
|
+
/** Abort the current upload */
|
|
737
|
+
abort: () => void;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Props for Flow.QuickUpload component.
|
|
742
|
+
*/
|
|
743
|
+
export interface FlowQuickUploadProps {
|
|
744
|
+
/** Render function receiving quick upload state */
|
|
745
|
+
children: (props: FlowQuickUploadRenderProps) => ReactNode;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Quick upload component for single-file flows.
|
|
750
|
+
* Picks a file and starts upload immediately.
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```tsx
|
|
754
|
+
* <Flow.QuickUpload>
|
|
755
|
+
* {({ isUploading, progress, pickAndUpload, abort }) => (
|
|
756
|
+
* <Button
|
|
757
|
+
* onPress={isUploading ? abort : pickAndUpload}
|
|
758
|
+
* title={isUploading ? `Uploading ${progress}%` : 'Upload File'}
|
|
759
|
+
* />
|
|
760
|
+
* )}
|
|
761
|
+
* </Flow.QuickUpload>
|
|
762
|
+
* ```
|
|
763
|
+
*/
|
|
764
|
+
function FlowQuickUpload({ children }: FlowQuickUploadProps) {
|
|
765
|
+
const flow = useFlowContext();
|
|
766
|
+
|
|
767
|
+
const renderProps: FlowQuickUploadRenderProps = {
|
|
768
|
+
isUploading: flow.isActive,
|
|
769
|
+
progress: flow.state.progress,
|
|
770
|
+
status: flow.state.status,
|
|
771
|
+
pickAndUpload: flow.pickAndUpload,
|
|
772
|
+
abort: flow.abort,
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
return <>{children(renderProps)}</>;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ============ COMPOUND COMPONENT EXPORT ============
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Flow compound component for flow-based file uploads on React Native.
|
|
782
|
+
*
|
|
783
|
+
* Provides a composable, headless API for building flow upload interfaces.
|
|
784
|
+
* All sub-components use render props for complete UI control.
|
|
785
|
+
*
|
|
786
|
+
* @example Quick Upload (Single File)
|
|
787
|
+
* ```tsx
|
|
788
|
+
* <Flow flowId="image-optimizer" storageId="s3" onSuccess={handleSuccess}>
|
|
789
|
+
* <Flow.QuickUpload>
|
|
790
|
+
* {({ isUploading, progress, pickAndUpload, abort }) => (
|
|
791
|
+
* <View>
|
|
792
|
+
* <Button
|
|
793
|
+
* onPress={isUploading ? abort : pickAndUpload}
|
|
794
|
+
* title={isUploading ? 'Cancel' : 'Upload Image'}
|
|
795
|
+
* />
|
|
796
|
+
* {isUploading && <Text>{progress}%</Text>}
|
|
797
|
+
* </View>
|
|
798
|
+
* )}
|
|
799
|
+
* </Flow.QuickUpload>
|
|
800
|
+
* </Flow>
|
|
801
|
+
* ```
|
|
802
|
+
*
|
|
803
|
+
* @example Multi-Input Flow
|
|
804
|
+
* ```tsx
|
|
805
|
+
* <Flow flowId="video-processor" storageId="s3">
|
|
806
|
+
* <Flow.Inputs>
|
|
807
|
+
* {({ inputs, isLoading }) => (
|
|
808
|
+
* isLoading ? <ActivityIndicator /> : inputs.map(input => (
|
|
809
|
+
* <Flow.Input key={input.nodeId} nodeId={input.nodeId}>
|
|
810
|
+
* {({ metadata, pickFile, value }) => (
|
|
811
|
+
* <View>
|
|
812
|
+
* <Text>{metadata.nodeName}</Text>
|
|
813
|
+
* <Button onPress={pickFile} title="Select File" />
|
|
814
|
+
* <Flow.Input.Preview>
|
|
815
|
+
* {({ hasFile, fileName }) => hasFile && <Text>{fileName}</Text>}
|
|
816
|
+
* </Flow.Input.Preview>
|
|
817
|
+
* </View>
|
|
818
|
+
* )}
|
|
819
|
+
* </Flow.Input>
|
|
820
|
+
* ))
|
|
821
|
+
* )}
|
|
822
|
+
* </Flow.Inputs>
|
|
823
|
+
* <CustomSubmitButton />
|
|
824
|
+
* </Flow>
|
|
825
|
+
* ```
|
|
826
|
+
*/
|
|
827
|
+
export const Flow = Object.assign(FlowRoot, {
|
|
828
|
+
Inputs: FlowInputs,
|
|
829
|
+
Input: Object.assign(FlowInput, {
|
|
830
|
+
FilePicker: FlowInputFilePicker,
|
|
831
|
+
Preview: FlowInputPreview,
|
|
832
|
+
}),
|
|
833
|
+
Progress: FlowProgress,
|
|
834
|
+
Status: FlowStatus,
|
|
835
|
+
Error: FlowError,
|
|
836
|
+
Submit: FlowSubmit,
|
|
837
|
+
Cancel: FlowCancel,
|
|
838
|
+
Reset: FlowReset,
|
|
839
|
+
QuickUpload: FlowQuickUpload,
|
|
840
|
+
});
|