@ttt-productions/upload-ui 0.7.6 → 0.7.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { type ReactNode, type Ref } from 'react';
|
|
2
|
-
import type { MediaOriginSpec, UploadState } from '@ttt-productions/media-schemas';
|
|
2
|
+
import type { ClientMediaClaim, MediaOriginSpec, UploadState } from '@ttt-productions/media-schemas';
|
|
3
3
|
export interface DeferredUploadFormShellHandle {
|
|
4
4
|
submit: () => void;
|
|
5
5
|
}
|
|
@@ -14,7 +14,11 @@ export interface DeferredUploadFormShellProps<TVariables extends {
|
|
|
14
14
|
mutateAsync: (vars: TVariables) => Promise<TResult>;
|
|
15
15
|
isPending: boolean;
|
|
16
16
|
};
|
|
17
|
-
buildVariables: (file: File | null, onProgress: (s: UploadState | null) => void, signal: AbortSignal
|
|
17
|
+
buildVariables: (file: File | null, onProgress: (s: UploadState | null) => void, signal: AbortSignal,
|
|
18
|
+
/** The untrusted claim MediaInput emitted with the selected file (advisory;
|
|
19
|
+
* the server byte inspection is the only classification authority). Rides
|
|
20
|
+
* and clears with the file — additive 4th arg, existing callers unchanged. */
|
|
21
|
+
claim?: ClientMediaClaim) => TVariables;
|
|
18
22
|
onSuccess?: (result: TResult) => void;
|
|
19
23
|
onError?: (err: unknown) => void;
|
|
20
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deferred-upload-form-shell.d.ts","sourceRoot":"","sources":["../../src/react/deferred-upload-form-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,GAAG,EACT,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"deferred-upload-form-shell.d.ts","sourceRoot":"","sources":["../../src/react/deferred-upload-form-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,GAAG,EACT,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAErG,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B,CAC3C,UAAU,SAAS;IACjB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,EACD,OAAO,GAAG,OAAO;IAEjB;wFACoF;IACpF,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE;QACR,WAAW,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;QACpD,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,cAAc,EAAE,CACd,IAAI,EAAE,IAAI,GAAG,IAAI,EACjB,UAAU,EAAE,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,EAC3C,MAAM,EAAE,WAAW;IACnB;;mFAE+E;IAC/E,KAAK,CAAC,EAAE,gBAAgB,KACrB,UAAU,CAAC;IAChB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAyGD,eAAO,MAAM,uBAAuB,EAA+C,CACjF,UAAU,SAAS;IACjB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,EACD,OAAO,GAAG,OAAO,EAEjB,KAAK,EAAE,4BAA4B,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,GAAG,CAAC,6BAA6B,CAAC,CAAA;CAAE,KACpG,KAAK,CAAC,YAAY,CAAC"}
|
|
@@ -5,16 +5,19 @@ import { MediaInput } from '@ttt-productions/file-input/react';
|
|
|
5
5
|
function DeferredUploadFormShellInner(props, ref) {
|
|
6
6
|
const { spec, mutation, buildVariables, onSuccess, onError, onFileChange, onBeforeSelect, children, } = props;
|
|
7
7
|
const [selectedFile, setSelectedFile] = useState(null);
|
|
8
|
+
const [selectedClaim, setSelectedClaim] = useState(undefined);
|
|
8
9
|
const [uploadState, setUploadState] = useState(null);
|
|
9
10
|
const fileAreaRef = useRef(null);
|
|
10
11
|
const abortControllerRef = useRef(null);
|
|
11
12
|
const handleMediaChange = useCallback((payload) => {
|
|
12
13
|
const next = payload.error ? null : (payload.file ?? null);
|
|
13
14
|
setSelectedFile(next);
|
|
15
|
+
setSelectedClaim(next ? payload.claim : undefined);
|
|
14
16
|
onFileChange?.(next);
|
|
15
17
|
}, [onFileChange]);
|
|
16
18
|
const handleClear = useCallback(() => {
|
|
17
19
|
setSelectedFile(null);
|
|
20
|
+
setSelectedClaim(undefined);
|
|
18
21
|
onFileChange?.(null);
|
|
19
22
|
}, [onFileChange]);
|
|
20
23
|
const handleCancel = useCallback(() => {
|
|
@@ -29,9 +32,10 @@ function DeferredUploadFormShellInner(props, ref) {
|
|
|
29
32
|
const controller = new AbortController();
|
|
30
33
|
abortControllerRef.current = controller;
|
|
31
34
|
try {
|
|
32
|
-
const result = await mutation.mutateAsync(buildVariables(selectedFile, setUploadState, controller.signal));
|
|
35
|
+
const result = await mutation.mutateAsync(buildVariables(selectedFile, setUploadState, controller.signal, selectedClaim));
|
|
33
36
|
setUploadState(null);
|
|
34
37
|
setSelectedFile(null);
|
|
38
|
+
setSelectedClaim(undefined);
|
|
35
39
|
onFileChange?.(null);
|
|
36
40
|
onSuccess?.(result);
|
|
37
41
|
}
|
|
@@ -40,6 +44,7 @@ function DeferredUploadFormShellInner(props, ref) {
|
|
|
40
44
|
// User-initiated cancel: swallow the AbortError, reset selection, do not fire onError.
|
|
41
45
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
42
46
|
setSelectedFile(null);
|
|
47
|
+
setSelectedClaim(undefined);
|
|
43
48
|
onFileChange?.(null);
|
|
44
49
|
return;
|
|
45
50
|
}
|
|
@@ -48,7 +53,7 @@ function DeferredUploadFormShellInner(props, ref) {
|
|
|
48
53
|
finally {
|
|
49
54
|
abortControllerRef.current = null;
|
|
50
55
|
}
|
|
51
|
-
}, [mutation, selectedFile, buildVariables, onSuccess, onError, onFileChange]);
|
|
56
|
+
}, [mutation, selectedFile, selectedClaim, buildVariables, onSuccess, onError, onFileChange]);
|
|
52
57
|
useImperativeHandle(ref, () => ({ submit: () => { void handleSubmit(); } }), [handleSubmit]);
|
|
53
58
|
return (_jsxs(_Fragment, { children: [children, _jsx("div", { ref: fileAreaRef, tabIndex: -1, "aria-live": "polite", className: "outline-none", children: _jsx(MediaInput, { spec: spec, selectedFile: selectedFile, uploadState: uploadState, isLoading: mutation.isPending, disabled: mutation.isPending, onChange: handleMediaChange, onClear: handleClear, onCancel: handleCancel, onBeforeSelect: onBeforeSelect }) })] }));
|
|
54
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deferred-upload-form-shell.js","sourceRoot":"","sources":["../../src/react/deferred-upload-form-shell.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAc,EACZ,QAAQ,EACR,MAAM,EACN,WAAW,EACX,mBAAmB,EACnB,UAAU,GAGX,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"deferred-upload-form-shell.js","sourceRoot":"","sources":["../../src/react/deferred-upload-form-shell.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport React, {\n useState,\n useRef,\n useCallback,\n useImperativeHandle,\n forwardRef,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { MediaInput } from '@ttt-productions/file-input/react';\nimport type { MediaInputChangePayload } from '@ttt-productions/file-input';\nimport type { ClientMediaClaim, MediaOriginSpec, UploadState } from '@ttt-productions/media-schemas';\n\nexport interface DeferredUploadFormShellHandle {\n submit: () => void;\n}\n\nexport interface DeferredUploadFormShellProps<\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n> {\n /** The media-origin spec describing accept patterns, size caps, and label.\n * Consumers pass this directly — the shell does not look it up from a registry. */\n spec: MediaOriginSpec;\n mutation: {\n mutateAsync: (vars: TVariables) => Promise<TResult>;\n isPending: boolean;\n };\n buildVariables: (\n file: File | null,\n onProgress: (s: UploadState | null) => void,\n signal: AbortSignal,\n /** The untrusted claim MediaInput emitted with the selected file (advisory;\n * the server byte inspection is the only classification authority). Rides\n * and clears with the file — additive 4th arg, existing callers unchanged. */\n claim?: ClientMediaClaim,\n ) => TVariables;\n onSuccess?: (result: TResult) => void;\n onError?: (err: unknown) => void;\n /**\n * Optional callback fired whenever the shell's internal selectedFile state\n * changes — on user select, on user clear, and on the post-success internal\n * reset. Forms typically use this to mirror selectedFile externally so they\n * can compute their own submit-button `disabled` flag (e.g. require-a-file\n * submit gating).\n */\n onFileChange?: (file: File | null) => void;\n /**\n * Optional guard forwarded to MediaInput, fired before the picker/capture\n * opens for any action. Return false (or a Promise resolving false) to abort\n * — e.g. an email-verification gate. See MediaInputProps.onBeforeSelect.\n */\n onBeforeSelect?: () => boolean | Promise<boolean>;\n children?: ReactNode;\n}\n\nfunction DeferredUploadFormShellInner<\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n>(\n props: DeferredUploadFormShellProps<TVariables, TResult>,\n ref: Ref<DeferredUploadFormShellHandle>,\n) {\n const {\n spec,\n mutation,\n buildVariables,\n onSuccess,\n onError,\n onFileChange,\n onBeforeSelect,\n children,\n } = props;\n\n const [selectedFile, setSelectedFile] = useState<File | null>(null);\n const [selectedClaim, setSelectedClaim] = useState<ClientMediaClaim | undefined>(undefined);\n const [uploadState, setUploadState] = useState<UploadState | null>(null);\n const fileAreaRef = useRef<HTMLDivElement>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const handleMediaChange = useCallback((payload: MediaInputChangePayload) => {\n const next = payload.error ? null : (payload.file ?? null);\n setSelectedFile(next);\n setSelectedClaim(next ? payload.claim : undefined);\n onFileChange?.(next);\n }, [onFileChange]);\n\n const handleClear = useCallback(() => {\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n }, [onFileChange]);\n\n const handleCancel = useCallback(() => {\n abortControllerRef.current?.abort();\n }, []);\n\n const handleSubmit = useCallback(async () => {\n if (mutation.isPending) return;\n if (selectedFile) {\n fileAreaRef.current?.focus();\n }\n const controller = new AbortController();\n abortControllerRef.current = controller;\n try {\n const result = await mutation.mutateAsync(\n buildVariables(selectedFile, setUploadState, controller.signal, selectedClaim),\n );\n setUploadState(null);\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n onSuccess?.(result);\n } catch (err) {\n setUploadState(null);\n // User-initiated cancel: swallow the AbortError, reset selection, do not fire onError.\n if (err instanceof Error && err.name === 'AbortError') {\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n return;\n }\n onError?.(err);\n } finally {\n abortControllerRef.current = null;\n }\n }, [mutation, selectedFile, selectedClaim, buildVariables, onSuccess, onError, onFileChange]);\n\n useImperativeHandle(ref, () => ({ submit: () => { void handleSubmit(); } }), [handleSubmit]);\n\n return (\n <>\n {children}\n\n <div\n ref={fileAreaRef}\n tabIndex={-1}\n aria-live=\"polite\"\n className=\"outline-none\"\n >\n <MediaInput\n spec={spec}\n selectedFile={selectedFile}\n uploadState={uploadState}\n isLoading={mutation.isPending}\n disabled={mutation.isPending}\n onChange={handleMediaChange}\n onClear={handleClear}\n onCancel={handleCancel}\n onBeforeSelect={onBeforeSelect}\n />\n </div>\n </>\n );\n}\n\nexport const DeferredUploadFormShell = forwardRef(DeferredUploadFormShellInner) as <\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n>(\n props: DeferredUploadFormShellProps<TVariables, TResult> & { ref?: Ref<DeferredUploadFormShellHandle> },\n) => React.ReactElement;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/upload-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Upload UI primitives: local upload guard provider, useGuardedUpload helper, DeferredUploadFormShell for forms that defer Firebase Storage upload until form submit.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"prepublishOnly": "npm run clean && npm run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ttt-productions/file-input": "^0.11.
|
|
43
|
+
"@ttt-productions/file-input": "^0.11.9",
|
|
44
44
|
"@ttt-productions/media-schemas": "^0.14.5",
|
|
45
45
|
"@ttt-productions/ui-core": "^0.11.12",
|
|
46
46
|
"@ttt-productions/upload-core": "^0.7.3"
|