@pipelex/mthds-form 0.23.1 → 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-EQJBM3NS.js → chunk-NSHGLM6A.js} +26 -3
- package/dist/chunk-NSHGLM6A.js.map +1 -0
- package/dist/{chunk-NOSHTDID.js → chunk-UGKFWX7L.js} +294 -48
- package/dist/chunk-UGKFWX7L.js.map +1 -0
- package/dist/core/index.js +1 -1
- package/dist/core/sources.d.ts +59 -6
- package/dist/core/sources.js +1 -1
- package/dist/{field-renderer-lNhtw-Cz.d.ts → field-renderer-CH-biG8C.d.ts} +111 -5
- package/dist/generative/index.d.ts +1 -1
- package/dist/generative/index.js +2 -2
- package/dist/react/index.d.ts +40 -84
- package/dist/react/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-EQJBM3NS.js.map +0 -1
- package/dist/chunk-NOSHTDID.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")}`,
|
|
@@ -1089,6 +1122,7 @@ function FilePickerBody({
|
|
|
1089
1122
|
{
|
|
1090
1123
|
file,
|
|
1091
1124
|
formats,
|
|
1125
|
+
glyph: source.renderGlyph?.(file),
|
|
1092
1126
|
role: multiple ? "checkbox" : "radio",
|
|
1093
1127
|
chosen: isChosen,
|
|
1094
1128
|
disabled: !isChosen && !fits([...choices, file]),
|
|
@@ -1192,7 +1226,8 @@ function rowFormat(file, formats) {
|
|
|
1192
1226
|
}
|
|
1193
1227
|
function rowMeta(s, file, format) {
|
|
1194
1228
|
const parts = [];
|
|
1195
|
-
|
|
1229
|
+
const type = file.typeLabel || format?.label;
|
|
1230
|
+
if (type) parts.push(type);
|
|
1196
1231
|
if (file.size !== void 0) parts.push(s.fileSize(file.size));
|
|
1197
1232
|
if (file.origin) parts.push(file.origin);
|
|
1198
1233
|
if (file.createdAt) {
|
|
@@ -1201,9 +1236,13 @@ function rowMeta(s, file, format) {
|
|
|
1201
1236
|
}
|
|
1202
1237
|
return parts.join(" \xB7 ");
|
|
1203
1238
|
}
|
|
1239
|
+
function drawsNothing(glyph) {
|
|
1240
|
+
return glyph === null || glyph === void 0 || typeof glyph === "boolean" || glyph === "";
|
|
1241
|
+
}
|
|
1204
1242
|
function FileRow({
|
|
1205
1243
|
file,
|
|
1206
1244
|
formats,
|
|
1245
|
+
glyph,
|
|
1207
1246
|
role,
|
|
1208
1247
|
chosen,
|
|
1209
1248
|
disabled,
|
|
@@ -1228,13 +1267,17 @@ function FileRow({
|
|
|
1228
1267
|
selectedCheckedFillClass
|
|
1229
1268
|
),
|
|
1230
1269
|
children: [
|
|
1231
|
-
/* @__PURE__ */ jsx(
|
|
1270
|
+
drawsNothing(glyph) ? /* @__PURE__ */ jsx(
|
|
1232
1271
|
Glyph,
|
|
1233
1272
|
{
|
|
1234
1273
|
className: "size-6 shrink-0 text-muted-foreground",
|
|
1235
1274
|
strokeWidth: 1.5,
|
|
1236
1275
|
"aria-hidden": "true"
|
|
1237
1276
|
}
|
|
1277
|
+
) : (
|
|
1278
|
+
// `flex` so an inline glyph sits on no text baseline, `shrink-0` so a
|
|
1279
|
+
// long name never narrows it; the size is the glyph's own.
|
|
1280
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", "data-slot": "file-row-glyph", className: "flex shrink-0", children: glyph })
|
|
1238
1281
|
),
|
|
1239
1282
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
1240
1283
|
/* @__PURE__ */ jsx("span", { className: "block truncate text-[16px] font-medium text-foreground", children: file.name }),
|
|
@@ -1257,6 +1300,10 @@ function FileRow({
|
|
|
1257
1300
|
}
|
|
1258
1301
|
);
|
|
1259
1302
|
}
|
|
1303
|
+
var ListDropAreaContext = createContext(null);
|
|
1304
|
+
function useListDropArea() {
|
|
1305
|
+
return useContext(ListDropAreaContext);
|
|
1306
|
+
}
|
|
1260
1307
|
var IMAGE_EXT_RE = /\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)(\?|$)/i;
|
|
1261
1308
|
var PDF_EXT_RE = /\.pdf(\?|$)/i;
|
|
1262
1309
|
var DATA_URL_MIME_RE = /^data:([^;,]+)/i;
|
|
@@ -1268,9 +1315,6 @@ function isReadableLink(url) {
|
|
|
1268
1315
|
function dataUrlMime(url) {
|
|
1269
1316
|
return DATA_URL_MIME_RE.exec(url)?.[1]?.toLowerCase();
|
|
1270
1317
|
}
|
|
1271
|
-
function isThenable(answer) {
|
|
1272
|
-
return (typeof answer === "object" || typeof answer === "function") && answer !== null && typeof answer.then === "function";
|
|
1273
|
-
}
|
|
1274
1318
|
function storedUrlOf(stored) {
|
|
1275
1319
|
if (typeof stored === "string") return stored === "" ? void 0 : stored;
|
|
1276
1320
|
if (stored === null || typeof stored !== "object") return void 0;
|
|
@@ -1326,6 +1370,7 @@ function FileField({
|
|
|
1326
1370
|
}, []);
|
|
1327
1371
|
useEffect(() => () => setLocal(null), [setLocal]);
|
|
1328
1372
|
const [rejected, setRejected] = useState(null);
|
|
1373
|
+
const dropArea = useListDropArea();
|
|
1329
1374
|
const [dismissedUploadError, setDismissedUploadError] = useState(null);
|
|
1330
1375
|
useEffect(() => {
|
|
1331
1376
|
if (uploadError === void 0) setDismissedUploadError(null);
|
|
@@ -1339,19 +1384,11 @@ function FileField({
|
|
|
1339
1384
|
const hint = acceptLabel(formats);
|
|
1340
1385
|
const accept = useMemo(() => acceptMap(formats), [formats]);
|
|
1341
1386
|
const canUpload = onDropFile !== void 0;
|
|
1342
|
-
const
|
|
1343
|
-
(file) => {
|
|
1344
|
-
if (!onDropFile) return;
|
|
1345
|
-
if (!isAcceptedFile(formats, file)) {
|
|
1346
|
-
setRejected(file.name);
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
setRejected(null);
|
|
1350
|
-
setDismissedUploadError(null);
|
|
1351
|
-
const upload = onDropFile(file);
|
|
1387
|
+
const track = useCallback(
|
|
1388
|
+
(file, upload, heldUrl) => {
|
|
1352
1389
|
const awaitingUpload = isThenable(upload);
|
|
1353
1390
|
setLocal({ objectUrl: URL.createObjectURL(file), type: file.type, awaitingUpload });
|
|
1354
|
-
urlInFlight.current =
|
|
1391
|
+
urlInFlight.current = heldUrl;
|
|
1355
1392
|
flightSeen.current = false;
|
|
1356
1393
|
if (!awaitingUpload) return;
|
|
1357
1394
|
const generation = localGeneration.current;
|
|
@@ -1368,8 +1405,28 @@ function FileField({
|
|
|
1368
1405
|
}
|
|
1369
1406
|
);
|
|
1370
1407
|
},
|
|
1371
|
-
[
|
|
1408
|
+
[setLocal]
|
|
1372
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]
|
|
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]);
|
|
1373
1430
|
const busy = disabled || uploading;
|
|
1374
1431
|
const {
|
|
1375
1432
|
getRootProps,
|
|
@@ -1393,17 +1450,22 @@ function FileField({
|
|
|
1393
1450
|
// handlers on a `role="presentation"` div, and the element a keyboard or
|
|
1394
1451
|
// voice-control user lands on is a generic with no role and no name.
|
|
1395
1452
|
noKeyboard: true,
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
},
|
|
1400
|
-
// react-dropzone rejects a file its own matcher refuses without ever calling
|
|
1401
|
-
// `onDrop`, so without this a wrong drop is silently swallowed - the control
|
|
1402
|
-
// 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
|
|
1403
1456
|
// refused file.
|
|
1404
|
-
|
|
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
|
+
}
|
|
1405
1467
|
const name = rejections[0]?.file.name;
|
|
1406
|
-
if (name) setRejected(name);
|
|
1468
|
+
if (name) setRejected({ kind: "type", name, over: value?.url });
|
|
1407
1469
|
}
|
|
1408
1470
|
});
|
|
1409
1471
|
const clear = useCallback(() => {
|
|
@@ -1429,11 +1491,13 @@ function FileField({
|
|
|
1429
1491
|
if (!localPreview || localPreview.boundUrl !== void 0) return;
|
|
1430
1492
|
if (failure !== null) setLocal(null);
|
|
1431
1493
|
}, [failure, localPreview, setLocal]);
|
|
1494
|
+
const refusal = rejected !== null && rejected.over === value?.url ? rejected : null;
|
|
1432
1495
|
const lastUrl = useRef(value?.url);
|
|
1433
1496
|
useEffect(() => {
|
|
1434
1497
|
if (lastUrl.current === value?.url) return;
|
|
1435
1498
|
lastUrl.current = value?.url;
|
|
1436
1499
|
if (uploadError !== void 0) setDismissedUploadError(uploadError);
|
|
1500
|
+
setRejected(null);
|
|
1437
1501
|
}, [value?.url, uploadError]);
|
|
1438
1502
|
useEffect(() => {
|
|
1439
1503
|
if (!localPreview) return;
|
|
@@ -1590,11 +1654,15 @@ function FileField({
|
|
|
1590
1654
|
}
|
|
1591
1655
|
)
|
|
1592
1656
|
),
|
|
1593
|
-
!busy && (
|
|
1594
|
-
/* @__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 }),
|
|
1595
1659
|
" \u2014 ",
|
|
1596
1660
|
s.unsupportedFileType(hint)
|
|
1597
|
-
] }) :
|
|
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 })),
|
|
1598
1666
|
linkOpen ? /* @__PURE__ */ jsx(
|
|
1599
1667
|
"input",
|
|
1600
1668
|
{
|
|
@@ -2189,6 +2257,76 @@ function firstFocusable(within) {
|
|
|
2189
2257
|
}
|
|
2190
2258
|
return null;
|
|
2191
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
|
+
}
|
|
2192
2330
|
|
|
2193
2331
|
// src/react/row-moves.ts
|
|
2194
2332
|
function destinationOf(order) {
|
|
@@ -2216,6 +2354,10 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2216
2354
|
const rowsRef = useRef(null);
|
|
2217
2355
|
const landedRow = useRef(null);
|
|
2218
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]);
|
|
2219
2361
|
const setItem = (index, itemValue) => {
|
|
2220
2362
|
const next = items.slice();
|
|
2221
2363
|
next[index] = itemValue;
|
|
@@ -2232,7 +2374,7 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2232
2374
|
onChange(items.filter((_, i) => i !== index));
|
|
2233
2375
|
};
|
|
2234
2376
|
const addItem = () => onChange([...items, emptyValue(field.item)]);
|
|
2235
|
-
const busy = listIsBusy(id, env?.uploadingIds);
|
|
2377
|
+
const busy = listIsBusy(id, env?.uploadingIds) || arriving.size > 0;
|
|
2236
2378
|
const hostErrors = env?.uploadErrors;
|
|
2237
2379
|
useEffect(() => {
|
|
2238
2380
|
setStaleErrors((prev) => {
|
|
@@ -2241,16 +2383,19 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2241
2383
|
return next.size === prev.size ? prev : next;
|
|
2242
2384
|
});
|
|
2243
2385
|
}, [hostErrors]);
|
|
2386
|
+
const fileItem = field.item.kind === "document" || field.item.kind === "image" ? field.item : null;
|
|
2387
|
+
const tracksUploads = fileItem !== null;
|
|
2244
2388
|
const rowEnv = useMemo(() => {
|
|
2245
|
-
if (!env || staleErrors.size === 0) return env;
|
|
2389
|
+
if (!env || staleErrors.size === 0 && !tracksUploads) return env;
|
|
2246
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
|
+
);
|
|
2247
2396
|
return {
|
|
2248
2397
|
...env,
|
|
2249
|
-
uploadErrors
|
|
2250
|
-
[...env.uploadErrors ?? []].filter(([key, message]) => staleErrors.get(key) !== message)
|
|
2251
|
-
),
|
|
2252
|
-
// A new file at a position is a new attempt there, so what the host says
|
|
2253
|
-
// about that position from now on is about the file that is there.
|
|
2398
|
+
uploadErrors,
|
|
2254
2399
|
onDropFile: drop ? (dropId, file) => {
|
|
2255
2400
|
setStaleErrors((prev) => {
|
|
2256
2401
|
if (!prev.has(dropId)) return prev;
|
|
@@ -2258,14 +2403,70 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2258
2403
|
next.delete(dropId);
|
|
2259
2404
|
return next;
|
|
2260
2405
|
});
|
|
2261
|
-
|
|
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;
|
|
2262
2415
|
} : void 0
|
|
2263
2416
|
};
|
|
2264
|
-
}, [env, staleErrors]);
|
|
2417
|
+
}, [env, staleErrors, tracksUploads]);
|
|
2265
2418
|
const isFull = field.maxItemCount !== void 0 && items.length >= field.maxItemCount;
|
|
2266
2419
|
const fileSource = env?.fileSource;
|
|
2267
|
-
const fileItem = field.item.kind === "document" || field.item.kind === "image" ? field.item : null;
|
|
2268
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
|
+
};
|
|
2269
2470
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
2270
2471
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
|
|
2271
2472
|
/* @__PURE__ */ jsx(
|
|
@@ -2284,8 +2485,8 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2284
2485
|
] }),
|
|
2285
2486
|
field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
|
|
2286
2487
|
/* @__PURE__ */ jsxs("div", { ref: rowsRef, className: "space-y-2", children: [
|
|
2287
|
-
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 }),
|
|
2288
|
-
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(
|
|
2289
2490
|
"div",
|
|
2290
2491
|
{
|
|
2291
2492
|
"data-list-row": true,
|
|
@@ -2324,7 +2525,16 @@ function ListField({ field, value, onChange, id, error, env }) {
|
|
|
2324
2525
|
]
|
|
2325
2526
|
},
|
|
2326
2527
|
rowKeys[index] ?? index
|
|
2327
|
-
)),
|
|
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
|
+
) }),
|
|
2328
2538
|
/* @__PURE__ */ jsxs(
|
|
2329
2539
|
"button",
|
|
2330
2540
|
{
|
|
@@ -2401,6 +2611,42 @@ function listIsBusy(id, uploadingIds) {
|
|
|
2401
2611
|
return false;
|
|
2402
2612
|
}
|
|
2403
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
|
+
}
|
|
2404
2650
|
function withRenumberedErrors(stale, listId, removed, hostErrors) {
|
|
2405
2651
|
if (!hostErrors?.size) return stale;
|
|
2406
2652
|
const prefix = `${listId}.`;
|
|
@@ -3122,12 +3368,12 @@ function withoutId(ids, id) {
|
|
|
3122
3368
|
next.delete(id);
|
|
3123
3369
|
return next;
|
|
3124
3370
|
}
|
|
3125
|
-
var
|
|
3371
|
+
var NO_UPLOADS2 = /* @__PURE__ */ new Map();
|
|
3126
3372
|
var NO_MOVES = [];
|
|
3127
3373
|
var NOTHING_HELD = {
|
|
3128
3374
|
edited: null,
|
|
3129
3375
|
failed: EMPTY_IDS,
|
|
3130
|
-
uploads:
|
|
3376
|
+
uploads: NO_UPLOADS2,
|
|
3131
3377
|
moves: NO_MOVES
|
|
3132
3378
|
};
|
|
3133
3379
|
function withoutEntry(entries, id) {
|
|
@@ -3307,7 +3553,7 @@ function useMethodForm({
|
|
|
3307
3553
|
);
|
|
3308
3554
|
const reset = useCallback(
|
|
3309
3555
|
(next) => {
|
|
3310
|
-
commit({ edited: next ?? null, failed: EMPTY_IDS, uploads:
|
|
3556
|
+
commit({ edited: next ?? null, failed: EMPTY_IDS, uploads: NO_UPLOADS2, moves: NO_MOVES });
|
|
3311
3557
|
setGiven(next ?? null);
|
|
3312
3558
|
setAttempted(false);
|
|
3313
3559
|
setResets((count) => count + 1);
|
|
@@ -6636,5 +6882,5 @@ function pdfFileName(field, value, baseName) {
|
|
|
6636
6882
|
}
|
|
6637
6883
|
|
|
6638
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 };
|
|
6639
|
-
//# sourceMappingURL=chunk-
|
|
6640
|
-
//# sourceMappingURL=chunk-
|
|
6885
|
+
//# sourceMappingURL=chunk-UGKFWX7L.js.map
|
|
6886
|
+
//# sourceMappingURL=chunk-UGKFWX7L.js.map
|