@pipelex/mthds-form 0.23.2 → 0.24.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/dist/{chunk-O2VIVDHO.js → chunk-NSHGLM6A.js} +26 -3
- package/dist/chunk-NSHGLM6A.js.map +1 -0
- package/dist/{chunk-W5MYXDLO.js → chunk-UGKFWX7L.js} +282 -46
- package/dist/chunk-UGKFWX7L.js.map +1 -0
- package/dist/core/index.js +1 -1
- package/dist/core/sources.d.ts +40 -1
- package/dist/core/sources.js +1 -1
- package/dist/generative/index.js +2 -2
- package/dist/react/index.d.ts +29 -2
- package/dist/react/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-O2VIVDHO.js.map +0 -1
- package/dist/chunk-W5MYXDLO.js.map +0 -1
|
@@ -24,6 +24,29 @@ function addChosenFiles(held, chosen) {
|
|
|
24
24
|
}
|
|
25
25
|
return items;
|
|
26
26
|
}
|
|
27
|
+
function placeDroppedFiles(held, count, maxItemCount, isFree = () => true) {
|
|
28
|
+
const items = [...held ?? []];
|
|
29
|
+
const targets = [];
|
|
30
|
+
for (let index = 0; index < items.length && targets.length < count; index += 1) {
|
|
31
|
+
if (heldUrl(items[index]) === void 0 && isFree(index)) targets.push(index);
|
|
32
|
+
}
|
|
33
|
+
let appended = 0;
|
|
34
|
+
while (targets.length < count && (maxItemCount === void 0 || items.length < maxItemCount)) {
|
|
35
|
+
targets.push(items.length);
|
|
36
|
+
items.push(void 0);
|
|
37
|
+
appended += 1;
|
|
38
|
+
}
|
|
39
|
+
return { items, appended, targets, leftOut: count - targets.length };
|
|
40
|
+
}
|
|
41
|
+
function roomForDroppedFiles(held, maxItemCount, isFree = () => true) {
|
|
42
|
+
if (maxItemCount === void 0) return void 0;
|
|
43
|
+
const items = held ?? [];
|
|
44
|
+
let free = 0;
|
|
45
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
46
|
+
if (heldUrl(items[index]) === void 0 && isFree(index)) free += 1;
|
|
47
|
+
}
|
|
48
|
+
return free + Math.max(0, maxItemCount - items.length);
|
|
49
|
+
}
|
|
27
50
|
function chosenFilesAdded(held, chosen) {
|
|
28
51
|
const urls = heldUrls(held ?? []);
|
|
29
52
|
const before = urls.size;
|
|
@@ -36,6 +59,6 @@ function roomForChosenFiles(held, maxItemCount) {
|
|
|
36
59
|
return Math.max(0, maxItemCount - files);
|
|
37
60
|
}
|
|
38
61
|
|
|
39
|
-
export { addChosenFiles, chosenFilesAdded, roomForChosenFiles, sourceFileValue };
|
|
40
|
-
//# sourceMappingURL=chunk-
|
|
41
|
-
//# sourceMappingURL=chunk-
|
|
62
|
+
export { addChosenFiles, chosenFilesAdded, placeDroppedFiles, roomForChosenFiles, roomForDroppedFiles, sourceFileValue };
|
|
63
|
+
//# sourceMappingURL=chunk-NSHGLM6A.js.map
|
|
64
|
+
//# sourceMappingURL=chunk-NSHGLM6A.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/sources.ts"],"names":[],"mappings":"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","file":"chunk-NSHGLM6A.js","sourcesContent":["/**\n * The two optional host seams beside the upload: a **file source**, which lets\n * a file field take a file the host already stores, and an **example source**,\n * which lets a whole form take a set of values someone saved for the method.\n *\n * The kernel states the shapes and applies the answers; the host answers. It\n * knows nothing of any platform's routes, records or permissions: a host maps\n * its own list onto these shapes, and a host that gives neither seam gets the\n * form it had before. The controls that use them are `./react`'s\n * (`FilePicker`, `FillWithExample`), and docs/sources.md is the contract.\n *\n * Shapes and pure functions only, so a host's server can import the types it\n * answers with, and the rules that write an answer into a value are tested in\n * node. No React, no fetch. What a row SAYS is data here (`SourceFile`'s\n * `typeLabel` and `origin` are the host's words); how a row DRAWS is a\n * rendering, so the glyph a host draws on a row is `./react`'s\n * (`FilePickerSource.renderGlyph`), never a field of these shapes.\n */\n\n/** Whose files a file source lists: the person's own, or the whole team's. */\nexport type FileSourceScope = 'mine' | 'everyone';\n\n/** One page of a file source's list, as the picker asks for it. */\nexport interface FileSourceQuery {\n /** What the person searched for, trimmed; empty when they searched for nothing. */\n search: string;\n /** Whose files to list. */\n scope: FileSourceScope;\n /**\n * The MIME types the field accepts, after any narrowing a host applied\n * (`narrowFileFormats`), in the field's own order. The list must hold only\n * files of these types: the picker writes what is chosen straight into the\n * field, and a field that refuses a dropped file of another type must not\n * be handed one this way.\n */\n mediaTypes: readonly string[];\n /** Where to continue: absent for the first page, else the previous page's `nextCursor`. */\n cursor?: string;\n /** Aborted when the answer is no longer wanted: a newer search, a closed picker. */\n signal: AbortSignal;\n}\n\n/** One row of a file source's list: a file the host already stores. */\nexport interface SourceFile {\n /** The row's identity in the host's list, which a choice is kept by. */\n id: string;\n /**\n * The stored file's address, written into the field as its `url`, exactly as\n * an upload's result is. Several rows may share one address.\n */\n url: string;\n /** The file's name, shown on the row and written into the field as its `filename`. */\n name: string;\n /**\n * The file's MIME type, which picks the field's format the row's file is:\n * the row names that format's label (\"PNG\") unless `typeLabel` words the\n * type, and draws an image's glyph or a document's from it unless the\n * host draws its own.\n */\n mediaType?: string;\n /**\n * The file's type in the host's own words (\"Picture\", \"PDF document\"), said\n * on the row's second line in place of the format's label, so a row reads\n * as the host's own list of files does. An empty string counts as none.\n */\n typeLabel?: string;\n /** The file's size in bytes. */\n size?: number;\n /** How the file came to be. A `test` file is marked \"Test file\" at the head of its row's second line. */\n kind?: 'uploaded' | 'generated' | 'test';\n /** When the file was stored, as an ISO 8601 timestamp; the row says it as a date. */\n createdAt?: string;\n /**\n * Where the file came from, in the host's own words (\"Uploaded by Ana\",\n * \"Made by Invoice extraction\"), shown on the row's second line as it is.\n */\n origin?: string;\n}\n\n/** One page of a file source's answer. */\nexport interface FileSourcePage {\n /** The page's files, newest first. A page may be empty and still carry a cursor. */\n files: readonly SourceFile[];\n /** Where the next page starts, or nothing when this page is the last. */\n nextCursor?: string | null;\n}\n\n/**\n * Where a file field can take a file the host already stores. When a form has\n * one, every file field offers \"Choose from your team's files\", and a list of\n * files offers it for several at once.\n */\nexport interface FileSource {\n /** Lists one page of files for a query. Rejecting shows the failure with Try again. */\n list: (query: FileSourceQuery) => Promise<FileSourcePage>;\n /** Which of Mine and Everyone the picker opens on. */\n initialScope: FileSourceScope;\n}\n\n/** One example of a method: a name and the inputs a run of it sends. */\nexport interface FormExample {\n /** The example's identity in the host's list. */\n id: string;\n /** The example's name, shown on its row. */\n name: string;\n /**\n * The inputs as a run takes them: each input `{ concept, content }`, an\n * optional input left out, an empty plural input a bare `[]`. It is the\n * shape `apiInputsFromRunValues` produces, so a host saves an example from\n * `form.inputs` and hands it back as it is.\n */\n inputs: Readonly<Record<string, unknown>>;\n}\n\n/** One page of an example source's list. */\nexport interface ExampleSourceQuery {\n /** Where to continue: absent for the first page, else the previous page's `nextCursor`. */\n cursor?: string;\n /** Aborted when the answer is no longer wanted. */\n signal: AbortSignal;\n}\n\n/** One page of an example source's answer. */\nexport interface ExampleSourcePage {\n /** The page's examples, in the order the list shows them. */\n examples: readonly FormExample[];\n /** Where the next page starts, or nothing when this page is the last. */\n nextCursor?: string | null;\n}\n\n/**\n * Where a form can take a whole set of values. When a form has one, it offers\n * \"Fill with an example\". `rename` and `delete` are capabilities: each is\n * offered on a row's menu only when given, so a host gives them only to a\n * person allowed to change examples.\n */\nexport interface ExampleSource {\n /** Lists one page of the method's examples. Rejecting shows the failure with Try again. */\n list: (query: ExampleSourceQuery) => Promise<ExampleSourcePage>;\n /**\n * Renames an example. Resolving to the example as stored replaces the row;\n * resolving to nothing keeps the row under the new name.\n */\n rename?: (example: FormExample, name: string) => Promise<FormExample | void>;\n /** Deletes an example. Asked for only after the person confirmed it. */\n delete?: (example: FormExample) => Promise<void>;\n /**\n * Whether \"Fill with an example\" is left out while the method has no\n * example. `false` by default, when the empty list says how to make one.\n * A host sets it for a person who cannot save an example, to whom an empty\n * list has nothing to offer; the form then asks for the first page as it\n * mounts, to know whether to offer the control.\n */\n hideWhenEmpty?: boolean;\n}\n\n/** What a chosen file writes into its field: the same `{ url, filename }` an upload writes. */\nexport function sourceFileValue(file: SourceFile): { url: string; filename: string } {\n return { url: file.url, filename: file.name };\n}\n\n/** The `url` a list item holds, when it is a file value holding one. */\nfunction heldUrl(item: unknown): string | undefined {\n if (!item || typeof item !== 'object') return undefined;\n const url = (item as { url?: unknown }).url;\n return typeof url === 'string' && url !== '' ? url : undefined;\n}\n\n/** The addresses a list's items hold, each once. */\nfunction heldUrls(items: readonly unknown[]): Set<string> {\n return new Set(items.map(heldUrl).filter((url): url is string => url !== undefined));\n}\n\n/**\n * A list of files with the chosen files written into it, in the order they\n * were chosen: first into the rows that hold no file yet (a row the person\n * added and left empty), then as new rows after the last. A file whose address\n * the list already holds is not written again, and neither is a second choice\n * of one address: two rows of a source may name the same stored file. Returns\n * a new array and leaves `held` as it was.\n */\nexport function addChosenFiles(\n held: readonly unknown[] | undefined,\n chosen: readonly SourceFile[],\n): unknown[] {\n const items = [...(held ?? [])];\n const urls = heldUrls(items);\n let free = 0;\n for (const file of chosen) {\n if (urls.has(file.url)) continue;\n urls.add(file.url);\n while (free < items.length && heldUrl(items[free]) !== undefined) free += 1;\n if (free < items.length) items[free] = sourceFileValue(file);\n else items.push(sourceFileValue(file));\n free += 1;\n }\n return items;\n}\n\n/** Where `placeDroppedFiles` puts a batch of files, and what it leaves out. */\nexport interface DroppedFilesPlacement {\n /**\n * The list once the rows the batch needs are appended: `held` as it was,\n * then one empty row (`undefined`) per file that did not go into a free row.\n * A new array whenever rows are appended, and a copy of `held` otherwise.\n */\n items: unknown[];\n /** How many rows were appended, so a caller writes the list only when it grew. */\n appended: number;\n /** The row each placed file goes to, in the order the files were given. */\n targets: number[];\n /** How many files, from the end of the batch, found no room. */\n leftOut: number;\n}\n\n/**\n * Where a batch of `count` files dropped on a list of files goes: first into\n * the rows that are free, in index order, then into new rows after the last,\n * as far as `maxItemCount` allows. The files keep the order they were given,\n * and those beyond the room are left out from the end of the batch.\n *\n * A row is free when it holds no file (its value carries no `url`) and\n * `isFree` says so too. The caller's predicate is where it adds what only it\n * knows of a row: that a file is still arriving there, whose write-back the row\n * must be kept for, or that the host reports a failure there, the only record\n * that one of the person's files did not arrive. Without one, every row holding\n * no file is free.\n *\n * It mirrors `addChosenFiles`, so the two ways several files enter a list fill\n * it alike, and it knows nothing of `File`: the caller judges each file and\n * hands over how many it accepted.\n */\nexport function placeDroppedFiles(\n held: readonly unknown[] | undefined,\n count: number,\n maxItemCount: number | undefined,\n isFree: (index: number) => boolean = () => true,\n): DroppedFilesPlacement {\n const items = [...(held ?? [])];\n const targets: number[] = [];\n for (let index = 0; index < items.length && targets.length < count; index += 1) {\n if (heldUrl(items[index]) === undefined && isFree(index)) targets.push(index);\n }\n let appended = 0;\n while (targets.length < count && (maxItemCount === undefined || items.length < maxItemCount)) {\n targets.push(items.length);\n items.push(undefined);\n appended += 1;\n }\n return { items, appended, targets, leftOut: count - targets.length };\n}\n\n/**\n * How many files `placeDroppedFiles` can place in `held`: its free rows, by the\n * same test, and the rows its bound still allows after the last. `undefined`\n * when the list has no bound, since appending then never runs out.\n */\nexport function roomForDroppedFiles(\n held: readonly unknown[] | undefined,\n maxItemCount: number | undefined,\n isFree: (index: number) => boolean = () => true,\n): number | undefined {\n if (maxItemCount === undefined) return undefined;\n const items = held ?? [];\n let free = 0;\n for (let index = 0; index < items.length; index += 1) {\n if (heldUrl(items[index]) === undefined && isFree(index)) free += 1;\n }\n return free + Math.max(0, maxItemCount - items.length);\n}\n\n/**\n * How many files `addChosenFiles` writes for `chosen` into `held`: the choices\n * whose address the list does not hold yet, each address once. This is what a\n * choice spends of `roomForChosenFiles`, so a held file chosen again, or a\n * second row naming a chosen file, takes no room.\n */\nexport function chosenFilesAdded(\n held: readonly unknown[] | undefined,\n chosen: readonly SourceFile[],\n): number {\n const urls = heldUrls(held ?? []);\n const before = urls.size;\n for (const file of chosen) urls.add(file.url);\n return urls.size - before;\n}\n\n/**\n * How many files a list may still take from a source: its bound less the files\n * it holds, since `addChosenFiles` writes into its empty rows before adding\n * any. `undefined` when the list has no bound.\n */\nexport function roomForChosenFiles(\n held: readonly unknown[] | undefined,\n maxItemCount: number | undefined,\n): number | undefined {\n if (maxItemCount === undefined) return undefined;\n const files = (held ?? []).filter((item) => heldUrl(item) !== undefined).length;\n return Math.max(0, maxItemCount - files);\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { planFileSave, planStuffSave, splitFileName, typedFileStem } from './chunk-ICUEHNEH.js';
|
|
2
2
|
import { seedInputs, closeOptionalStructures, openOptionalStructures } from './chunk-VH5CDQDR.js';
|
|
3
|
-
import { sourceFileValue, roomForChosenFiles, addChosenFiles, chosenFilesAdded } from './chunk-
|
|
3
|
+
import { sourceFileValue, roomForChosenFiles, roomForDroppedFiles, addChosenFiles, placeDroppedFiles, chosenFilesAdded } from './chunk-NSHGLM6A.js';
|
|
4
4
|
import { readStuffFile } from './chunk-RRTI7AIR.js';
|
|
5
5
|
import { apiInputsFromRunValues, sameRunValues, runValuesFromInputs, setValueAtPath } from './chunk-ZX52CQJ4.js';
|
|
6
6
|
import { viewableUrl, isNativeDateNode, isNativeHtmlNode, readHtmlContent, isNativeCompositeNode, readCompositeContent, isNativeValueNode, readDateContent, formatDateContent, readImageContent, readDataUrl, readDocumentContent } from './chunk-CM4KVG53.js';
|
|
@@ -31,6 +31,9 @@ import { marked } from 'marked';
|
|
|
31
31
|
function cn(...inputs) {
|
|
32
32
|
return twMerge(clsx(inputs));
|
|
33
33
|
}
|
|
34
|
+
function isThenable(answer) {
|
|
35
|
+
return (typeof answer === "object" || typeof answer === "function") && answer !== null && typeof answer.then === "function";
|
|
36
|
+
}
|
|
34
37
|
var CATEGORY_ICON = {
|
|
35
38
|
text: Type,
|
|
36
39
|
date: CalendarDays,
|
|
@@ -222,6 +225,9 @@ function formatFileDate(date) {
|
|
|
222
225
|
if (days === 1) return "Yesterday";
|
|
223
226
|
return inCurrentYear(date) ? DAY.format(date) : DAY_YEAR.format(date);
|
|
224
227
|
}
|
|
228
|
+
function someNames(names, more) {
|
|
229
|
+
return AND.format(more > 0 ? [...names, `${more} more`] : names);
|
|
230
|
+
}
|
|
225
231
|
var EN_FIELD_STRINGS = {
|
|
226
232
|
optionalBadge: "optional",
|
|
227
233
|
selectPlaceholder: "Select\u2026",
|
|
@@ -276,6 +282,18 @@ var EN_FIELD_STRINGS = {
|
|
|
276
282
|
yes: "Yes",
|
|
277
283
|
no: "No",
|
|
278
284
|
unsupportedFileType: (formats) => `That file type is not supported. Accepted formats: ${formats}.`,
|
|
285
|
+
dropFilesOrBrowse: "Drop files or click to browse",
|
|
286
|
+
dropFilesToAdd: "Drop to add the files",
|
|
287
|
+
addFilesAria: (label) => label ? `Add files to ${label}` : "Add files",
|
|
288
|
+
filesAdded: (count) => count === 1 ? "1 file added" : `${count} files added`,
|
|
289
|
+
filesRefused: (names, more, formats) => `${names.length + more === 1 ? "Not added, because its" : "Not added, because their"} file type is not supported: ${someNames(names, more)}. Accepted formats: ${formats}.`,
|
|
290
|
+
filesLeftOut: (names, more, fitted) => {
|
|
291
|
+
const these = names.length + more === 1 ? "this file was" : "these files were";
|
|
292
|
+
const room = fitted === 0 ? "The list is full" : `The list had room for ${fitted === 1 ? "1 more file" : `${fitted} more files`}`;
|
|
293
|
+
return `${room}, so ${these} not added: ${someNames(names, more)}.`;
|
|
294
|
+
},
|
|
295
|
+
oneFileOnly: "This field takes one file.",
|
|
296
|
+
oneFileOnlyInList: "This field takes one file. Drop several files on the list's drop area.",
|
|
279
297
|
hideOptionalFields: "Hide optional fields",
|
|
280
298
|
hideOptionalInputs: "Hide optional inputs",
|
|
281
299
|
optionalFieldsCount: (count) => count === 1 ? "1 optional field" : `${count} optional fields`,
|
|
@@ -377,6 +395,9 @@ function formatBytes2(bytes) {
|
|
|
377
395
|
if (bytes < 1024 * 1024) return KILOBYTES.format(bytes / 1024);
|
|
378
396
|
return MEGABYTES.format(bytes / (1024 * 1024));
|
|
379
397
|
}
|
|
398
|
+
function someNames2(names, more) {
|
|
399
|
+
return AND2.format(more > 0 ? [...names, `${more} ${plural(more, "autre", "autres")}`] : names);
|
|
400
|
+
}
|
|
380
401
|
var FR_FIELD_STRINGS = {
|
|
381
402
|
optionalBadge: "facultatif",
|
|
382
403
|
selectPlaceholder: "Choisir\u2026",
|
|
@@ -431,6 +452,18 @@ var FR_FIELD_STRINGS = {
|
|
|
431
452
|
yes: "Oui",
|
|
432
453
|
no: "Non",
|
|
433
454
|
unsupportedFileType: (formats) => `Ce type de fichier n\u2019est pas pris en charge. Formats accept\xE9s\xA0: ${formats}.`,
|
|
455
|
+
dropFilesOrBrowse: "D\xE9posez des fichiers ou cliquez pour parcourir",
|
|
456
|
+
dropFilesToAdd: "D\xE9posez pour ajouter les fichiers",
|
|
457
|
+
addFilesAria: (label) => label ? `Ajouter des fichiers \xE0 ${label}` : "Ajouter des fichiers",
|
|
458
|
+
filesAdded: (count) => `${count} ${plural(count, "fichier ajout\xE9", "fichiers ajout\xE9s")}`,
|
|
459
|
+
filesRefused: (names, more, formats) => `${names.length + more === 1 ? "Non ajout\xE9, car son type n\u2019est pas pris en charge" : "Non ajout\xE9s, car leur type n\u2019est pas pris en charge"}\xA0: ${someNames2(names, more)}. Formats accept\xE9s\xA0: ${formats}.`,
|
|
460
|
+
filesLeftOut: (names, more, fitted) => {
|
|
461
|
+
const these = names.length + more === 1 ? "ce fichier n\u2019a donc pas \xE9t\xE9 ajout\xE9" : "ces fichiers n\u2019ont donc pas \xE9t\xE9 ajout\xE9s";
|
|
462
|
+
const room = fitted === 0 ? "La liste est pleine" : `La liste n\u2019avait de place que pour ${fitted} ${plural(fitted, "fichier", "fichiers")} de plus`;
|
|
463
|
+
return `${room}, ${these}\xA0: ${someNames2(names, more)}.`;
|
|
464
|
+
},
|
|
465
|
+
oneFileOnly: "Ce champ accepte un seul fichier.",
|
|
466
|
+
oneFileOnlyInList: "Ce champ accepte un seul fichier. D\xE9posez-en plusieurs dans la zone de d\xE9p\xF4t de la liste.",
|
|
434
467
|
hideOptionalFields: "Masquer les champs facultatifs",
|
|
435
468
|
hideOptionalInputs: "Masquer les entr\xE9es facultatives",
|
|
436
469
|
optionalFieldsCount: (count) => `${count} ${plural(count, "champ facultatif", "champs facultatifs")}`,
|
|
@@ -1267,6 +1300,10 @@ function FileRow({
|
|
|
1267
1300
|
}
|
|
1268
1301
|
);
|
|
1269
1302
|
}
|
|
1303
|
+
var ListDropAreaContext = createContext(null);
|
|
1304
|
+
function useListDropArea() {
|
|
1305
|
+
return useContext(ListDropAreaContext);
|
|
1306
|
+
}
|
|
1270
1307
|
var IMAGE_EXT_RE = /\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)(\?|$)/i;
|
|
1271
1308
|
var PDF_EXT_RE = /\.pdf(\?|$)/i;
|
|
1272
1309
|
var DATA_URL_MIME_RE = /^data:([^;,]+)/i;
|
|
@@ -1278,9 +1315,6 @@ function isReadableLink(url) {
|
|
|
1278
1315
|
function dataUrlMime(url) {
|
|
1279
1316
|
return DATA_URL_MIME_RE.exec(url)?.[1]?.toLowerCase();
|
|
1280
1317
|
}
|
|
1281
|
-
function isThenable(answer) {
|
|
1282
|
-
return (typeof answer === "object" || typeof answer === "function") && answer !== null && typeof answer.then === "function";
|
|
1283
|
-
}
|
|
1284
1318
|
function storedUrlOf(stored) {
|
|
1285
1319
|
if (typeof stored === "string") return stored === "" ? void 0 : stored;
|
|
1286
1320
|
if (stored === null || typeof stored !== "object") return void 0;
|
|
@@ -1336,6 +1370,7 @@ function FileField({
|
|
|
1336
1370
|
}, []);
|
|
1337
1371
|
useEffect(() => () => setLocal(null), [setLocal]);
|
|
1338
1372
|
const [rejected, setRejected] = useState(null);
|
|
1373
|
+
const dropArea = useListDropArea();
|
|
1339
1374
|
const [dismissedUploadError, setDismissedUploadError] = useState(null);
|
|
1340
1375
|
useEffect(() => {
|
|
1341
1376
|
if (uploadError === void 0) setDismissedUploadError(null);
|
|
@@ -1349,19 +1384,11 @@ function FileField({
|
|
|
1349
1384
|
const hint = acceptLabel(formats);
|
|
1350
1385
|
const accept = useMemo(() => acceptMap(formats), [formats]);
|
|
1351
1386
|
const canUpload = onDropFile !== void 0;
|
|
1352
|
-
const
|
|
1353
|
-
(file) => {
|
|
1354
|
-
if (!onDropFile) return;
|
|
1355
|
-
if (!isAcceptedFile(formats, file)) {
|
|
1356
|
-
setRejected(file.name);
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
setRejected(null);
|
|
1360
|
-
setDismissedUploadError(null);
|
|
1361
|
-
const upload = onDropFile(file);
|
|
1387
|
+
const track = useCallback(
|
|
1388
|
+
(file, upload, heldUrl) => {
|
|
1362
1389
|
const awaitingUpload = isThenable(upload);
|
|
1363
1390
|
setLocal({ objectUrl: URL.createObjectURL(file), type: file.type, awaitingUpload });
|
|
1364
|
-
urlInFlight.current =
|
|
1391
|
+
urlInFlight.current = heldUrl;
|
|
1365
1392
|
flightSeen.current = false;
|
|
1366
1393
|
if (!awaitingUpload) return;
|
|
1367
1394
|
const generation = localGeneration.current;
|
|
@@ -1378,8 +1405,28 @@ function FileField({
|
|
|
1378
1405
|
}
|
|
1379
1406
|
);
|
|
1380
1407
|
},
|
|
1381
|
-
[
|
|
1408
|
+
[setLocal]
|
|
1409
|
+
);
|
|
1410
|
+
const handleFile = useCallback(
|
|
1411
|
+
(file) => {
|
|
1412
|
+
if (!onDropFile) return;
|
|
1413
|
+
if (!isAcceptedFile(formats, file)) {
|
|
1414
|
+
setRejected({ kind: "type", name: file.name, over: value?.url });
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
setRejected(null);
|
|
1418
|
+
setDismissedUploadError(null);
|
|
1419
|
+
track(file, onDropFile(file), value?.url);
|
|
1420
|
+
},
|
|
1421
|
+
[formats, onDropFile, track, value?.url]
|
|
1382
1422
|
);
|
|
1423
|
+
useEffect(() => {
|
|
1424
|
+
const placed = dropArea?.takePlaced(id);
|
|
1425
|
+
if (!placed) return;
|
|
1426
|
+
setRejected(null);
|
|
1427
|
+
setDismissedUploadError(null);
|
|
1428
|
+
track(placed.file, placed.upload, void 0);
|
|
1429
|
+
}, [dropArea, id, track]);
|
|
1383
1430
|
const busy = disabled || uploading;
|
|
1384
1431
|
const {
|
|
1385
1432
|
getRootProps,
|
|
@@ -1403,17 +1450,22 @@ function FileField({
|
|
|
1403
1450
|
// handlers on a `role="presentation"` div, and the element a keyboard or
|
|
1404
1451
|
// voice-control user lands on is a generic with no role and no name.
|
|
1405
1452
|
noKeyboard: true,
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
},
|
|
1410
|
-
// react-dropzone rejects a file its own matcher refuses without ever calling
|
|
1411
|
-
// `onDrop`, so without this a wrong drop is silently swallowed - the control
|
|
1412
|
-
// simply does nothing, which reads as a broken dropzone rather than a
|
|
1453
|
+
// Every drop and pick lands here, the files react-dropzone's own matcher
|
|
1454
|
+
// refused included. Those must be answered: left alone, a wrong drop is
|
|
1455
|
+
// silently swallowed, which reads as a broken dropzone rather than a
|
|
1413
1456
|
// refused file.
|
|
1414
|
-
|
|
1457
|
+
onDrop: (accepted, rejections) => {
|
|
1458
|
+
if (accepted.length + rejections.length > 1) {
|
|
1459
|
+
setRejected({ kind: "batch", over: value?.url });
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const file = accepted[0];
|
|
1463
|
+
if (file) {
|
|
1464
|
+
handleFile(file);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1415
1467
|
const name = rejections[0]?.file.name;
|
|
1416
|
-
if (name) setRejected(name);
|
|
1468
|
+
if (name) setRejected({ kind: "type", name, over: value?.url });
|
|
1417
1469
|
}
|
|
1418
1470
|
});
|
|
1419
1471
|
const clear = useCallback(() => {
|
|
@@ -1439,11 +1491,13 @@ function FileField({
|
|
|
1439
1491
|
if (!localPreview || localPreview.boundUrl !== void 0) return;
|
|
1440
1492
|
if (failure !== null) setLocal(null);
|
|
1441
1493
|
}, [failure, localPreview, setLocal]);
|
|
1494
|
+
const refusal = rejected !== null && rejected.over === value?.url ? rejected : null;
|
|
1442
1495
|
const lastUrl = useRef(value?.url);
|
|
1443
1496
|
useEffect(() => {
|
|
1444
1497
|
if (lastUrl.current === value?.url) return;
|
|
1445
1498
|
lastUrl.current = value?.url;
|
|
1446
1499
|
if (uploadError !== void 0) setDismissedUploadError(uploadError);
|
|
1500
|
+
setRejected(null);
|
|
1447
1501
|
}, [value?.url, uploadError]);
|
|
1448
1502
|
useEffect(() => {
|
|
1449
1503
|
if (!localPreview) return;
|
|
@@ -1600,11 +1654,15 @@ function FileField({
|
|
|
1600
1654
|
}
|
|
1601
1655
|
)
|
|
1602
1656
|
),
|
|
1603
|
-
!busy && (
|
|
1604
|
-
/* @__PURE__ */ jsx("span", { className: "font-mono", children:
|
|
1657
|
+
!busy && (refusal ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-[12px] text-destructive", children: refusal.kind === "type" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1658
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono", children: refusal.name }),
|
|
1605
1659
|
" \u2014 ",
|
|
1606
1660
|
s.unsupportedFileType(hint)
|
|
1607
|
-
] }) :
|
|
1661
|
+
] }) : dropArea?.open ? (
|
|
1662
|
+
// Pointed to only while it takes files: a full list's area is
|
|
1663
|
+
// shut, and would refuse the very drop this sends there.
|
|
1664
|
+
s.oneFileOnlyInList
|
|
1665
|
+
) : s.oneFileOnly }) : failure && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-[12px] text-destructive", children: failure })),
|
|
1608
1666
|
linkOpen ? /* @__PURE__ */ jsx(
|
|
1609
1667
|
"input",
|
|
1610
1668
|
{
|
|
@@ -2199,6 +2257,76 @@ function firstFocusable(within) {
|
|
|
2199
2257
|
}
|
|
2200
2258
|
return null;
|
|
2201
2259
|
}
|
|
2260
|
+
var NAMED = 3;
|
|
2261
|
+
function ListFileDrop({ label, formats, shut, onPlace }) {
|
|
2262
|
+
const s = useFieldStrings();
|
|
2263
|
+
const hint = acceptLabel(formats);
|
|
2264
|
+
const accept = useMemo(() => acceptMap(formats), [formats]);
|
|
2265
|
+
const [notice, setNotice] = useState(null);
|
|
2266
|
+
const [added, setAdded] = useState({ count: 0, seq: 0 });
|
|
2267
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
2268
|
+
multiple: true,
|
|
2269
|
+
disabled: shut,
|
|
2270
|
+
// Filters the OS picker; `isAcceptedFile` below is the enforcement, as on a
|
|
2271
|
+
// single field, because a drop bypasses the picker entirely.
|
|
2272
|
+
accept,
|
|
2273
|
+
noKeyboard: true,
|
|
2274
|
+
onDrop: (accepted, rejections) => {
|
|
2275
|
+
const refused = rejections.map((rejection) => rejection.file);
|
|
2276
|
+
const taken = [];
|
|
2277
|
+
for (const file of accepted) (isAcceptedFile(formats, file) ? taken : refused).push(file);
|
|
2278
|
+
const fitted = taken.length > 0 ? onPlace(taken) : 0;
|
|
2279
|
+
setNotice({
|
|
2280
|
+
refused: refused.map((file) => file.name),
|
|
2281
|
+
leftOut: taken.slice(fitted).map((file) => file.name),
|
|
2282
|
+
fitted
|
|
2283
|
+
});
|
|
2284
|
+
if (fitted > 0) setAdded((prev) => ({ count: fitted, seq: prev.seq + 1 }));
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
2288
|
+
/* @__PURE__ */ jsxs(
|
|
2289
|
+
"div",
|
|
2290
|
+
{
|
|
2291
|
+
...getRootProps(),
|
|
2292
|
+
"data-list-file-drop": true,
|
|
2293
|
+
className: cn(
|
|
2294
|
+
"flex flex-col items-center justify-center gap-1 rounded-md border border-dashed px-4 py-4 text-center max-sm:min-h-11",
|
|
2295
|
+
"cursor-pointer select-none transition-colors",
|
|
2296
|
+
"focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background",
|
|
2297
|
+
isDragActive ? "border-primary/60 bg-primary/5" : "border-border bg-input hover:border-border hover:bg-muted",
|
|
2298
|
+
shut && "cursor-not-allowed opacity-60"
|
|
2299
|
+
),
|
|
2300
|
+
children: [
|
|
2301
|
+
/* @__PURE__ */ jsx(
|
|
2302
|
+
"input",
|
|
2303
|
+
{
|
|
2304
|
+
...getInputProps({ tabIndex: 0 }),
|
|
2305
|
+
disabled: shut,
|
|
2306
|
+
"aria-label": s.addFilesAria(label)
|
|
2307
|
+
}
|
|
2308
|
+
),
|
|
2309
|
+
/* @__PURE__ */ jsx(Upload, { className: "h-4 w-4 text-muted-foreground", strokeWidth: 1.75 }),
|
|
2310
|
+
/* @__PURE__ */ jsx("p", { className: "text-[13px] text-foreground", children: isDragActive ? s.dropFilesToAdd : s.dropFilesOrBrowse }),
|
|
2311
|
+
hint && /* @__PURE__ */ jsx("p", { className: "font-mono text-[10.5px] text-muted-foreground", children: hint })
|
|
2312
|
+
]
|
|
2313
|
+
}
|
|
2314
|
+
),
|
|
2315
|
+
notice && (notice.refused.length > 0 || notice.leftOut.length > 0) && /* @__PURE__ */ jsxs("div", { role: "alert", className: "space-y-1 text-[12px] text-destructive", children: [
|
|
2316
|
+
notice.refused.length > 0 && /* @__PURE__ */ jsx("p", { children: s.filesRefused(
|
|
2317
|
+
notice.refused.slice(0, NAMED),
|
|
2318
|
+
Math.max(0, notice.refused.length - NAMED),
|
|
2319
|
+
hint
|
|
2320
|
+
) }),
|
|
2321
|
+
notice.leftOut.length > 0 && /* @__PURE__ */ jsx("p", { children: s.filesLeftOut(
|
|
2322
|
+
notice.leftOut.slice(0, NAMED),
|
|
2323
|
+
Math.max(0, notice.leftOut.length - NAMED),
|
|
2324
|
+
notice.fitted
|
|
2325
|
+
) })
|
|
2326
|
+
] }),
|
|
2327
|
+
/* @__PURE__ */ jsx("p", { role: "status", className: "sr-only", children: /* @__PURE__ */ jsx("span", { children: added.count > 0 ? s.filesAdded(added.count) : "" }, added.seq) })
|
|
2328
|
+
] });
|
|
2329
|
+
}
|
|
2202
2330
|
|
|
2203
2331
|
// src/react/row-moves.ts
|
|
2204
2332
|
function destinationOf(order) {
|
|
@@ -2226,6 +2354,10 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2226
2354
|
const rowsRef = useRef(null);
|
|
2227
2355
|
const landedRow = useRef(null);
|
|
2228
2356
|
const [staleErrors, setStaleErrors] = useState(NO_ERRORS);
|
|
2357
|
+
const [pending, setPending] = useState(NO_UPLOADS);
|
|
2358
|
+
const nextUpload = useRef(0);
|
|
2359
|
+
const rowMoves = env?.rowMoves;
|
|
2360
|
+
const arriving = useMemo(() => arrivingRows(pending, rowMoves), [pending, rowMoves]);
|
|
2229
2361
|
const setItem = (index, itemValue) => {
|
|
2230
2362
|
const next = items.slice();
|
|
2231
2363
|
next[index] = itemValue;
|
|
@@ -2242,7 +2374,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2242
2374
|
onChange(items.filter((_, i) => i !== index));
|
|
2243
2375
|
};
|
|
2244
2376
|
const addItem = () => onChange([...items, emptyValue(field.item)]);
|
|
2245
|
-
const busy = listIsBusy(id, env?.uploadingIds);
|
|
2377
|
+
const busy = listIsBusy(id, env?.uploadingIds) || arriving.size > 0;
|
|
2246
2378
|
const hostErrors = env?.uploadErrors;
|
|
2247
2379
|
useEffect(() => {
|
|
2248
2380
|
setStaleErrors((prev) => {
|
|
@@ -2251,16 +2383,19 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2251
2383
|
return next.size === prev.size ? prev : next;
|
|
2252
2384
|
});
|
|
2253
2385
|
}, [hostErrors]);
|
|
2386
|
+
const fileItem = field.item.kind === "document" || field.item.kind === "image" ? field.item : null;
|
|
2387
|
+
const tracksUploads = fileItem !== null;
|
|
2254
2388
|
const rowEnv = useMemo(() => {
|
|
2255
|
-
if (!env || staleErrors.size === 0) return env;
|
|
2389
|
+
if (!env || staleErrors.size === 0 && !tracksUploads) return env;
|
|
2256
2390
|
const drop = env.onDropFile;
|
|
2391
|
+
const uploadErrors = staleErrors.size === 0 ? env.uploadErrors : new Map(
|
|
2392
|
+
[...env.uploadErrors ?? []].filter(
|
|
2393
|
+
([key, message]) => staleErrors.get(key) !== message
|
|
2394
|
+
)
|
|
2395
|
+
);
|
|
2257
2396
|
return {
|
|
2258
2397
|
...env,
|
|
2259
|
-
uploadErrors
|
|
2260
|
-
[...env.uploadErrors ?? []].filter(([key, message]) => staleErrors.get(key) !== message)
|
|
2261
|
-
),
|
|
2262
|
-
// A new file at a position is a new attempt there, so what the host says
|
|
2263
|
-
// about that position from now on is about the file that is there.
|
|
2398
|
+
uploadErrors,
|
|
2264
2399
|
onDropFile: drop ? (dropId, file) => {
|
|
2265
2400
|
setStaleErrors((prev) => {
|
|
2266
2401
|
if (!prev.has(dropId)) return prev;
|
|
@@ -2268,14 +2403,70 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2268
2403
|
next.delete(dropId);
|
|
2269
2404
|
return next;
|
|
2270
2405
|
});
|
|
2271
|
-
|
|
2406
|
+
const upload = drop(dropId, file);
|
|
2407
|
+
if (tracksUploads && isThenable(upload)) {
|
|
2408
|
+
const token = nextUpload.current++;
|
|
2409
|
+
const seq = env.rowMoves?.[env.rowMoves.length - 1]?.seq ?? 0;
|
|
2410
|
+
setPending((prev) => new Map(prev).set(token, { id: dropId, seq }));
|
|
2411
|
+
const answered = () => setPending((prev) => withoutUpload(prev, token));
|
|
2412
|
+
void Promise.resolve(upload).then(answered, answered);
|
|
2413
|
+
}
|
|
2414
|
+
return upload;
|
|
2272
2415
|
} : void 0
|
|
2273
2416
|
};
|
|
2274
|
-
}, [env, staleErrors]);
|
|
2417
|
+
}, [env, staleErrors, tracksUploads]);
|
|
2275
2418
|
const isFull = field.maxItemCount !== void 0 && items.length >= field.maxItemCount;
|
|
2276
2419
|
const fileSource = env?.fileSource;
|
|
2277
|
-
const fileItem = field.item.kind === "document" || field.item.kind === "image" ? field.item : null;
|
|
2278
2420
|
const room = roomForChosenFiles(items, field.maxItemCount);
|
|
2421
|
+
const dropFile = rowEnv?.onDropFile;
|
|
2422
|
+
const dropOffered = fileItem !== null && dropFile !== void 0;
|
|
2423
|
+
const isFreeRow = (index) => {
|
|
2424
|
+
const rowId = `${id}.${index}`;
|
|
2425
|
+
return !arriving.has(rowId) && !env?.uploadingIds?.has(rowId) && !rowEnv?.uploadErrors?.has(rowId);
|
|
2426
|
+
};
|
|
2427
|
+
const dropRoom = dropOffered ? roomForDroppedFiles(items, field.maxItemCount, isFreeRow) : 0;
|
|
2428
|
+
const dropOpen = dropOffered && !env?.disabled && dropRoom !== 0;
|
|
2429
|
+
const placed = useRef(/* @__PURE__ */ new Map());
|
|
2430
|
+
const [placements, setPlacements] = useState(0);
|
|
2431
|
+
const dropArea = useMemo(() => {
|
|
2432
|
+
if (!dropOffered) return null;
|
|
2433
|
+
return {
|
|
2434
|
+
generation: placements,
|
|
2435
|
+
open: dropOpen,
|
|
2436
|
+
takePlaced: (rowId) => {
|
|
2437
|
+
const file = placed.current.get(rowId);
|
|
2438
|
+
placed.current.delete(rowId);
|
|
2439
|
+
return file;
|
|
2440
|
+
}
|
|
2441
|
+
};
|
|
2442
|
+
}, [dropOffered, dropOpen, placements]);
|
|
2443
|
+
const dropRef = useRef(null);
|
|
2444
|
+
const landing = useRef(null);
|
|
2445
|
+
useEffect(() => {
|
|
2446
|
+
const landed = landing.current;
|
|
2447
|
+
if (landed === null) return;
|
|
2448
|
+
landing.current = null;
|
|
2449
|
+
const rows = rowsRef.current?.querySelectorAll(":scope > [data-list-row]");
|
|
2450
|
+
const target = firstFocusable(rows?.[landed]) ?? followingFocusable(dropRef.current, dropRef.current);
|
|
2451
|
+
target?.focus();
|
|
2452
|
+
});
|
|
2453
|
+
const placeFiles = (files) => {
|
|
2454
|
+
if (!dropFile) return 0;
|
|
2455
|
+
const placement = placeDroppedFiles(items, files.length, field.maxItemCount, isFreeRow);
|
|
2456
|
+
if (placement.appended > 0) onChange(placement.items);
|
|
2457
|
+
placement.targets.forEach((index, order) => {
|
|
2458
|
+
const file = files[order];
|
|
2459
|
+
if (!file) return;
|
|
2460
|
+
const rowId = `${id}.${index}`;
|
|
2461
|
+
placed.current.set(rowId, { file, upload: handOver(dropFile, rowId, file) });
|
|
2462
|
+
});
|
|
2463
|
+
const count = placement.targets.length;
|
|
2464
|
+
if (count > 0) setPlacements((generation) => generation + 1);
|
|
2465
|
+
if (count > 0 && count === dropRoom && dropRef.current?.contains(document.activeElement)) {
|
|
2466
|
+
landing.current = placement.targets[count - 1] ?? null;
|
|
2467
|
+
}
|
|
2468
|
+
return count;
|
|
2469
|
+
};
|
|
2279
2470
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2280
2471
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
|
|
2281
2472
|
/* @__PURE__ */ jsx(
|
|
@@ -2294,8 +2485,8 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2294
2485
|
] }),
|
|
2295
2486
|
field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
|
|
2296
2487
|
/* @__PURE__ */ jsxs("div", { ref: rowsRef, className: "space-y-2", children: [
|
|
2297
|
-
items.length === 0 && /* @__PURE__ */ jsx("p", { className: "rounded-md border border-dashed border-border bg-input/50 px-3 py-4 text-center text-[12px] text-muted-foreground", children: s.noItemsYet }),
|
|
2298
|
-
items.map((item, index) => /* @__PURE__ */ jsxs(
|
|
2488
|
+
items.length === 0 && !dropOffered && /* @__PURE__ */ jsx("p", { className: "rounded-md border border-dashed border-border bg-input/50 px-3 py-4 text-center text-[12px] text-muted-foreground", children: s.noItemsYet }),
|
|
2489
|
+
/* @__PURE__ */ jsx(ListDropAreaContext.Provider, { value: dropArea, children: items.map((item, index) => /* @__PURE__ */ jsxs(
|
|
2299
2490
|
"div",
|
|
2300
2491
|
{
|
|
2301
2492
|
"data-list-row": true,
|
|
@@ -2334,7 +2525,16 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2334
2525
|
]
|
|
2335
2526
|
},
|
|
2336
2527
|
rowKeys[index] ?? index
|
|
2337
|
-
)),
|
|
2528
|
+
)) }),
|
|
2529
|
+
dropOffered && fileItem && /* @__PURE__ */ jsx("div", { ref: dropRef, children: /* @__PURE__ */ jsx(
|
|
2530
|
+
ListFileDrop,
|
|
2531
|
+
{
|
|
2532
|
+
label: fieldLabel(field.title, field.name, presentation),
|
|
2533
|
+
formats: fileItem.formats,
|
|
2534
|
+
shut: !dropOpen,
|
|
2535
|
+
onPlace: placeFiles
|
|
2536
|
+
}
|
|
2537
|
+
) }),
|
|
2338
2538
|
/* @__PURE__ */ jsxs(
|
|
2339
2539
|
"button",
|
|
2340
2540
|
{
|
|
@@ -2411,6 +2611,42 @@ function listIsBusy(id, uploadingIds) {
|
|
|
2411
2611
|
return false;
|
|
2412
2612
|
}
|
|
2413
2613
|
var NO_ERRORS = /* @__PURE__ */ new Map();
|
|
2614
|
+
var NO_IDS = /* @__PURE__ */ new Set();
|
|
2615
|
+
var NO_UPLOADS = /* @__PURE__ */ new Map();
|
|
2616
|
+
function withoutUpload(uploads, token) {
|
|
2617
|
+
if (!uploads.has(token)) return uploads;
|
|
2618
|
+
const next = new Map(uploads);
|
|
2619
|
+
next.delete(token);
|
|
2620
|
+
return next;
|
|
2621
|
+
}
|
|
2622
|
+
function arrivingRows(uploads, moves) {
|
|
2623
|
+
if (uploads.size === 0) return NO_IDS;
|
|
2624
|
+
const rows = /* @__PURE__ */ new Set();
|
|
2625
|
+
for (const upload of uploads.values()) {
|
|
2626
|
+
let at = upload.id;
|
|
2627
|
+
for (const move of moves ?? []) {
|
|
2628
|
+
if (at === void 0) break;
|
|
2629
|
+
if (move.seq > upload.seq) at = movedId(at, move.listId, destinationOf(move.order));
|
|
2630
|
+
}
|
|
2631
|
+
if (at !== void 0) rows.add(at);
|
|
2632
|
+
}
|
|
2633
|
+
return rows;
|
|
2634
|
+
}
|
|
2635
|
+
function handOver(drop, rowId, file) {
|
|
2636
|
+
try {
|
|
2637
|
+
return drop(rowId, file);
|
|
2638
|
+
} catch (error) {
|
|
2639
|
+
reportHostError(error);
|
|
2640
|
+
const failed = Promise.reject(error);
|
|
2641
|
+
failed.catch(() => {
|
|
2642
|
+
});
|
|
2643
|
+
return failed;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
function reportHostError(error) {
|
|
2647
|
+
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
2648
|
+
else console.error(error);
|
|
2649
|
+
}
|
|
2414
2650
|
function withRenumberedErrors(stale, listId, removed, hostErrors) {
|
|
2415
2651
|
if (!hostErrors?.size) return stale;
|
|
2416
2652
|
const prefix = `${listId}.`;
|
|
@@ -3132,12 +3368,12 @@ function withoutId(ids, id) {
|
|
|
3132
3368
|
next.delete(id);
|
|
3133
3369
|
return next;
|
|
3134
3370
|
}
|
|
3135
|
-
var
|
|
3371
|
+
var NO_UPLOADS2 = /* @__PURE__ */ new Map();
|
|
3136
3372
|
var NO_MOVES = [];
|
|
3137
3373
|
var NOTHING_HELD = {
|
|
3138
3374
|
edited: null,
|
|
3139
3375
|
failed: EMPTY_IDS,
|
|
3140
|
-
uploads:
|
|
3376
|
+
uploads: NO_UPLOADS2,
|
|
3141
3377
|
moves: NO_MOVES
|
|
3142
3378
|
};
|
|
3143
3379
|
function withoutEntry(entries, id) {
|
|
@@ -3317,7 +3553,7 @@ function useMethodForm({
|
|
|
3317
3553
|
);
|
|
3318
3554
|
const reset = useCallback(
|
|
3319
3555
|
(next) => {
|
|
3320
|
-
commit({ edited: next ?? null, failed: EMPTY_IDS, uploads:
|
|
3556
|
+
commit({ edited: next ?? null, failed: EMPTY_IDS, uploads: NO_UPLOADS2, moves: NO_MOVES });
|
|
3321
3557
|
setGiven(next ?? null);
|
|
3322
3558
|
setAttempted(false);
|
|
3323
3559
|
setResets((count) => count + 1);
|
|
@@ -6646,5 +6882,5 @@ function pdfFileName(field, value, baseName) {
|
|
|
6646
6882
|
}
|
|
6647
6883
|
|
|
6648
6884
|
export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EN_FIELD_STRINGS, EnumField, FIELD_STRINGS_BY_LOCALE, FR_FIELD_STRINGS, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, FilePicker, FilePickerEntry, FillWithExample, HtmlPreview, ImageField, JsonView, ListField, Markdown, MethodForm, NumberField, ObjectField, OptionalToggle, PdfDownloadButton, ProseField, ResultEnvProvider, ResultField, ResultRoot, SUPPORTED_LOCALES, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, checkedAccentClass, cn, fieldControlClass, fieldStringsFor, focusOutlineClass, humanizeFieldName, linkTextClass, renderHtmlPdf, resolveHtmlStorageUrls, saveInBrowser, selectedBorderClass, selectedFillClass, selectedFillStrongClass, useFieldDomId, useFieldPresentation, useFieldStrings, useMethodForm, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
|
|
6649
|
-
//# sourceMappingURL=chunk-
|
|
6650
|
-
//# sourceMappingURL=chunk-
|
|
6885
|
+
//# sourceMappingURL=chunk-UGKFWX7L.js.map
|
|
6886
|
+
//# sourceMappingURL=chunk-UGKFWX7L.js.map
|