@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 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