@marktiderman/genesis-forms-web 0.5.0 → 0.6.0
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 +22 -0
- package/dist/index.d.ts +46 -2
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -159,6 +159,28 @@ sends (it says "Wait for the photo to finish uploading."). Without `onUpload`, o
|
|
|
159
159
|
`UploadContext` around the form, a photo field says photos can't be added here. `photoFileProblem`,
|
|
160
160
|
`photoFileType` and `photoAccept` are the renderer's own checks, for a custom one.
|
|
161
161
|
|
|
162
|
+
`signedUploadHost` is an upload host for genesis-forms/server's `createUploadHandler`: for each file
|
|
163
|
+
it asks your endpoint for a signed upload, sends the bytes with `send`, and resolves to the
|
|
164
|
+
server's reference. A refusal rejects with the server's words. `url(ref)` reads a stored file
|
|
165
|
+
through your `readUrl`, null when it can't (for thumbnails in a staff view):
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
const host = signedUploadHost({
|
|
169
|
+
endpoint: `/api/upload/${slug}`,
|
|
170
|
+
claim: () => (editToken ? { edit: editToken } : { attemptId }), // read at each upload
|
|
171
|
+
// Supabase Storage; without `send`, the file is PUT to `upload.url`.
|
|
172
|
+
send: async (file, upload, contentType) => {
|
|
173
|
+
const { error } = await supabase.storage
|
|
174
|
+
.from(upload.bucket!)
|
|
175
|
+
.uploadToSignedUrl(upload.path, upload.token, file, { contentType });
|
|
176
|
+
if (error) throw error;
|
|
177
|
+
},
|
|
178
|
+
readUrl: async (ref) => signedReadUrl(ref), // your storage, or null
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
<Form form={feedback} onUpload={host.upload} onSubmit={send} />;
|
|
182
|
+
```
|
|
183
|
+
|
|
162
184
|
A **`calculated`** field shows the value the engine worked out from other answers, read-only, in an
|
|
163
185
|
`<output>` named by the question that announces changes politely. It follows the field's `format`
|
|
164
186
|
and `decimals`, and shows a dash until there is a value.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, ComponentType } from 'react';
|
|
3
|
-
import { FormDefinition, FormEngine, CreateFormOptions, HostOptions, LinkParams, FormState, FormProgress, FieldBlock, PhotoRef } from '@marktiderman/genesis-forms';
|
|
3
|
+
import { FormDefinition, FormEngine, CreateFormOptions, HostOptions, LinkParams, FormState, FormProgress, FieldBlock, PhotoRef, SignedUpload } from '@marktiderman/genesis-forms';
|
|
4
4
|
import * as _marktiderman_genesis_forms_react from '@marktiderman/genesis-forms-react';
|
|
5
5
|
import { RendererRegistry, FieldRendererProps, RendererEntry } from '@marktiderman/genesis-forms-react';
|
|
6
6
|
|
|
@@ -298,6 +298,8 @@ declare const defaultRegistry: _marktiderman_genesis_forms_react.RendererRegistr
|
|
|
298
298
|
*/
|
|
299
299
|
interface UploadHost {
|
|
300
300
|
upload(file: File, field: FieldBlock): Promise<PhotoRef>;
|
|
301
|
+
/** A link to read a stored file (the host's storage signs it), or null when it can't be read. */
|
|
302
|
+
url?(ref: PhotoRef, field?: FieldBlock): Promise<string | null>;
|
|
301
303
|
}
|
|
302
304
|
/** Provide it around `<Form>` (or pass `onUpload`) so `photo` fields can store files. */
|
|
303
305
|
declare const UploadContext: react.Context<UploadHost | null>;
|
|
@@ -320,6 +322,48 @@ declare function photoFileProblem(field: FieldBlock, file: {
|
|
|
320
322
|
/** The `accept` attribute for a field: its MIME types, plus HEIC's file names for browsers that give it no type. */
|
|
321
323
|
declare function photoAccept(field: FieldBlock): string;
|
|
322
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Who is uploading: a new response's visit (`attemptId`, a UUID the page keeps for the
|
|
327
|
+
* visit), or an edit link's token while changing a sent response.
|
|
328
|
+
*/
|
|
329
|
+
type UploadClaim = {
|
|
330
|
+
attemptId: string;
|
|
331
|
+
} | {
|
|
332
|
+
edit: string;
|
|
333
|
+
};
|
|
334
|
+
interface SignedUploadHostOptions {
|
|
335
|
+
/** The app's upload endpoint (`createUploadHandler`'s route), or one per field. */
|
|
336
|
+
endpoint: string | ((field: FieldBlock) => string);
|
|
337
|
+
/** Who is uploading, read at each upload. */
|
|
338
|
+
claim: UploadClaim | (() => UploadClaim);
|
|
339
|
+
/**
|
|
340
|
+
* Sends the bytes where the server signed. Default: PUT the file to `upload.url`
|
|
341
|
+
* (with its type), which a store that signs plain URLs sets. Supabase Storage:
|
|
342
|
+
* `supabase.storage.from(upload.bucket).uploadToSignedUrl(upload.path, upload.token, file, { contentType })`.
|
|
343
|
+
* A throw (or rejection) is the upload failing.
|
|
344
|
+
*/
|
|
345
|
+
send?: ((file: File, upload: SignedUpload, contentType: string) => Promise<void>) | undefined;
|
|
346
|
+
/**
|
|
347
|
+
* A link to read a stored file, for thumbnails and staff views: the app's storage signs
|
|
348
|
+
* it. Null when the reader can't open it. Without it, `url` resolves null.
|
|
349
|
+
*/
|
|
350
|
+
readUrl?: ((ref: PhotoRef, field?: FieldBlock) => Promise<string | null>) | undefined;
|
|
351
|
+
/** Default the global `fetch`. */
|
|
352
|
+
fetch?: typeof fetch | undefined;
|
|
353
|
+
}
|
|
354
|
+
/** An `UploadHost` that always has `url`. */
|
|
355
|
+
interface SignedUploadHost extends UploadHost {
|
|
356
|
+
url(ref: PhotoRef, field?: FieldBlock): Promise<string | null>;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The browser half of the upload handshake (genesis-forms/server's `createUploadHandler`):
|
|
360
|
+
* for each file it asks `endpoint` for a signed upload (`UploadRequest` as JSON), sends
|
|
361
|
+
* the bytes with `send`, and resolves to the reference the server handed back, which is
|
|
362
|
+
* all the answer keeps. A refusal rejects with the server's words. Pass it as `<Form
|
|
363
|
+
* onUpload={host.upload}>` or in an `UploadContext`.
|
|
364
|
+
*/
|
|
365
|
+
declare function signedUploadHost(options: SignedUploadHostOptions): SignedUploadHost;
|
|
366
|
+
|
|
323
367
|
/**
|
|
324
368
|
* The video or audio a form is about, as the host app plays it. `media-notes`
|
|
325
369
|
* fields read it: with `currentTime` the add row starts at the current moment
|
|
@@ -370,4 +414,4 @@ declare function hideQueryOnLoad(win: HiddenQueryWindow, isReload: boolean, now?
|
|
|
370
414
|
/** Whether this page load was a reload (Navigation Timing), in the browser; false elsewhere. */
|
|
371
415
|
declare const wasReload: () => boolean;
|
|
372
416
|
|
|
373
|
-
export { type BrandSlotProps, CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type FormTheme, HIDDEN_QUERY_KEEP_MS, type HeaderSlotProps, type HiddenQueryWindow, type MediaTime, MediaTimeContext, type NavigationSlotProps, PROGRESS_DELAY_MS, type ProgressSlotProps, SourceTextContext, type ThemeColors, UPLOAD_WAIT_MESSAGE, UploadContext, type UploadHost, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
|
|
417
|
+
export { type BrandSlotProps, CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, type EndingSlotProps, type ErrorSummaryItem, type ErrorSummarySlotProps, FieldChrome, type FieldChromeProps, Form, type FormBrand, type FormPresentation, type FormPresentationValue, type FormProps, type FormSlotMap, type FormTheme, HIDDEN_QUERY_KEEP_MS, type HeaderSlotProps, type HiddenQueryWindow, type MediaTime, MediaTimeContext, type NavigationSlotProps, PROGRESS_DELAY_MS, type ProgressSlotProps, type SignedUploadHost, type SignedUploadHostOptions, SourceTextContext, type ThemeColors, UPLOAD_WAIT_MESSAGE, type UploadClaim, UploadContext, type UploadHost, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, signedUploadHost, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
|
package/dist/index.js
CHANGED
|
@@ -2658,6 +2658,44 @@ function FormBody({
|
|
|
2658
2658
|
}
|
|
2659
2659
|
var classicSection = "rounded-xl border bg-card px-5 py-6 text-card-foreground sm:px-8 has-[[aria-invalid=true]]:border-destructive";
|
|
2660
2660
|
|
|
2661
|
+
// src/signed-upload.ts
|
|
2662
|
+
async function putToUrl(file, upload, contentType, fetcher) {
|
|
2663
|
+
if (!upload.url) throw new Error("The server signed no URL to send the file to; pass `send`.");
|
|
2664
|
+
const res = await fetcher(upload.url, { method: "PUT", headers: { "content-type": contentType }, body: file });
|
|
2665
|
+
if (!res.ok) throw new Error(`The file wasn't stored (${res.status}).`);
|
|
2666
|
+
}
|
|
2667
|
+
function signedUploadHost(options) {
|
|
2668
|
+
const fetcher = (...args) => (options.fetch ?? fetch)(...args);
|
|
2669
|
+
return {
|
|
2670
|
+
async upload(file, field) {
|
|
2671
|
+
const type = photoFileType(file);
|
|
2672
|
+
const claim = typeof options.claim === "function" ? options.claim() : options.claim;
|
|
2673
|
+
const endpoint = typeof options.endpoint === "function" ? options.endpoint(field) : options.endpoint;
|
|
2674
|
+
const request = { fieldId: field.id, name: file.name, type, size: file.size, ...claim };
|
|
2675
|
+
const response = await fetcher(endpoint, {
|
|
2676
|
+
method: "POST",
|
|
2677
|
+
headers: { "content-type": "application/json" },
|
|
2678
|
+
body: JSON.stringify(request)
|
|
2679
|
+
});
|
|
2680
|
+
const body = await response.json().catch(() => ({}));
|
|
2681
|
+
if (!response.ok || !body.ref || !body.upload) {
|
|
2682
|
+
throw new Error(typeof body.error === "string" ? body.error : `The photo wasn't accepted (${response.status}).`);
|
|
2683
|
+
}
|
|
2684
|
+
if (options.send) await options.send(file, body.upload, type);
|
|
2685
|
+
else await putToUrl(file, body.upload, type, fetcher);
|
|
2686
|
+
return body.ref;
|
|
2687
|
+
},
|
|
2688
|
+
async url(ref, field) {
|
|
2689
|
+
if (!options.readUrl) return null;
|
|
2690
|
+
try {
|
|
2691
|
+
return await options.readUrl(ref, field);
|
|
2692
|
+
} catch {
|
|
2693
|
+
return null;
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2661
2699
|
// src/clean-url.ts
|
|
2662
2700
|
var KEY_PREFIX = "form-query:";
|
|
2663
2701
|
var HIDDEN_QUERY_KEEP_MS = 30 * 60 * 1e3;
|
|
@@ -2689,6 +2727,6 @@ function nativeReplaceState(history) {
|
|
|
2689
2727
|
}
|
|
2690
2728
|
var wasReload = () => typeof performance !== "undefined" && performance.getEntriesByType?.("navigation")[0]?.type === "reload";
|
|
2691
2729
|
|
|
2692
|
-
export { CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, FieldChrome, Form, HIDDEN_QUERY_KEEP_MS, MediaTimeContext, PROGRESS_DELAY_MS, SourceTextContext, UPLOAD_WAIT_MESSAGE, UploadContext, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
|
|
2730
|
+
export { CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, FieldChrome, Form, HIDDEN_QUERY_KEEP_MS, MediaTimeContext, PROGRESS_DELAY_MS, SourceTextContext, UPLOAD_WAIT_MESSAGE, UploadContext, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, signedUploadHost, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
|
|
2693
2731
|
//# sourceMappingURL=index.js.map
|
|
2694
2732
|
//# sourceMappingURL=index.js.map
|