@usefillo/core 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/dist/index.d.ts +259 -20
- package/dist/index.js +968 -172
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,8 +13,16 @@ function isAnswered(value) {
|
|
|
13
13
|
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
14
14
|
return true;
|
|
15
15
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
16
|
+
function valueEquals(value, target) {
|
|
17
|
+
if (value === target) return true;
|
|
18
|
+
if (typeof value === "number" && typeof target === "string" && target.trim() !== "")
|
|
19
|
+
return Number(target) === value;
|
|
20
|
+
if (typeof target === "number" && typeof value === "string" && value.trim() !== "")
|
|
21
|
+
return Number(value) === target;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function evalCondition(cond, resolve) {
|
|
25
|
+
const value = resolve(cond.fieldId);
|
|
18
26
|
switch (cond.op) {
|
|
19
27
|
case "answered":
|
|
20
28
|
return isAnswered(value);
|
|
@@ -22,10 +30,11 @@ function evalCondition(cond, data) {
|
|
|
22
30
|
return !isAnswered(value);
|
|
23
31
|
case "eq":
|
|
24
32
|
if (Array.isArray(value)) return value.some((v) => v === cond.value);
|
|
25
|
-
return value
|
|
33
|
+
return valueEquals(value, cond.value);
|
|
26
34
|
case "neq":
|
|
35
|
+
if (!isAnswered(value)) return false;
|
|
27
36
|
if (Array.isArray(value)) return !value.some((v) => v === cond.value);
|
|
28
|
-
return value
|
|
37
|
+
return !valueEquals(value, cond.value);
|
|
29
38
|
case "contains":
|
|
30
39
|
if (typeof value === "string")
|
|
31
40
|
return value.toLowerCase().includes(String(cond.value ?? "").toLowerCase());
|
|
@@ -37,22 +46,45 @@ function evalCondition(cond, data) {
|
|
|
37
46
|
return typeof value === "number" && value < Number(cond.value);
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
|
-
function
|
|
49
|
+
function blockVisibleWith(block, resolve) {
|
|
41
50
|
if (!block.visibleIf || block.visibleIf.length === 0) return true;
|
|
42
|
-
return block.visibleIf.every((cond) => evalCondition(cond,
|
|
51
|
+
return block.visibleIf.every((cond) => evalCondition(cond, resolve));
|
|
52
|
+
}
|
|
53
|
+
function makeResolver(data, scoped, visible) {
|
|
54
|
+
return (fieldId) => scoped.has(fieldId) && !visible.has(fieldId) ? void 0 : data[fieldId];
|
|
55
|
+
}
|
|
56
|
+
function visibleFieldIds(fields, data) {
|
|
57
|
+
const scoped = new Set(fields.map((f) => f.id));
|
|
58
|
+
let visible = scoped;
|
|
59
|
+
for (let i = 0; i <= fields.length; i++) {
|
|
60
|
+
const resolve = makeResolver(data, scoped, visible);
|
|
61
|
+
const next = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const f of fields) if (blockVisibleWith(f, resolve)) next.add(f.id);
|
|
63
|
+
if (next.size === visible.size && [...next].every((id) => visible.has(id))) return next;
|
|
64
|
+
visible = next;
|
|
65
|
+
}
|
|
66
|
+
return visible;
|
|
67
|
+
}
|
|
68
|
+
function isBlockVisible(block, data) {
|
|
69
|
+
return blockVisibleWith(block, (fieldId) => data[fieldId]);
|
|
43
70
|
}
|
|
44
71
|
function visibleBlocks(page, data) {
|
|
45
|
-
|
|
72
|
+
const fields = page.blocks.filter(isField);
|
|
73
|
+
const scoped = new Set(fields.map((f) => f.id));
|
|
74
|
+
const resolve = makeResolver(data, scoped, visibleFieldIds(fields, data));
|
|
75
|
+
return page.blocks.filter((b) => blockVisibleWith(b, resolve));
|
|
46
76
|
}
|
|
47
77
|
function allFields(form) {
|
|
48
78
|
return form.pages.flatMap((p) => p.blocks.filter(isField));
|
|
49
79
|
}
|
|
50
80
|
function visibleFields(form, data) {
|
|
51
|
-
|
|
81
|
+
const fields = allFields(form);
|
|
82
|
+
const visible = visibleFieldIds(fields, data);
|
|
83
|
+
return fields.filter((f) => visible.has(f.id));
|
|
52
84
|
}
|
|
53
85
|
|
|
54
86
|
// src/validation.ts
|
|
55
|
-
import
|
|
87
|
+
import * as z from "zod/mini";
|
|
56
88
|
|
|
57
89
|
// src/phone.ts
|
|
58
90
|
var C = (iso2, name, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name, dialCode, lengths, groups, example });
|
|
@@ -129,6 +161,138 @@ var PHONE_COUNTRIES = [
|
|
|
129
161
|
C("KW", "Kuwait", "965", [8], [4, 4], "50012345")
|
|
130
162
|
];
|
|
131
163
|
var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
|
|
164
|
+
var timeZoneIso = {
|
|
165
|
+
"Africa/Cairo": "EG",
|
|
166
|
+
"Africa/Casablanca": "MA",
|
|
167
|
+
"Africa/Johannesburg": "ZA",
|
|
168
|
+
"Africa/Lagos": "NG",
|
|
169
|
+
"Africa/Nairobi": "KE",
|
|
170
|
+
"America/Adak": "US",
|
|
171
|
+
"America/Anchorage": "US",
|
|
172
|
+
"America/Argentina/Buenos_Aires": "AR",
|
|
173
|
+
"America/Argentina/Cordoba": "AR",
|
|
174
|
+
"America/Argentina/Mendoza": "AR",
|
|
175
|
+
"America/Bahia": "BR",
|
|
176
|
+
"America/Belem": "BR",
|
|
177
|
+
"America/Bogota": "CO",
|
|
178
|
+
"America/Boise": "US",
|
|
179
|
+
"America/Boa_Vista": "BR",
|
|
180
|
+
"America/Cancun": "MX",
|
|
181
|
+
"America/Chicago": "US",
|
|
182
|
+
"America/Chihuahua": "MX",
|
|
183
|
+
"America/Campo_Grande": "BR",
|
|
184
|
+
"America/Costa_Rica": "CR",
|
|
185
|
+
"America/Cuiaba": "BR",
|
|
186
|
+
"America/Denver": "US",
|
|
187
|
+
"America/Detroit": "US",
|
|
188
|
+
"America/Edmonton": "CA",
|
|
189
|
+
"America/Fortaleza": "BR",
|
|
190
|
+
"America/Guayaquil": "EC",
|
|
191
|
+
"America/Halifax": "CA",
|
|
192
|
+
"America/Hermosillo": "MX",
|
|
193
|
+
"America/Indiana/Indianapolis": "US",
|
|
194
|
+
"America/Indianapolis": "US",
|
|
195
|
+
"America/Juneau": "US",
|
|
196
|
+
"America/Kentucky/Louisville": "US",
|
|
197
|
+
"America/Lima": "PE",
|
|
198
|
+
"America/Los_Angeles": "US",
|
|
199
|
+
"America/Maceio": "BR",
|
|
200
|
+
"America/Manaus": "BR",
|
|
201
|
+
"America/Matamoros": "MX",
|
|
202
|
+
"America/Mazatlan": "MX",
|
|
203
|
+
"America/Merida": "MX",
|
|
204
|
+
"America/Mexico_City": "MX",
|
|
205
|
+
"America/Moncton": "CA",
|
|
206
|
+
"America/Monterrey": "MX",
|
|
207
|
+
"America/Montevideo": "UY",
|
|
208
|
+
"America/New_York": "US",
|
|
209
|
+
"America/Noronha": "BR",
|
|
210
|
+
"America/Nome": "US",
|
|
211
|
+
"America/North_Dakota/Center": "US",
|
|
212
|
+
"America/Ojinaga": "MX",
|
|
213
|
+
"America/Phoenix": "US",
|
|
214
|
+
"America/Porto_Velho": "BR",
|
|
215
|
+
"America/Punta_Arenas": "CL",
|
|
216
|
+
"America/Recife": "BR",
|
|
217
|
+
"America/Regina": "CA",
|
|
218
|
+
"America/Rio_Branco": "BR",
|
|
219
|
+
"America/Santiago": "CL",
|
|
220
|
+
"America/Sao_Paulo": "BR",
|
|
221
|
+
"America/Sitka": "US",
|
|
222
|
+
"America/St_Johns": "CA",
|
|
223
|
+
"America/Tijuana": "MX",
|
|
224
|
+
"America/Toronto": "CA",
|
|
225
|
+
"America/Vancouver": "CA",
|
|
226
|
+
"America/Whitehorse": "CA",
|
|
227
|
+
"America/Winnipeg": "CA",
|
|
228
|
+
"America/Yakutat": "US",
|
|
229
|
+
"Asia/Bangkok": "TH",
|
|
230
|
+
"Asia/Calcutta": "IN",
|
|
231
|
+
"Asia/Dacca": "BD",
|
|
232
|
+
"Asia/Dhaka": "BD",
|
|
233
|
+
"Asia/Dubai": "AE",
|
|
234
|
+
"Asia/Hong_Kong": "HK",
|
|
235
|
+
"Asia/Ho_Chi_Minh": "VN",
|
|
236
|
+
"Asia/Jakarta": "ID",
|
|
237
|
+
"Asia/Jerusalem": "IL",
|
|
238
|
+
"Asia/Karachi": "PK",
|
|
239
|
+
"Asia/Kolkata": "IN",
|
|
240
|
+
"Asia/Kuala_Lumpur": "MY",
|
|
241
|
+
"Asia/Kuwait": "KW",
|
|
242
|
+
"Asia/Manila": "PH",
|
|
243
|
+
"Asia/Qatar": "QA",
|
|
244
|
+
"Asia/Riyadh": "SA",
|
|
245
|
+
"Asia/Seoul": "KR",
|
|
246
|
+
"Asia/Shanghai": "CN",
|
|
247
|
+
"Asia/Saigon": "VN",
|
|
248
|
+
"Asia/Singapore": "SG",
|
|
249
|
+
"Asia/Taipei": "TW",
|
|
250
|
+
"Asia/Tel_Aviv": "IL",
|
|
251
|
+
"Asia/Tokyo": "JP",
|
|
252
|
+
"Asia/Urumqi": "CN",
|
|
253
|
+
"Atlantic/Reykjavik": "IS",
|
|
254
|
+
"Australia/Adelaide": "AU",
|
|
255
|
+
"Australia/Brisbane": "AU",
|
|
256
|
+
"Australia/Darwin": "AU",
|
|
257
|
+
"Australia/Hobart": "AU",
|
|
258
|
+
"Australia/Melbourne": "AU",
|
|
259
|
+
"Australia/Perth": "AU",
|
|
260
|
+
"Australia/Sydney": "AU",
|
|
261
|
+
"Europe/Amsterdam": "NL",
|
|
262
|
+
"Europe/Athens": "GR",
|
|
263
|
+
"Europe/Belgrade": "RS",
|
|
264
|
+
"Europe/Berlin": "DE",
|
|
265
|
+
"Europe/Bratislava": "SK",
|
|
266
|
+
"Europe/Brussels": "BE",
|
|
267
|
+
"Europe/Bucharest": "RO",
|
|
268
|
+
"Europe/Budapest": "HU",
|
|
269
|
+
"Europe/Copenhagen": "DK",
|
|
270
|
+
"Europe/Dublin": "IE",
|
|
271
|
+
"Europe/Helsinki": "FI",
|
|
272
|
+
"Europe/Zagreb": "HR",
|
|
273
|
+
"Europe/Istanbul": "TR",
|
|
274
|
+
"Europe/Kiev": "UA",
|
|
275
|
+
"Europe/Kyiv": "UA",
|
|
276
|
+
"Europe/Lisbon": "PT",
|
|
277
|
+
"Europe/Ljubljana": "SI",
|
|
278
|
+
"Europe/London": "GB",
|
|
279
|
+
"Europe/Luxembourg": "LU",
|
|
280
|
+
"Europe/Madrid": "ES",
|
|
281
|
+
"Europe/Moscow": "RU",
|
|
282
|
+
"Europe/Oslo": "NO",
|
|
283
|
+
"Europe/Paris": "FR",
|
|
284
|
+
"Europe/Prague": "CZ",
|
|
285
|
+
"Europe/Riga": "LV",
|
|
286
|
+
"Europe/Rome": "IT",
|
|
287
|
+
"Europe/Sofia": "BG",
|
|
288
|
+
"Europe/Stockholm": "SE",
|
|
289
|
+
"Europe/Tallinn": "EE",
|
|
290
|
+
"Europe/Vienna": "AT",
|
|
291
|
+
"Europe/Vilnius": "LT",
|
|
292
|
+
"Europe/Warsaw": "PL",
|
|
293
|
+
"Europe/Zurich": "CH",
|
|
294
|
+
"Pacific/Auckland": "NZ"
|
|
295
|
+
};
|
|
132
296
|
function flagEmoji(iso2) {
|
|
133
297
|
const cc = iso2.toUpperCase();
|
|
134
298
|
if (!/^[A-Z]{2}$/.test(cc)) return "\u{1F3F3}\uFE0F";
|
|
@@ -137,6 +301,9 @@ function flagEmoji(iso2) {
|
|
|
137
301
|
function countryByIso(iso2) {
|
|
138
302
|
return iso2 ? byIso.get(iso2.toUpperCase()) : void 0;
|
|
139
303
|
}
|
|
304
|
+
function countryByTimeZone(timeZone) {
|
|
305
|
+
return countryByIso(timeZone ? timeZoneIso[timeZone] : void 0);
|
|
306
|
+
}
|
|
140
307
|
function countryByDialCode(digits) {
|
|
141
308
|
let best;
|
|
142
309
|
for (const c of PHONE_COUNTRIES) {
|
|
@@ -200,11 +367,11 @@ function isPossiblePhone(value) {
|
|
|
200
367
|
|
|
201
368
|
// src/validation.ts
|
|
202
369
|
var fileValueSchema = z.object({
|
|
203
|
-
fileId: z.string().
|
|
370
|
+
fileId: z.string().check(z.minLength(1)),
|
|
204
371
|
name: z.string(),
|
|
205
|
-
size: z.number().nonnegative(),
|
|
372
|
+
size: z.number().check(z.nonnegative()),
|
|
206
373
|
mime: z.string(),
|
|
207
|
-
url: z.
|
|
374
|
+
url: z.optional(z.string())
|
|
208
375
|
});
|
|
209
376
|
function isEmpty(value) {
|
|
210
377
|
if (value === null || value === void 0) return true;
|
|
@@ -214,18 +381,18 @@ function isEmpty(value) {
|
|
|
214
381
|
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
215
382
|
return false;
|
|
216
383
|
}
|
|
217
|
-
function validateField(
|
|
384
|
+
function validateField(field2, value) {
|
|
218
385
|
if (isEmpty(value)) {
|
|
219
|
-
return
|
|
386
|
+
return field2.required ? "This field is required" : null;
|
|
220
387
|
}
|
|
221
|
-
switch (
|
|
388
|
+
switch (field2.kind) {
|
|
222
389
|
case "short_text":
|
|
223
390
|
case "long_text": {
|
|
224
391
|
if (typeof value !== "string") return "Expected text";
|
|
225
|
-
if (
|
|
226
|
-
return `Must be at most ${
|
|
227
|
-
const hardCap =
|
|
228
|
-
if (!
|
|
392
|
+
if (field2.maxLength && value.length > field2.maxLength)
|
|
393
|
+
return `Must be at most ${field2.maxLength} characters`;
|
|
394
|
+
const hardCap = field2.kind === "long_text" ? 2e4 : 2e3;
|
|
395
|
+
if (!field2.maxLength && value.length > hardCap) return "Answer is too long";
|
|
229
396
|
return null;
|
|
230
397
|
}
|
|
231
398
|
case "email": {
|
|
@@ -251,26 +418,26 @@ function validateField(field, value) {
|
|
|
251
418
|
}
|
|
252
419
|
case "number": {
|
|
253
420
|
const n = typeof value === "string" ? Number(value) : value;
|
|
254
|
-
if (typeof n !== "number" || Number.
|
|
255
|
-
if (
|
|
256
|
-
if (
|
|
421
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return "Enter a number";
|
|
422
|
+
if (field2.min !== void 0 && n < field2.min) return `Must be at least ${field2.min}`;
|
|
423
|
+
if (field2.max !== void 0 && n > field2.max) return `Must be at most ${field2.max}`;
|
|
257
424
|
return null;
|
|
258
425
|
}
|
|
259
426
|
case "select":
|
|
260
427
|
case "dropdown": {
|
|
261
428
|
if (typeof value !== "string" || value === "") return "Pick an option";
|
|
262
|
-
if (
|
|
263
|
-
if (
|
|
429
|
+
if (field2.options.some((o) => o.id === value)) return null;
|
|
430
|
+
if (field2.allowOther && value.length <= 500) return null;
|
|
264
431
|
return "Pick a valid option";
|
|
265
432
|
}
|
|
266
433
|
case "multi_select": {
|
|
267
434
|
if (!Array.isArray(value)) return "Pick at least one option";
|
|
268
|
-
const ids = new Set(
|
|
435
|
+
const ids = new Set(field2.options.map((o) => o.id));
|
|
269
436
|
const arr = value;
|
|
270
437
|
let others = 0;
|
|
271
438
|
for (const v of arr) {
|
|
272
439
|
if (ids.has(v)) continue;
|
|
273
|
-
if (
|
|
440
|
+
if (field2.allowOther !== true || typeof v !== "string" || v.length === 0 || v.length > 500)
|
|
274
441
|
return "Pick valid options";
|
|
275
442
|
if (++others > 1) return "Only one 'Other' answer is allowed";
|
|
276
443
|
}
|
|
@@ -280,15 +447,15 @@ function validateField(field, value) {
|
|
|
280
447
|
return typeof value === "boolean" ? null : "Invalid value";
|
|
281
448
|
}
|
|
282
449
|
case "rating": {
|
|
283
|
-
const max =
|
|
450
|
+
const max = field2.max ?? 5;
|
|
284
451
|
const n = typeof value === "string" ? Number(value) : value;
|
|
285
452
|
if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > max)
|
|
286
453
|
return `Pick a rating between 1 and ${max}`;
|
|
287
454
|
return null;
|
|
288
455
|
}
|
|
289
456
|
case "linear_scale": {
|
|
290
|
-
const min =
|
|
291
|
-
const max =
|
|
457
|
+
const min = field2.min ?? 1;
|
|
458
|
+
const max = field2.max ?? 10;
|
|
292
459
|
const n = typeof value === "string" ? Number(value) : value;
|
|
293
460
|
if (typeof n !== "number" || !Number.isInteger(n) || n < min || n > max)
|
|
294
461
|
return `Pick a value between ${min} and ${max}`;
|
|
@@ -296,7 +463,7 @@ function validateField(field, value) {
|
|
|
296
463
|
}
|
|
297
464
|
case "ranking": {
|
|
298
465
|
if (!Array.isArray(value)) return "Drag the options into order";
|
|
299
|
-
const ids =
|
|
466
|
+
const ids = field2.options.map((o) => o.id).sort();
|
|
300
467
|
const got = [...value].sort();
|
|
301
468
|
if (ids.length !== got.length || ids.some((id, i) => id !== got[i]))
|
|
302
469
|
return "Rank every option";
|
|
@@ -306,12 +473,12 @@ function validateField(field, value) {
|
|
|
306
473
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
307
474
|
return "Answer the grid";
|
|
308
475
|
const answers = value;
|
|
309
|
-
const columnIds = new Set(
|
|
310
|
-
const rowIds = new Set(
|
|
476
|
+
const columnIds = new Set(field2.columns.map((c) => c.id));
|
|
477
|
+
const rowIds = new Set(field2.rows.map((r) => r.id));
|
|
311
478
|
for (const [rowId, columnId] of Object.entries(answers)) {
|
|
312
479
|
if (!rowIds.has(rowId) || !columnIds.has(columnId)) return "Invalid grid answer";
|
|
313
480
|
}
|
|
314
|
-
if (
|
|
481
|
+
if (field2.required && Object.keys(answers).length < field2.rows.length)
|
|
315
482
|
return "Answer every row";
|
|
316
483
|
return null;
|
|
317
484
|
}
|
|
@@ -329,12 +496,16 @@ function validateField(field, value) {
|
|
|
329
496
|
case "date": {
|
|
330
497
|
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
331
498
|
return "Enter a valid date";
|
|
499
|
+
const [y, m, d] = value.split("-").map(Number);
|
|
500
|
+
const dt = new Date(Date.UTC(y, m - 1, d));
|
|
501
|
+
if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== m - 1 || dt.getUTCDate() !== d)
|
|
502
|
+
return "Enter a valid date";
|
|
332
503
|
return null;
|
|
333
504
|
}
|
|
334
505
|
case "file_upload": {
|
|
335
506
|
const r = z.array(fileValueSchema).safeParse(value);
|
|
336
507
|
if (!r.success) return "Upload did not complete";
|
|
337
|
-
const maxFiles =
|
|
508
|
+
const maxFiles = field2.maxFiles ?? 1;
|
|
338
509
|
if (r.data.length > maxFiles)
|
|
339
510
|
return `At most ${maxFiles} file${maxFiles === 1 ? "" : "s"}`;
|
|
340
511
|
return null;
|
|
@@ -345,24 +516,24 @@ function validateResponse(form, data) {
|
|
|
345
516
|
const fields = visibleFields(form, data);
|
|
346
517
|
const errors = {};
|
|
347
518
|
const cleaned = {};
|
|
348
|
-
for (const
|
|
349
|
-
const value = normalizeValue(
|
|
350
|
-
const error = validateField(
|
|
519
|
+
for (const field2 of fields) {
|
|
520
|
+
const value = normalizeValue(field2, data[field2.id]);
|
|
521
|
+
const error = validateField(field2, value);
|
|
351
522
|
if (error) {
|
|
352
|
-
errors[
|
|
523
|
+
errors[field2.id] = error;
|
|
353
524
|
} else if (!isEmpty(value)) {
|
|
354
|
-
cleaned[
|
|
525
|
+
cleaned[field2.id] = value;
|
|
355
526
|
}
|
|
356
527
|
}
|
|
357
528
|
return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
|
|
358
529
|
}
|
|
359
|
-
function normalizeValue(
|
|
530
|
+
function normalizeValue(field2, value) {
|
|
360
531
|
if (value === void 0 || value === null) return value;
|
|
361
|
-
if (
|
|
532
|
+
if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
|
|
362
533
|
const n = Number(value);
|
|
363
534
|
return Number.isNaN(n) ? value : n;
|
|
364
535
|
}
|
|
365
|
-
if (
|
|
536
|
+
if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
|
|
366
537
|
const n = Number(value);
|
|
367
538
|
return Number.isNaN(n) ? value : n;
|
|
368
539
|
}
|
|
@@ -371,7 +542,7 @@ function normalizeValue(field, value) {
|
|
|
371
542
|
}
|
|
372
543
|
|
|
373
544
|
// src/schema-validation.ts
|
|
374
|
-
import
|
|
545
|
+
import * as z2 from "zod/mini";
|
|
375
546
|
var FIELD_KINDS = [
|
|
376
547
|
"short_text",
|
|
377
548
|
"long_text",
|
|
@@ -395,27 +566,31 @@ var FIELD_KINDS = [
|
|
|
395
566
|
];
|
|
396
567
|
var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
|
|
397
568
|
var ALL_KINDS = [...FIELD_KINDS, ...CONTENT_KINDS2];
|
|
398
|
-
var idSchema = z2.string().
|
|
399
|
-
var optionSchema = z2.
|
|
400
|
-
|
|
569
|
+
var idSchema = z2.string().check(z2.minLength(1), z2.maxLength(128));
|
|
570
|
+
var optionSchema = z2.looseObject({
|
|
571
|
+
id: idSchema,
|
|
572
|
+
label: z2.string().check(z2.maxLength(1e3)),
|
|
573
|
+
icon: z2.optional(z2.unknown())
|
|
574
|
+
});
|
|
575
|
+
var blockSchema = z2.looseObject({
|
|
401
576
|
id: idSchema,
|
|
402
577
|
kind: z2.enum(ALL_KINDS),
|
|
403
|
-
label: z2.string().
|
|
404
|
-
options: z2.array(optionSchema).
|
|
405
|
-
rows: z2.array(optionSchema).
|
|
406
|
-
columns: z2.array(optionSchema).
|
|
407
|
-
})
|
|
578
|
+
label: z2.optional(z2.string().check(z2.maxLength(2e3))),
|
|
579
|
+
options: z2.optional(z2.array(optionSchema).check(z2.maxLength(200))),
|
|
580
|
+
rows: z2.optional(z2.array(optionSchema).check(z2.maxLength(200))),
|
|
581
|
+
columns: z2.optional(z2.array(optionSchema).check(z2.maxLength(200)))
|
|
582
|
+
});
|
|
408
583
|
var pageSchema = z2.object({
|
|
409
584
|
id: idSchema,
|
|
410
|
-
title: z2.string().
|
|
411
|
-
blocks: z2.array(blockSchema).
|
|
585
|
+
title: z2.optional(z2.string().check(z2.maxLength(500))),
|
|
586
|
+
blocks: z2.array(blockSchema).check(z2.maxLength(500))
|
|
412
587
|
});
|
|
413
588
|
var schemaShape = z2.object({
|
|
414
589
|
version: z2.literal(1),
|
|
415
|
-
title: z2.string().
|
|
416
|
-
description: z2.string().
|
|
417
|
-
pages: z2.array(pageSchema).
|
|
418
|
-
settings: z2.record(z2.string(), z2.unknown())
|
|
590
|
+
title: z2._default(z2.optional(z2.string().check(z2.maxLength(2e3))), ""),
|
|
591
|
+
description: z2.optional(z2.string().check(z2.maxLength(5e3))),
|
|
592
|
+
pages: z2.array(pageSchema).check(z2.minLength(1), z2.maxLength(50)),
|
|
593
|
+
settings: z2._default(z2.optional(z2.record(z2.string(), z2.unknown())), {})
|
|
419
594
|
});
|
|
420
595
|
var MAX_SCHEMA_VERSION = 1;
|
|
421
596
|
var FILLO_SCHEMA_VERSION = 1;
|
|
@@ -505,7 +680,7 @@ function normalizeSettings(value) {
|
|
|
505
680
|
redirectUrl: normalizeUrl(rec.redirectUrl),
|
|
506
681
|
showProgress: bool(rec.showProgress),
|
|
507
682
|
submissionLimit,
|
|
508
|
-
notifyEmail: z2.email().
|
|
683
|
+
notifyEmail: z2.email().check(z2.maxLength(254)).safeParse(rec.notifyEmail).success ? rec.notifyEmail : void 0,
|
|
509
684
|
sendReceipt: bool(rec.sendReceipt)
|
|
510
685
|
};
|
|
511
686
|
}
|
|
@@ -694,21 +869,26 @@ function normalizeFormTheme(input) {
|
|
|
694
869
|
function assertNever(x) {
|
|
695
870
|
throw new Error(`Unhandled field kind: ${JSON.stringify(x)}`);
|
|
696
871
|
}
|
|
697
|
-
function formatAnswer(
|
|
872
|
+
function formatAnswer(field2, value) {
|
|
698
873
|
if (value === null || value === void 0 || value === "") return "";
|
|
699
|
-
switch (
|
|
874
|
+
switch (field2.kind) {
|
|
700
875
|
case "file_upload":
|
|
876
|
+
if (!Array.isArray(value)) return String(value);
|
|
701
877
|
return value.map((f) => f.name).join(", ");
|
|
702
878
|
case "select":
|
|
703
879
|
case "dropdown":
|
|
704
|
-
return
|
|
880
|
+
return field2.options.find((o) => o.id === value)?.label ?? String(value);
|
|
705
881
|
case "multi_select":
|
|
706
|
-
|
|
882
|
+
if (!Array.isArray(value)) return String(value);
|
|
883
|
+
return value.map((id) => field2.options.find((o) => o.id === id)?.label ?? id).join(", ");
|
|
707
884
|
case "ranking":
|
|
708
|
-
|
|
885
|
+
if (!Array.isArray(value)) return String(value);
|
|
886
|
+
return value.map((id, i) => `${i + 1}. ${field2.options.find((o) => o.id === id)?.label ?? id}`).join(", ");
|
|
709
887
|
case "matrix": {
|
|
888
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
889
|
+
return String(value);
|
|
710
890
|
const answers = value;
|
|
711
|
-
return
|
|
891
|
+
return field2.rows.filter((row) => answers[row.id]).map((row) => `${row.label}: ${field2.columns.find((c) => c.id === answers[row.id])?.label ?? ""}`).join(", ");
|
|
712
892
|
}
|
|
713
893
|
case "signature":
|
|
714
894
|
return typeof value === "string" && value.startsWith("data:image/") ? "\u270D signed" : "";
|
|
@@ -735,7 +915,7 @@ function formatAnswer(field, value) {
|
|
|
735
915
|
case "custom":
|
|
736
916
|
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
737
917
|
default:
|
|
738
|
-
return assertNever(
|
|
918
|
+
return assertNever(field2);
|
|
739
919
|
}
|
|
740
920
|
}
|
|
741
921
|
|
|
@@ -1075,14 +1255,40 @@ var sha1Base64 = (bytes) => bytesToBase64(new Sha1().update(bytes).digest());
|
|
|
1075
1255
|
|
|
1076
1256
|
// src/client.ts
|
|
1077
1257
|
var FilloError = class extends Error {
|
|
1078
|
-
constructor(message, status) {
|
|
1258
|
+
constructor(message, status, retryAfterSec2) {
|
|
1079
1259
|
super(message);
|
|
1080
1260
|
this.status = status;
|
|
1261
|
+
this.retryAfterSec = retryAfterSec2;
|
|
1081
1262
|
this.name = "FilloError";
|
|
1082
1263
|
}
|
|
1083
1264
|
status;
|
|
1265
|
+
retryAfterSec;
|
|
1084
1266
|
};
|
|
1267
|
+
function isFilloError(err) {
|
|
1268
|
+
return err instanceof Error && err.name === "FilloError";
|
|
1269
|
+
}
|
|
1270
|
+
async function errorMessage(res, fallback) {
|
|
1271
|
+
try {
|
|
1272
|
+
const body = await res.json();
|
|
1273
|
+
if (typeof body.error === "string" && body.error) return body.error;
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
return fallback;
|
|
1277
|
+
}
|
|
1278
|
+
function retryAfterSec(res) {
|
|
1279
|
+
const raw = Number(res.headers.get("retry-after"));
|
|
1280
|
+
return Number.isFinite(raw) && raw > 0 ? raw : void 0;
|
|
1281
|
+
}
|
|
1085
1282
|
var MAX_CHUNK_RETRIES = 3;
|
|
1283
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1284
|
+
function isRetryableStatus(status) {
|
|
1285
|
+
return status >= 500 || status === 429 || status === 408;
|
|
1286
|
+
}
|
|
1287
|
+
function asRequestError(err) {
|
|
1288
|
+
if (err instanceof DOMException && err.name === "TimeoutError")
|
|
1289
|
+
return new FilloError("Request timed out", 0);
|
|
1290
|
+
return err;
|
|
1291
|
+
}
|
|
1086
1292
|
function compareVersions(a, b) {
|
|
1087
1293
|
const left = a.split(".").map((part) => Number(part) || 0);
|
|
1088
1294
|
const right = b.split(".").map((part) => Number(part) || 0);
|
|
@@ -1122,6 +1328,7 @@ async function digestPartBase64(bytes) {
|
|
|
1122
1328
|
return sha1Base64(bytes);
|
|
1123
1329
|
}
|
|
1124
1330
|
var DEFAULT_BASE_URL = "https://fillo.so";
|
|
1331
|
+
var CLIENT_HEADER = { "X-Fillo-Client": `@usefillo/core@${FILLO_SDK_VERSION}` };
|
|
1125
1332
|
var FilloClient = class {
|
|
1126
1333
|
/** Server origin this client targets, normalized (no trailing slash). */
|
|
1127
1334
|
baseUrl;
|
|
@@ -1129,7 +1336,7 @@ var FilloClient = class {
|
|
|
1129
1336
|
/** Publishable workspace key, when configured. */
|
|
1130
1337
|
key;
|
|
1131
1338
|
constructor(options2 = {}) {
|
|
1132
|
-
this.baseUrl = options2.sameOrigin ? "" : DEFAULT_BASE_URL;
|
|
1339
|
+
this.baseUrl = options2.baseUrl ? options2.baseUrl.replace(/\/$/, "") : options2.sameOrigin ? "" : DEFAULT_BASE_URL;
|
|
1133
1340
|
this.key = options2.key;
|
|
1134
1341
|
this.fetch = options2.fetch ?? ((...args) => globalThis.fetch(...args));
|
|
1135
1342
|
}
|
|
@@ -1137,13 +1344,20 @@ var FilloClient = class {
|
|
|
1137
1344
|
return `${this.baseUrl}/api/v1${path}`;
|
|
1138
1345
|
}
|
|
1139
1346
|
async json(path, init) {
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1347
|
+
const headers = init?.body != null ? { "Content-Type": "application/json", ...CLIENT_HEADER, ...init?.headers } : init?.headers;
|
|
1348
|
+
let res;
|
|
1349
|
+
try {
|
|
1350
|
+
res = await this.fetch(this.url(path), {
|
|
1351
|
+
...init,
|
|
1352
|
+
headers,
|
|
1353
|
+
signal: init?.signal ?? AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
1354
|
+
});
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
throw asRequestError(err);
|
|
1357
|
+
}
|
|
1144
1358
|
if (!res.ok) {
|
|
1145
|
-
const
|
|
1146
|
-
throw new FilloError(
|
|
1359
|
+
const message = await errorMessage(res, `Request failed: ${res.status}`);
|
|
1360
|
+
throw new FilloError(message, res.status, retryAfterSec(res));
|
|
1147
1361
|
}
|
|
1148
1362
|
return await res.json();
|
|
1149
1363
|
}
|
|
@@ -1179,17 +1393,24 @@ var FilloClient = class {
|
|
|
1179
1393
|
}
|
|
1180
1394
|
/** Submit a response. Returns per-field errors instead of throwing on validation failure. */
|
|
1181
1395
|
async submit(formId, data, meta) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1396
|
+
let res;
|
|
1397
|
+
try {
|
|
1398
|
+
res = await this.fetch(this.url(`/forms/${encodeURIComponent(formId)}/responses`), {
|
|
1399
|
+
method: "POST",
|
|
1400
|
+
headers: { "Content-Type": "application/json", ...CLIENT_HEADER },
|
|
1401
|
+
body: JSON.stringify({ data, meta }),
|
|
1402
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
1403
|
+
});
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
throw asRequestError(err);
|
|
1406
|
+
}
|
|
1187
1407
|
if (res.status === 422) {
|
|
1188
1408
|
const body2 = await res.json();
|
|
1189
1409
|
return { ok: false, errors: body2.errors };
|
|
1190
1410
|
}
|
|
1191
1411
|
if (!res.ok) {
|
|
1192
|
-
|
|
1412
|
+
const message = await errorMessage(res, `Submit failed: ${res.status}`);
|
|
1413
|
+
throw new FilloError(message, res.status, retryAfterSec(res));
|
|
1193
1414
|
}
|
|
1194
1415
|
const body = await res.json();
|
|
1195
1416
|
return { ok: true, responseId: body.id, duplicate: body.duplicate };
|
|
@@ -1203,7 +1424,8 @@ var FilloClient = class {
|
|
|
1203
1424
|
const res = await this.fetch(this.url(`/forms/${encodeURIComponent(formId)}/sessions`), {
|
|
1204
1425
|
method: "POST",
|
|
1205
1426
|
headers: { "Content-Type": "application/json" },
|
|
1206
|
-
body: JSON.stringify({ pageCount })
|
|
1427
|
+
body: JSON.stringify({ pageCount }),
|
|
1428
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
1207
1429
|
});
|
|
1208
1430
|
if (!res.ok) return null;
|
|
1209
1431
|
const body = await res.json();
|
|
@@ -1219,7 +1441,8 @@ var FilloClient = class {
|
|
|
1219
1441
|
method: "POST",
|
|
1220
1442
|
headers: { "Content-Type": "application/json" },
|
|
1221
1443
|
body: JSON.stringify(data),
|
|
1222
|
-
keepalive: true
|
|
1444
|
+
keepalive: true,
|
|
1445
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
1223
1446
|
}).catch(() => {
|
|
1224
1447
|
});
|
|
1225
1448
|
} catch {
|
|
@@ -1303,10 +1526,17 @@ var FilloClient = class {
|
|
|
1303
1526
|
JSON.stringify({ name: transport.fileName, parent: { id: transport.folderId } })
|
|
1304
1527
|
);
|
|
1305
1528
|
form.append("file", file, transport.fileName);
|
|
1306
|
-
const res = await this.retry(
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1529
|
+
const res = await this.retry(options2, async () => {
|
|
1530
|
+
const r = await this.fetch(transport.uploadUrl, {
|
|
1531
|
+
method: "POST",
|
|
1532
|
+
headers: auth,
|
|
1533
|
+
body: form,
|
|
1534
|
+
signal: this.uploadSignal(options2)
|
|
1535
|
+
});
|
|
1536
|
+
if (!r.ok && isRetryableStatus(r.status))
|
|
1537
|
+
throw new FilloError(`Box upload failed: ${r.status}`, r.status);
|
|
1538
|
+
return r;
|
|
1539
|
+
});
|
|
1310
1540
|
if (!res.ok) throw new FilloError(`Box upload failed: ${res.status}`, res.status);
|
|
1311
1541
|
const body = await res.json();
|
|
1312
1542
|
report(file.size);
|
|
@@ -1318,9 +1548,8 @@ var FilloClient = class {
|
|
|
1318
1548
|
400
|
|
1319
1549
|
);
|
|
1320
1550
|
}
|
|
1321
|
-
const sessionRes = await this.retry(
|
|
1322
|
-
|
|
1323
|
-
() => this.fetch(transport.sessionUrl, {
|
|
1551
|
+
const sessionRes = await this.retry(options2, async () => {
|
|
1552
|
+
const r = await this.fetch(transport.sessionUrl, {
|
|
1324
1553
|
method: "POST",
|
|
1325
1554
|
headers: { ...auth, "Content-Type": "application/json" },
|
|
1326
1555
|
body: JSON.stringify({
|
|
@@ -1328,9 +1557,12 @@ var FilloClient = class {
|
|
|
1328
1557
|
file_size: transport.size,
|
|
1329
1558
|
file_name: transport.fileName
|
|
1330
1559
|
}),
|
|
1331
|
-
signal: options2
|
|
1332
|
-
})
|
|
1333
|
-
|
|
1560
|
+
signal: this.uploadSignal(options2)
|
|
1561
|
+
});
|
|
1562
|
+
if (!r.ok && isRetryableStatus(r.status))
|
|
1563
|
+
throw new FilloError(`Box session failed: ${r.status}`, r.status);
|
|
1564
|
+
return r;
|
|
1565
|
+
});
|
|
1334
1566
|
if (!sessionRes.ok) throw new FilloError(`Box session failed: ${sessionRes.status}`, sessionRes.status);
|
|
1335
1567
|
const session = await sessionRes.json();
|
|
1336
1568
|
const whole = new Sha1();
|
|
@@ -1342,9 +1574,8 @@ var FilloClient = class {
|
|
|
1342
1574
|
const bytes = new Uint8Array(await file.slice(offset, end).arrayBuffer());
|
|
1343
1575
|
whole.update(bytes);
|
|
1344
1576
|
const partDigest = await digestPartBase64(bytes);
|
|
1345
|
-
const res = await this.retry(
|
|
1346
|
-
|
|
1347
|
-
() => this.fetch(session.session_endpoints.upload_part, {
|
|
1577
|
+
const res = await this.retry(options2, async () => {
|
|
1578
|
+
const r = await this.fetch(session.session_endpoints.upload_part, {
|
|
1348
1579
|
method: "PUT",
|
|
1349
1580
|
headers: {
|
|
1350
1581
|
...auth,
|
|
@@ -1353,27 +1584,41 @@ var FilloClient = class {
|
|
|
1353
1584
|
"Content-Range": `bytes ${offset}-${end - 1}/${file.size}`
|
|
1354
1585
|
},
|
|
1355
1586
|
body: bytes,
|
|
1356
|
-
signal: options2
|
|
1357
|
-
})
|
|
1358
|
-
|
|
1587
|
+
signal: this.uploadSignal(options2)
|
|
1588
|
+
});
|
|
1589
|
+
if (!r.ok && isRetryableStatus(r.status))
|
|
1590
|
+
throw new FilloError(`Box part failed: ${r.status}`, r.status);
|
|
1591
|
+
return r;
|
|
1592
|
+
});
|
|
1359
1593
|
if (!res.ok) throw new FilloError(`Box part failed: ${res.status}`, res.status);
|
|
1360
1594
|
parts.push((await res.json()).part);
|
|
1361
1595
|
offset = end;
|
|
1362
1596
|
report(offset);
|
|
1363
1597
|
}
|
|
1364
|
-
const commitRes = await this.retry(
|
|
1365
|
-
|
|
1366
|
-
() => this.fetch(session.session_endpoints.commit, {
|
|
1598
|
+
const commitRes = await this.retry(options2, async () => {
|
|
1599
|
+
const r = await this.fetch(session.session_endpoints.commit, {
|
|
1367
1600
|
method: "POST",
|
|
1368
1601
|
headers: { ...auth, "Content-Type": "application/json", Digest: `sha=${bytesToBase64(whole.digest())}` },
|
|
1369
1602
|
body: JSON.stringify({ parts }),
|
|
1370
|
-
signal: options2
|
|
1371
|
-
})
|
|
1372
|
-
|
|
1603
|
+
signal: this.uploadSignal(options2)
|
|
1604
|
+
});
|
|
1605
|
+
if (!r.ok && isRetryableStatus(r.status))
|
|
1606
|
+
throw new FilloError(`Box commit failed: ${r.status}`, r.status);
|
|
1607
|
+
return r;
|
|
1608
|
+
});
|
|
1373
1609
|
if (!commitRes.ok) throw new FilloError(`Box commit failed: ${commitRes.status}`, commitRes.status);
|
|
1374
1610
|
const committed = await commitRes.json();
|
|
1375
1611
|
return { providerFileId: committed.entries?.[0]?.id };
|
|
1376
1612
|
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Caller signal (if any) combined with a fresh per-request timeout. Created per
|
|
1615
|
+
* fetch so each retry attempt gets its own deadline; the caller's own abort
|
|
1616
|
+
* still drives the resumable-upload cancel semantics.
|
|
1617
|
+
*/
|
|
1618
|
+
uploadSignal(options2) {
|
|
1619
|
+
const timeout = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
1620
|
+
return options2.signal ? AbortSignal.any([options2.signal, timeout]) : timeout;
|
|
1621
|
+
}
|
|
1377
1622
|
/** Retry a request with exponential backoff; never retries an aborted upload. */
|
|
1378
1623
|
async retry(options2, fn) {
|
|
1379
1624
|
let attempt = 0;
|
|
@@ -1398,10 +1643,12 @@ var FilloClient = class {
|
|
|
1398
1643
|
let offset = session.uploadedBytes;
|
|
1399
1644
|
const report = makeReporter(file.size, options2);
|
|
1400
1645
|
report(offset);
|
|
1646
|
+
let stalls = 0;
|
|
1401
1647
|
while (offset < file.size) {
|
|
1402
1648
|
options2.signal?.throwIfAborted();
|
|
1403
|
-
const
|
|
1404
|
-
|
|
1649
|
+
const startedAt = offset;
|
|
1650
|
+
let end = Math.min(offset + session.chunkSize, file.size);
|
|
1651
|
+
let chunk = file.slice(offset, end);
|
|
1405
1652
|
let attempt = 0;
|
|
1406
1653
|
for (; ; ) {
|
|
1407
1654
|
try {
|
|
@@ -1411,7 +1658,7 @@ var FilloClient = class {
|
|
|
1411
1658
|
"Content-Range": `bytes ${offset}-${end - 1}/${file.size}`
|
|
1412
1659
|
},
|
|
1413
1660
|
body: chunk,
|
|
1414
|
-
signal: options2
|
|
1661
|
+
signal: this.uploadSignal(options2)
|
|
1415
1662
|
});
|
|
1416
1663
|
if (res.status === 308) {
|
|
1417
1664
|
const range = res.headers.get("Range") ?? res.headers.get("range");
|
|
@@ -1433,9 +1680,19 @@ var FilloClient = class {
|
|
|
1433
1680
|
await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
|
|
1434
1681
|
const fresh = await this.getUploadSession(session.id, session.token);
|
|
1435
1682
|
offset = fresh.uploadedBytes;
|
|
1683
|
+
if (offset >= file.size) break;
|
|
1684
|
+
end = Math.min(offset + session.chunkSize, file.size);
|
|
1685
|
+
chunk = file.slice(offset, end);
|
|
1436
1686
|
}
|
|
1437
1687
|
}
|
|
1438
1688
|
report(offset);
|
|
1689
|
+
if (offset <= startedAt) {
|
|
1690
|
+
if (++stalls >= MAX_CHUNK_RETRIES) {
|
|
1691
|
+
throw new FilloError("Drive upload stalled \u2014 the storage endpoint stopped accepting progress", 0);
|
|
1692
|
+
}
|
|
1693
|
+
} else {
|
|
1694
|
+
stalls = 0;
|
|
1695
|
+
}
|
|
1439
1696
|
}
|
|
1440
1697
|
}
|
|
1441
1698
|
/**
|
|
@@ -1452,7 +1709,7 @@ var FilloClient = class {
|
|
|
1452
1709
|
method: "PUT",
|
|
1453
1710
|
headers: { "Content-Type": file.type || "application/octet-stream" },
|
|
1454
1711
|
body: file,
|
|
1455
|
-
signal: options2
|
|
1712
|
+
signal: this.uploadSignal(options2)
|
|
1456
1713
|
});
|
|
1457
1714
|
if (!res.ok) throw new FilloError(`S3 upload failed: ${res.status}`, res.status);
|
|
1458
1715
|
report(file.size);
|
|
@@ -1484,6 +1741,68 @@ async function provisionWorkspace(options2) {
|
|
|
1484
1741
|
return await res.json();
|
|
1485
1742
|
}
|
|
1486
1743
|
|
|
1744
|
+
// src/prefill.ts
|
|
1745
|
+
function parseDecimal(raw) {
|
|
1746
|
+
const s = raw.trim();
|
|
1747
|
+
if (!/^-?\d*\.?\d+$/.test(s)) return null;
|
|
1748
|
+
const n = Number(s);
|
|
1749
|
+
return Number.isFinite(n) ? n : null;
|
|
1750
|
+
}
|
|
1751
|
+
function prefillFromParams(form, params) {
|
|
1752
|
+
const data = {};
|
|
1753
|
+
for (const field2 of allFields(form)) {
|
|
1754
|
+
const raw = field2.kind === "hidden" ? params[field2.paramName ?? field2.id] ?? field2.defaultValue : params[field2.id];
|
|
1755
|
+
if (raw === void 0 || raw === "") continue;
|
|
1756
|
+
switch (field2.kind) {
|
|
1757
|
+
case "number": {
|
|
1758
|
+
const n = parseDecimal(raw);
|
|
1759
|
+
if (n === null) break;
|
|
1760
|
+
if (field2.min !== void 0 && n < field2.min) break;
|
|
1761
|
+
if (field2.max !== void 0 && n > field2.max) break;
|
|
1762
|
+
data[field2.id] = n;
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
case "rating": {
|
|
1766
|
+
const n = parseDecimal(raw);
|
|
1767
|
+
if (n !== null && Number.isInteger(n) && n >= 1 && n <= (field2.max ?? 5))
|
|
1768
|
+
data[field2.id] = n;
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
case "linear_scale": {
|
|
1772
|
+
const n = parseDecimal(raw);
|
|
1773
|
+
const min = field2.min ?? 1;
|
|
1774
|
+
const max = field2.max ?? 10;
|
|
1775
|
+
if (n !== null && Number.isInteger(n) && n >= min && n <= max) data[field2.id] = n;
|
|
1776
|
+
break;
|
|
1777
|
+
}
|
|
1778
|
+
case "checkbox":
|
|
1779
|
+
data[field2.id] = raw === "true" || raw === "1" || raw === "yes";
|
|
1780
|
+
break;
|
|
1781
|
+
case "select":
|
|
1782
|
+
case "dropdown": {
|
|
1783
|
+
if (field2.options.some((o) => o.id === raw)) data[field2.id] = raw;
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
case "multi_select": {
|
|
1787
|
+
const ids = new Set(field2.options.map((o) => o.id));
|
|
1788
|
+
const picked = raw.split(",").filter((v) => ids.has(v));
|
|
1789
|
+
if (picked.length > 0) data[field2.id] = picked;
|
|
1790
|
+
break;
|
|
1791
|
+
}
|
|
1792
|
+
case "file_upload":
|
|
1793
|
+
case "ranking":
|
|
1794
|
+
case "matrix":
|
|
1795
|
+
case "signature":
|
|
1796
|
+
case "custom":
|
|
1797
|
+
break;
|
|
1798
|
+
// not prefillable from a URL
|
|
1799
|
+
default:
|
|
1800
|
+
data[field2.id] = raw;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return data;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1487
1806
|
// src/controller.ts
|
|
1488
1807
|
var EMPTY_PAGE = { id: "empty", blocks: [] };
|
|
1489
1808
|
var warnedNoClient = false;
|
|
@@ -1553,6 +1872,10 @@ function hasSubmittedOnce(form, formId) {
|
|
|
1553
1872
|
if (form.settings.submissionLimit !== "once_per_visitor" || !formId) return false;
|
|
1554
1873
|
return Boolean(readStoredSubmission(formId)?.submittedAt);
|
|
1555
1874
|
}
|
|
1875
|
+
function submitFailureMessage(err) {
|
|
1876
|
+
if (isFilloError(err) && err.status && err.status > 0 && err.message) return err.message;
|
|
1877
|
+
return "Couldn't reach the server \u2014 check your connection and try again. If this keeps happening, a browser extension, firewall, or the site's Content-Security-Policy may be blocking the request.";
|
|
1878
|
+
}
|
|
1556
1879
|
function createFormController(options2) {
|
|
1557
1880
|
const { onChange, onSubmitted } = options2;
|
|
1558
1881
|
const initial = normalizeFormSchema(options2.form);
|
|
@@ -1563,10 +1886,32 @@ function createFormController(options2) {
|
|
|
1563
1886
|
let data = { ...options2.initialData ?? {} };
|
|
1564
1887
|
let errors = {};
|
|
1565
1888
|
let pageIndex = 0;
|
|
1566
|
-
let status =
|
|
1889
|
+
let status = "idle";
|
|
1890
|
+
queueMicrotask(() => {
|
|
1891
|
+
if (status === "idle" && hasSubmittedOnce(form, formId)) {
|
|
1892
|
+
status = "submitted";
|
|
1893
|
+
notify();
|
|
1894
|
+
}
|
|
1895
|
+
if (typeof location !== "undefined" && location.search) {
|
|
1896
|
+
const params = Object.fromEntries(new URLSearchParams(location.search));
|
|
1897
|
+
let merged = null;
|
|
1898
|
+
for (const [key, value] of Object.entries(prefillFromParams(form, params))) {
|
|
1899
|
+
if (data[key] === void 0) {
|
|
1900
|
+
merged = merged ?? { ...data };
|
|
1901
|
+
merged[key] = value;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (merged && status === "idle") {
|
|
1905
|
+
data = merged;
|
|
1906
|
+
notify();
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
let submitError;
|
|
1567
1911
|
const uploadingFields = /* @__PURE__ */ new Set();
|
|
1568
1912
|
const listeners = /* @__PURE__ */ new Set();
|
|
1569
1913
|
const mountedAt = Date.now();
|
|
1914
|
+
const idempotencyKey = randomSubmissionKey();
|
|
1570
1915
|
let sessionId = null;
|
|
1571
1916
|
let sessionStarted = false;
|
|
1572
1917
|
function computeSnapshot() {
|
|
@@ -1583,7 +1928,8 @@ function createFormController(options2) {
|
|
|
1583
1928
|
blocks: visibleBlocks(page, data),
|
|
1584
1929
|
isFirstPage: clamped === 0,
|
|
1585
1930
|
isLastPage: clamped === pageCount - 1,
|
|
1586
|
-
uploading: uploadingFields.size > 0
|
|
1931
|
+
uploading: uploadingFields.size > 0,
|
|
1932
|
+
submitError
|
|
1587
1933
|
};
|
|
1588
1934
|
}
|
|
1589
1935
|
let snapshot = computeSnapshot();
|
|
@@ -1616,6 +1962,7 @@ function createFormController(options2) {
|
|
|
1616
1962
|
}
|
|
1617
1963
|
data = { ...data, [fieldId]: value };
|
|
1618
1964
|
onChange?.(data);
|
|
1965
|
+
submitError = void 0;
|
|
1619
1966
|
if (fieldId in errors) {
|
|
1620
1967
|
errors = Object.fromEntries(Object.entries(errors).filter(([k]) => k !== fieldId));
|
|
1621
1968
|
}
|
|
@@ -1658,20 +2005,36 @@ function createFormController(options2) {
|
|
|
1658
2005
|
notify();
|
|
1659
2006
|
return;
|
|
1660
2007
|
}
|
|
2008
|
+
if (client && !formId && options2.resolveFormId) {
|
|
2009
|
+
status = "submitting";
|
|
2010
|
+
submitError = void 0;
|
|
2011
|
+
notify();
|
|
2012
|
+
try {
|
|
2013
|
+
formId = await options2.resolveFormId();
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
status = "idle";
|
|
2016
|
+
submitError = submitFailureMessage(err);
|
|
2017
|
+
notify();
|
|
2018
|
+
throw err;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
1661
2021
|
if (client && formId) {
|
|
1662
|
-
const
|
|
2022
|
+
const visitorKey = form.settings.submissionLimit === "once_per_visitor" ? ensureSubmissionKey(formId) : void 0;
|
|
2023
|
+
const submissionKey = visitorKey ?? idempotencyKey;
|
|
1663
2024
|
status = "submitting";
|
|
2025
|
+
submitError = void 0;
|
|
1664
2026
|
notify();
|
|
1665
2027
|
let res;
|
|
1666
2028
|
try {
|
|
1667
2029
|
res = await client.submit(formId, result.data, {
|
|
1668
2030
|
hp: options2.getHoneypot?.() ?? "",
|
|
1669
2031
|
elapsedMs: Date.now() - mountedAt,
|
|
1670
|
-
surface: options2.surface ?? "
|
|
2032
|
+
surface: options2.surface ?? "headless",
|
|
1671
2033
|
submissionKey
|
|
1672
2034
|
});
|
|
1673
2035
|
} catch (err) {
|
|
1674
2036
|
status = "idle";
|
|
2037
|
+
submitError = submitFailureMessage(err);
|
|
1675
2038
|
notify();
|
|
1676
2039
|
throw err;
|
|
1677
2040
|
}
|
|
@@ -1682,7 +2045,7 @@ function createFormController(options2) {
|
|
|
1682
2045
|
return;
|
|
1683
2046
|
}
|
|
1684
2047
|
if (sessionId) client.reportProgress(sessionId, { completed: true });
|
|
1685
|
-
if (
|
|
2048
|
+
if (visitorKey) markSubmitted(formId, res.responseId);
|
|
1686
2049
|
status = "submitted";
|
|
1687
2050
|
notify();
|
|
1688
2051
|
emitSubmitted(res.responseId, result.data);
|
|
@@ -1728,6 +2091,122 @@ function createFormController(options2) {
|
|
|
1728
2091
|
};
|
|
1729
2092
|
}
|
|
1730
2093
|
|
|
2094
|
+
// src/auto-submit.ts
|
|
2095
|
+
function isAutoSubmitBlock(block) {
|
|
2096
|
+
return isField(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
2097
|
+
}
|
|
2098
|
+
function needsExplicitSubmit(visible) {
|
|
2099
|
+
const interactive = visible.filter((b) => !(isField(b) && b.kind === "hidden"));
|
|
2100
|
+
if (interactive.length === 0) return false;
|
|
2101
|
+
if (interactive.length > 1) return true;
|
|
2102
|
+
return !isAutoSubmitBlock(interactive[0]);
|
|
2103
|
+
}
|
|
2104
|
+
function shouldAutoSubmit(field2, value, ctx) {
|
|
2105
|
+
if (ctx.form.settings.submitMode !== "auto") return false;
|
|
2106
|
+
if (!ctx.isLastPage || ctx.status !== "idle" || ctx.uploading) return false;
|
|
2107
|
+
const hasDiscreteAnswer = (() => {
|
|
2108
|
+
switch (field2.kind) {
|
|
2109
|
+
case "select":
|
|
2110
|
+
case "dropdown":
|
|
2111
|
+
return typeof value === "string" && field2.options.some((option) => option.id === value);
|
|
2112
|
+
case "checkbox":
|
|
2113
|
+
return value === true;
|
|
2114
|
+
case "rating":
|
|
2115
|
+
case "linear_scale":
|
|
2116
|
+
return typeof value === "number";
|
|
2117
|
+
default:
|
|
2118
|
+
return false;
|
|
2119
|
+
}
|
|
2120
|
+
})();
|
|
2121
|
+
if (!hasDiscreteAnswer) return false;
|
|
2122
|
+
if (value === ctx.data[field2.id]) return false;
|
|
2123
|
+
const nextData = { ...ctx.data, [field2.id]: value };
|
|
2124
|
+
const interactive = visibleFields(ctx.form, nextData).filter((f) => f.kind !== "hidden");
|
|
2125
|
+
if (interactive.length !== 1 || interactive[0]?.id !== field2.id) return false;
|
|
2126
|
+
return validateResponse(ctx.form, nextData).ok;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
// src/appearance.ts
|
|
2130
|
+
var FILLO_SLOTS = [
|
|
2131
|
+
"root",
|
|
2132
|
+
"header",
|
|
2133
|
+
"title",
|
|
2134
|
+
"description",
|
|
2135
|
+
"pageTitle",
|
|
2136
|
+
"progress",
|
|
2137
|
+
"progressFill",
|
|
2138
|
+
"blocks",
|
|
2139
|
+
"field",
|
|
2140
|
+
"label",
|
|
2141
|
+
"fieldDescription",
|
|
2142
|
+
"control",
|
|
2143
|
+
"options",
|
|
2144
|
+
"option",
|
|
2145
|
+
"optionLabel",
|
|
2146
|
+
"error",
|
|
2147
|
+
"footer",
|
|
2148
|
+
"button",
|
|
2149
|
+
"success"
|
|
2150
|
+
];
|
|
2151
|
+
var FILLO_DATA_ATTRS = {
|
|
2152
|
+
/** Slot name, on every slot element: `data-fillo="control"`. */
|
|
2153
|
+
slot: "data-fillo",
|
|
2154
|
+
/** Field kind, on the field wrapper: `data-kind="email"`. */
|
|
2155
|
+
kind: "data-kind",
|
|
2156
|
+
/** Field id, on the field wrapper (pre-existing). */
|
|
2157
|
+
field: "data-field",
|
|
2158
|
+
invalid: "data-invalid",
|
|
2159
|
+
required: "data-required",
|
|
2160
|
+
/** Selected option row / active scale step / active star. */
|
|
2161
|
+
selected: "data-selected",
|
|
2162
|
+
/** Checkbox / toggle checked state. */
|
|
2163
|
+
checked: "data-checked",
|
|
2164
|
+
dragOver: "data-drag-over",
|
|
2165
|
+
/** Engine status on the root: idle | submitting | submitted | error. */
|
|
2166
|
+
state: "data-state",
|
|
2167
|
+
page: "data-page",
|
|
2168
|
+
lastPage: "data-last-page"
|
|
2169
|
+
};
|
|
2170
|
+
function resolveSlotClass(appearance, state) {
|
|
2171
|
+
if (!appearance?.classNames && !appearance?.fields) return "";
|
|
2172
|
+
const parts = [];
|
|
2173
|
+
const general = appearance.classNames?.[state.slot];
|
|
2174
|
+
if (general) parts.push(typeof general === "function" ? general(state) : general);
|
|
2175
|
+
const field2 = state.fieldId ? appearance.fields?.[state.fieldId]?.[state.slot] : void 0;
|
|
2176
|
+
if (field2) parts.push(typeof field2 === "function" ? field2(state) : field2);
|
|
2177
|
+
return parts.filter(Boolean).join(" ");
|
|
2178
|
+
}
|
|
2179
|
+
function slotClass(base, appearance, state) {
|
|
2180
|
+
const extra = resolveSlotClass(appearance, state);
|
|
2181
|
+
return extra ? `${base} ${extra}` : base;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// src/strings.ts
|
|
2185
|
+
var DEFAULT_STRINGS = {
|
|
2186
|
+
back: "Back",
|
|
2187
|
+
next: "Next",
|
|
2188
|
+
submit: "Submit",
|
|
2189
|
+
submitting: "Submitting\u2026",
|
|
2190
|
+
uploading: "Uploading\u2026",
|
|
2191
|
+
optional: " (optional)",
|
|
2192
|
+
other: "Other",
|
|
2193
|
+
otherPrompt: "Other \u2014 please specify",
|
|
2194
|
+
otherPlaceholder: "Your answer",
|
|
2195
|
+
choosePlaceholder: "Choose\u2026",
|
|
2196
|
+
successTitle: "Thanks!",
|
|
2197
|
+
successMessage: "Your response has been recorded.",
|
|
2198
|
+
closed: "This form is no longer accepting responses.",
|
|
2199
|
+
notLive: "This form isn't live yet.",
|
|
2200
|
+
submitFailed: "This form can't submit right now. Please try again in a moment.",
|
|
2201
|
+
loadFailedNotFound: "Form not found \u2014 check the form id and that it's published.",
|
|
2202
|
+
loadFailedNetwork: "Couldn't reach the server \u2014 check your connection or CORS.",
|
|
2203
|
+
loadFailed: "This form could not be loaded.",
|
|
2204
|
+
renderFailed: "This form could not be rendered."
|
|
2205
|
+
};
|
|
2206
|
+
function resolveStrings(overrides) {
|
|
2207
|
+
return overrides ? { ...DEFAULT_STRINGS, ...overrides } : DEFAULT_STRINGS;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
1731
2210
|
// src/define.ts
|
|
1732
2211
|
function defineForm(def) {
|
|
1733
2212
|
return {
|
|
@@ -1747,76 +2226,376 @@ function isCodeForm(form) {
|
|
|
1747
2226
|
return typeof form === "object" && form !== null && "__filloCodeForm" in form;
|
|
1748
2227
|
}
|
|
1749
2228
|
var syncCache = /* @__PURE__ */ new Map();
|
|
2229
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2230
|
+
async function syncWithRetry(client, form) {
|
|
2231
|
+
let delay = 800;
|
|
2232
|
+
for (let attempt = 0; ; attempt++) {
|
|
2233
|
+
try {
|
|
2234
|
+
return await client.syncForm(form.id, form.schema, form.theme ?? null);
|
|
2235
|
+
} catch (err) {
|
|
2236
|
+
const status = isFilloError(err) ? err.status ?? 0 : 0;
|
|
2237
|
+
const retryable = !isFilloError(err) || status === 0 || status === 408 || status === 429 || status >= 500;
|
|
2238
|
+
if (!retryable || attempt >= 2) throw err;
|
|
2239
|
+
const hinted = isFilloError(err) ? err.retryAfterSec : void 0;
|
|
2240
|
+
await sleep(Math.min(hinted ? hinted * 1e3 : delay * (1 + Math.random() * 0.5), 15e3));
|
|
2241
|
+
delay *= 2;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
1750
2245
|
function contentHash(input) {
|
|
1751
2246
|
let h = 5381;
|
|
1752
2247
|
for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) | 0;
|
|
1753
2248
|
return (h >>> 0).toString(36);
|
|
1754
2249
|
}
|
|
1755
|
-
function
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
2250
|
+
function storageKey(client, handle) {
|
|
2251
|
+
return `fillo:sync:${client.baseUrl}|${client.key}|${handle}`;
|
|
2252
|
+
}
|
|
2253
|
+
function readStoredSync(client, handle, hash) {
|
|
2254
|
+
try {
|
|
2255
|
+
const raw = globalThis.localStorage?.getItem(storageKey(client, handle));
|
|
2256
|
+
if (!raw) return null;
|
|
2257
|
+
const parsed = JSON.parse(raw);
|
|
2258
|
+
if (typeof parsed.formId !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number")
|
|
2259
|
+
return null;
|
|
2260
|
+
const ttl = parsed.status === "published" ? 36e5 : 6e4;
|
|
2261
|
+
if (Date.now() - parsed.ts > ttl) return null;
|
|
2262
|
+
return parsed;
|
|
2263
|
+
} catch {
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
function writeStoredSync(client, handle, hash, r) {
|
|
2268
|
+
try {
|
|
2269
|
+
globalThis.localStorage?.setItem(
|
|
2270
|
+
storageKey(client, handle),
|
|
2271
|
+
JSON.stringify({
|
|
2272
|
+
formId: r.formId,
|
|
2273
|
+
slug: r.slug,
|
|
2274
|
+
status: r.status,
|
|
2275
|
+
hash,
|
|
2276
|
+
ts: Date.now()
|
|
2277
|
+
})
|
|
2278
|
+
);
|
|
2279
|
+
} catch {
|
|
1763
2280
|
}
|
|
2281
|
+
}
|
|
2282
|
+
function syncCodeForm(client, form, opts) {
|
|
2283
|
+
const content2 = JSON.stringify(form.schema) + JSON.stringify(form.theme ?? null);
|
|
2284
|
+
const hash = contentHash(content2);
|
|
2285
|
+
const key = `${client.baseUrl}:${client.key}:${form.id}:${hash}`;
|
|
2286
|
+
let pending = syncCache.get(key);
|
|
2287
|
+
if (pending && !opts?.bypassCache) return pending;
|
|
2288
|
+
if (!opts?.bypassCache) {
|
|
2289
|
+
const stored = readStoredSync(client, form.id, hash);
|
|
2290
|
+
if (stored) {
|
|
2291
|
+
pending = Promise.resolve({ formId: stored.formId, slug: stored.slug, status: stored.status });
|
|
2292
|
+
syncCache.set(key, pending);
|
|
2293
|
+
return pending;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
pending = syncWithRetry(client, form).then((r) => {
|
|
2297
|
+
writeStoredSync(client, form.id, hash, r);
|
|
2298
|
+
return r;
|
|
2299
|
+
});
|
|
2300
|
+
syncCache.set(key, pending);
|
|
2301
|
+
pending.catch(() => syncCache.delete(key));
|
|
1764
2302
|
return pending;
|
|
1765
2303
|
}
|
|
1766
2304
|
|
|
1767
|
-
// src/
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
2305
|
+
// src/jsx.ts
|
|
2306
|
+
var FilloJsxError = class extends Error {
|
|
2307
|
+
constructor(code, message) {
|
|
2308
|
+
super(`[fillo] ${message} \u2014 https://fillo.so/docs/troubleshooting#jsx-${code.toLowerCase()}`);
|
|
2309
|
+
this.code = code;
|
|
2310
|
+
this.name = "FilloJsxError";
|
|
2311
|
+
}
|
|
2312
|
+
code;
|
|
2313
|
+
};
|
|
2314
|
+
var BRAND = /* @__PURE__ */ Symbol.for("fillo.block");
|
|
2315
|
+
var REACT_FRAGMENT = /* @__PURE__ */ Symbol.for("react.fragment");
|
|
2316
|
+
var FIELD_BASE = ["label", "description", "required", "placeholder"];
|
|
2317
|
+
var field = (name, kind, extra, opts = {}) => ({
|
|
2318
|
+
name,
|
|
2319
|
+
kind,
|
|
2320
|
+
props: [...FIELD_BASE, ...extra],
|
|
2321
|
+
...opts
|
|
2322
|
+
});
|
|
2323
|
+
var content = (name, kind, textChildren) => ({
|
|
2324
|
+
name,
|
|
2325
|
+
kind,
|
|
2326
|
+
props: textChildren ? ["text"] : [],
|
|
2327
|
+
textChildren,
|
|
2328
|
+
content: true
|
|
2329
|
+
});
|
|
2330
|
+
var CHOICE_EXTRA = ["options", "allowOther", "shuffleOptions"];
|
|
2331
|
+
var JSX_BLOCK_SPECS = [
|
|
2332
|
+
field("Text", "short_text", ["maxLength"]),
|
|
2333
|
+
field("LongText", "long_text", ["maxLength"]),
|
|
2334
|
+
field("Email", "email", ["maxLength"]),
|
|
2335
|
+
field("Url", "url", ["maxLength"]),
|
|
2336
|
+
field("Phone", "phone", ["defaultCountry"]),
|
|
2337
|
+
field("Number", "number", ["min", "max"]),
|
|
2338
|
+
field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
|
|
2339
|
+
field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
|
|
2340
|
+
field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
|
|
2341
|
+
field("Checkbox", "checkbox", ["appearance"]),
|
|
2342
|
+
field("Rating", "rating", ["max"]),
|
|
2343
|
+
field("Scale", "linear_scale", ["min", "max", "minLabel", "maxLabel"]),
|
|
2344
|
+
field("Ranking", "ranking", ["options"], { optionChildren: true }),
|
|
2345
|
+
field("Matrix", "matrix", ["rows", "columns"]),
|
|
2346
|
+
field("Signature", "signature", []),
|
|
2347
|
+
field("Date", "date", []),
|
|
2348
|
+
field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
|
|
2349
|
+
field("Hidden", "hidden", ["paramName", "defaultValue"]),
|
|
2350
|
+
field("Custom", "custom", ["component", "config"]),
|
|
2351
|
+
content("Heading", "heading", true),
|
|
2352
|
+
content("Paragraph", "paragraph", true),
|
|
2353
|
+
{ name: "Divider", kind: "divider", props: [], content: true }
|
|
2354
|
+
];
|
|
2355
|
+
var PAGE_SPEC = { name: "Page", kind: "__page", props: ["title"] };
|
|
2356
|
+
var OPTION_SPEC = { name: "Option", kind: "__option", props: ["label", "icon"] };
|
|
2357
|
+
function makeInert(spec) {
|
|
2358
|
+
const component = () => {
|
|
2359
|
+
throw new FilloJsxError(
|
|
2360
|
+
"RENDERED_INERT",
|
|
2361
|
+
`<Fillo.${spec.name}> was rendered by React. Fillo fields are schema declarations, valid only inside <Fillo.Form> or Fillo.defineForm(). If this form lives in a server component, move it to a client module ("use client")`
|
|
2362
|
+
);
|
|
2363
|
+
};
|
|
2364
|
+
component[BRAND] = spec;
|
|
2365
|
+
return component;
|
|
2366
|
+
}
|
|
2367
|
+
var JSX_BLOCK_COMPONENTS = Object.fromEntries([
|
|
2368
|
+
...JSX_BLOCK_SPECS.map((spec) => [spec.name, makeInert(spec)]),
|
|
2369
|
+
["Page", makeInert(PAGE_SPEC)],
|
|
2370
|
+
["Option", makeInert(OPTION_SPEC)]
|
|
2371
|
+
]);
|
|
2372
|
+
var isElement = (v) => typeof v === "object" && v !== null && "type" in v && "props" in v;
|
|
2373
|
+
var brandOf = (type) => typeof type === "function" ? type[BRAND] : void 0;
|
|
2374
|
+
function flatten(children, out = []) {
|
|
2375
|
+
if (children === null || children === void 0 || typeof children === "boolean") return out;
|
|
2376
|
+
if (Array.isArray(children)) {
|
|
2377
|
+
for (const child of children) flatten(child, out);
|
|
2378
|
+
return out;
|
|
2379
|
+
}
|
|
2380
|
+
if (isElement(children)) {
|
|
2381
|
+
if (children.type === REACT_FRAGMENT) return flatten(children.props.children, out);
|
|
2382
|
+
out.push(children);
|
|
2383
|
+
return out;
|
|
2384
|
+
}
|
|
2385
|
+
throw new FilloJsxError(
|
|
2386
|
+
"NON_FILLO_CHILD",
|
|
2387
|
+
`Unexpected ${typeof children} child inside <Fillo.Form> \u2014 children define the schema; put text in <Fillo.Paragraph> and layout outside the form`
|
|
2388
|
+
);
|
|
2389
|
+
}
|
|
2390
|
+
function classify(el) {
|
|
2391
|
+
const spec = brandOf(el.type);
|
|
2392
|
+
if (spec) return spec;
|
|
2393
|
+
const t = el.type;
|
|
2394
|
+
if (typeof t === "string") {
|
|
2395
|
+
throw new FilloJsxError(
|
|
2396
|
+
"NON_FILLO_CHILD",
|
|
2397
|
+
`<${t}> is layout, and <Fillo.Form> children define the schema only. Put markup outside the form, or compose your own layout with FilloProvider (headless)`
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
if (typeof t === "object" && t !== null) {
|
|
2401
|
+
throw new FilloJsxError(
|
|
2402
|
+
"OPAQUE_TYPE",
|
|
2403
|
+
`A child of <Fillo.Form> has an opaque component type (lazy/memo or a server-component boundary). Define the form in a client module ("use client") and pass the form VALUE across boundaries instead`
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
const name = typeof t === "function" && t.name ? ` (${t.name})` : "";
|
|
2407
|
+
throw new FilloJsxError(
|
|
2408
|
+
"WRAPPER_COMPONENT",
|
|
2409
|
+
`A plain component${name} is not a Fillo field \u2014 <Fillo.Form> never renders children, so wrappers are invisible to it. For reuse, call a plain function that RETURNS Fillo elements: {contactFields()}`
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
function buildProps(spec, el) {
|
|
2413
|
+
const props = el.props;
|
|
2414
|
+
const allowed = /* @__PURE__ */ new Set([...spec.props, "id", "visibleIf", "children"]);
|
|
2415
|
+
for (const key of Object.keys(props)) {
|
|
2416
|
+
if (props[key] === void 0) continue;
|
|
2417
|
+
if (!allowed.has(key)) {
|
|
2418
|
+
const hint = key === "className" || key === "style" ? `styling lives on <Fillo.Form appearance={\u2026}>, never in the schema` : `schema props for Fillo.${spec.name}: ${[...spec.props].join(", ")}`;
|
|
2419
|
+
throw new FilloJsxError("UNKNOWN_PROP", `Fillo.${spec.name} has no prop "${key}" \u2014 ${hint}`);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
const id = props.id;
|
|
2423
|
+
if (typeof id !== "string" || id.length === 0 || id.length > 128) {
|
|
2424
|
+
throw new FilloJsxError(
|
|
2425
|
+
"MISSING_ID",
|
|
2426
|
+
`Every Fillo.${spec.name} needs a stable string id (1-128 chars) \u2014 ids key responses, logic, piping, and sync; never derive them from position`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
const out = { id, kind: spec.kind };
|
|
2430
|
+
if (spec.optionChildren && props.children !== void 0) {
|
|
2431
|
+
if (props.options !== void 0) {
|
|
2432
|
+
throw new FilloJsxError(
|
|
2433
|
+
"OPTION_PROP_CONFLICT",
|
|
2434
|
+
`Fillo.${spec.name} "${id}" has both an options prop and <Fillo.Option> children \u2014 pick one`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
out.__childOptions = flatten(props.children).map((child) => buildOption(child, spec, id));
|
|
2438
|
+
} else if (spec.textChildren && props.children !== void 0) {
|
|
2439
|
+
const parts = Array.isArray(props.children) ? props.children : [props.children];
|
|
2440
|
+
if (!parts.every((p) => typeof p === "string" || typeof p === "number")) {
|
|
2441
|
+
throw new FilloJsxError(
|
|
2442
|
+
"TEXT_CHILD_REQUIRED",
|
|
2443
|
+
`Fillo.${spec.name} children must be plain text (piping tokens like {{name}} included) \u2014 expressions and elements can't serialize into the schema`
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
if (props.text !== void 0) {
|
|
2447
|
+
throw new FilloJsxError(
|
|
2448
|
+
"OPTION_PROP_CONFLICT",
|
|
2449
|
+
`Fillo.${spec.name} "${id}" has both a text prop and children \u2014 pick one`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
out.__childText = parts.join("");
|
|
2453
|
+
} else if (props.children !== void 0) {
|
|
2454
|
+
throw new FilloJsxError(
|
|
2455
|
+
"NON_FILLO_CHILD",
|
|
2456
|
+
`Fillo.${spec.name} takes no children \u2014 its configuration is props-only`
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
for (const key of spec.props) {
|
|
2460
|
+
if (key === "options" && out.__childOptions !== void 0) {
|
|
2461
|
+
out.options = out.__childOptions;
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
if (key === "text" && out.__childText !== void 0) {
|
|
2465
|
+
out.text = out.__childText;
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (props[key] !== void 0) out[key] = props[key];
|
|
2469
|
+
}
|
|
2470
|
+
delete out.__childOptions;
|
|
2471
|
+
delete out.__childText;
|
|
2472
|
+
if (spec.textChildren && out.text === void 0) {
|
|
2473
|
+
throw new FilloJsxError("TEXT_CHILD_REQUIRED", `Fillo.${spec.name} "${id}" has no text`);
|
|
2474
|
+
}
|
|
2475
|
+
if (props.visibleIf !== void 0) {
|
|
2476
|
+
const conditions2 = Array.isArray(props.visibleIf) ? props.visibleIf : [props.visibleIf];
|
|
2477
|
+
out.visibleIf = conditions2;
|
|
2478
|
+
}
|
|
2479
|
+
return out;
|
|
2480
|
+
}
|
|
2481
|
+
function buildOption(el, parent, parentId) {
|
|
2482
|
+
const spec = classify(el);
|
|
2483
|
+
if (spec.kind !== "__option") {
|
|
2484
|
+
throw new FilloJsxError(
|
|
2485
|
+
"NON_FILLO_CHILD",
|
|
2486
|
+
`Fillo.${parent.name} "${parentId}" children must be <Fillo.Option> elements, found Fillo.${spec.name}`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
const props = el.props;
|
|
2490
|
+
for (const key of Object.keys(props)) {
|
|
2491
|
+
if (props[key] !== void 0 && key !== "id" && key !== "label" && key !== "icon") {
|
|
2492
|
+
throw new FilloJsxError("UNKNOWN_PROP", `Fillo.Option has no prop "${key}" \u2014 it takes id, label, icon`);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
if (typeof props.id !== "string" || props.id.length === 0) {
|
|
2496
|
+
throw new FilloJsxError(
|
|
2497
|
+
"MISSING_ID",
|
|
2498
|
+
`Every <Fillo.Option> in "${parentId}" needs a stable string id \u2014 it's the stored answer value`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
if (typeof props.label !== "string") {
|
|
2502
|
+
throw new FilloJsxError("MISSING_ID", `<Fillo.Option id="${props.id}"> needs a label`);
|
|
2503
|
+
}
|
|
2504
|
+
const option = { id: props.id, label: props.label };
|
|
2505
|
+
if (props.icon !== void 0) option.icon = props.icon;
|
|
2506
|
+
return option;
|
|
2507
|
+
}
|
|
2508
|
+
function schemaFromJsx(children, meta) {
|
|
2509
|
+
const elements = flatten(children);
|
|
2510
|
+
const pageSpecs = elements.map((el) => ({ el, spec: classify(el) }));
|
|
2511
|
+
const pages = [];
|
|
2512
|
+
const isPage = (s) => s.kind === "__page";
|
|
2513
|
+
if (pageSpecs.some(({ spec }) => isPage(spec))) {
|
|
2514
|
+
if (!pageSpecs.every(({ spec }) => isPage(spec))) {
|
|
2515
|
+
throw new FilloJsxError(
|
|
2516
|
+
"PAGE_MIX",
|
|
2517
|
+
`<Fillo.Form> children must be EITHER all <Fillo.Page> elements or all blocks \u2014 wrap the loose blocks in a page`
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
for (const { el } of pageSpecs) {
|
|
2521
|
+
const built = buildProps(PAGE_SPEC, { type: el.type, props: { ...el.props, children: void 0 } });
|
|
2522
|
+
const blocks = flatten(el.props.children).map((child) => {
|
|
2523
|
+
const spec = classify(child);
|
|
2524
|
+
if (isPage(spec) || spec.kind === "__option") {
|
|
2525
|
+
throw new FilloJsxError("PAGE_MIX", `<Fillo.${spec.name}> can't appear inside <Fillo.Page>`);
|
|
2526
|
+
}
|
|
2527
|
+
return buildProps(spec, child);
|
|
2528
|
+
});
|
|
2529
|
+
pages.push({
|
|
2530
|
+
id: built.id,
|
|
2531
|
+
...built.title !== void 0 ? { title: built.title } : {},
|
|
2532
|
+
blocks
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
} else {
|
|
2536
|
+
const blocks = pageSpecs.map(({ el, spec }) => {
|
|
2537
|
+
if (spec.kind === "__option") {
|
|
2538
|
+
throw new FilloJsxError(
|
|
2539
|
+
"NON_FILLO_CHILD",
|
|
2540
|
+
`<Fillo.Option> only makes sense inside a choice field (Select/MultiSelect/Dropdown/Ranking)`
|
|
2541
|
+
);
|
|
1801
2542
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
2543
|
+
return buildProps(spec, el);
|
|
2544
|
+
});
|
|
2545
|
+
pages.push({ id: "main", blocks });
|
|
2546
|
+
}
|
|
2547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2548
|
+
for (const page of pages) {
|
|
2549
|
+
for (const block of page.blocks) {
|
|
2550
|
+
if (seen.has(block.id)) {
|
|
2551
|
+
throw new FilloJsxError(
|
|
2552
|
+
"DUPLICATE_ID",
|
|
2553
|
+
`Two blocks share the id "${block.id}" \u2014 ids key responses and must be unique across the whole form`
|
|
2554
|
+
);
|
|
1807
2555
|
}
|
|
1808
|
-
|
|
1809
|
-
case "ranking":
|
|
1810
|
-
case "matrix":
|
|
1811
|
-
case "signature":
|
|
1812
|
-
case "custom":
|
|
1813
|
-
break;
|
|
1814
|
-
// not prefillable from a URL
|
|
1815
|
-
default:
|
|
1816
|
-
data[field.id] = raw;
|
|
2556
|
+
seen.add(block.id);
|
|
1817
2557
|
}
|
|
1818
2558
|
}
|
|
1819
|
-
return
|
|
2559
|
+
return {
|
|
2560
|
+
version: 1,
|
|
2561
|
+
title: meta.title ?? "",
|
|
2562
|
+
...meta.description !== void 0 ? { description: meta.description } : {},
|
|
2563
|
+
pages,
|
|
2564
|
+
settings: meta.settings ?? {}
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
function codeFormFromJsx(meta, children) {
|
|
2568
|
+
if (typeof meta.id !== "string" || meta.id.length === 0) {
|
|
2569
|
+
throw new FilloJsxError("MISSING_ID", `<Fillo.Form> needs an id \u2014 it names the form in your workspace`);
|
|
2570
|
+
}
|
|
2571
|
+
const schema = schemaFromJsx(children, {
|
|
2572
|
+
id: meta.id,
|
|
2573
|
+
title: meta.title,
|
|
2574
|
+
description: meta.description,
|
|
2575
|
+
settings: meta.settings
|
|
2576
|
+
});
|
|
2577
|
+
return defineForm({
|
|
2578
|
+
id: meta.id,
|
|
2579
|
+
title: schema.title,
|
|
2580
|
+
description: schema.description,
|
|
2581
|
+
pages: schema.pages,
|
|
2582
|
+
settings: schema.settings,
|
|
2583
|
+
theme: meta.theme
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// src/when.ts
|
|
2588
|
+
function when(fieldId) {
|
|
2589
|
+
const cond = (op, value) => Object.freeze(value === void 0 ? { fieldId, op } : { fieldId, op, value });
|
|
2590
|
+
return {
|
|
2591
|
+
eq: (value) => cond("eq", value),
|
|
2592
|
+
neq: (value) => cond("neq", value),
|
|
2593
|
+
contains: (value) => cond("contains", value),
|
|
2594
|
+
gt: (value) => cond("gt", value),
|
|
2595
|
+
lt: (value) => cond("lt", value),
|
|
2596
|
+
answered: () => cond("answered"),
|
|
2597
|
+
notAnswered: () => cond("not_answered")
|
|
2598
|
+
};
|
|
1820
2599
|
}
|
|
1821
2600
|
|
|
1822
2601
|
// src/piping.ts
|
|
@@ -1825,10 +2604,10 @@ function resolveText(text, data, form) {
|
|
|
1825
2604
|
if (!text.includes("{{")) return text;
|
|
1826
2605
|
const fields = allFields(form);
|
|
1827
2606
|
return text.replace(TOKEN, (_, id) => {
|
|
1828
|
-
const
|
|
2607
|
+
const field2 = fields.find((f) => f.id === id);
|
|
1829
2608
|
const value = data[id];
|
|
1830
2609
|
if (value === null || value === void 0 || value === "") return "";
|
|
1831
|
-
const label = (optionId) =>
|
|
2610
|
+
const label = (optionId) => field2 && "options" in field2 ? field2.options.find((o) => o.id === optionId)?.label ?? optionId : optionId;
|
|
1832
2611
|
if (Array.isArray(value)) return value.map((v) => label(String(v))).join(", ");
|
|
1833
2612
|
if (typeof value === "string") return label(value);
|
|
1834
2613
|
return String(value);
|
|
@@ -1849,18 +2628,26 @@ function pipeBlock(block, data, form) {
|
|
|
1849
2628
|
export {
|
|
1850
2629
|
BLOCK_KIND_META,
|
|
1851
2630
|
CONTENT_KINDS,
|
|
2631
|
+
DEFAULT_STRINGS,
|
|
1852
2632
|
DRAFT_KINDS,
|
|
2633
|
+
FILLO_DATA_ATTRS,
|
|
1853
2634
|
FILLO_SCHEMA_VERSION,
|
|
1854
2635
|
FILLO_SDK_VERSION,
|
|
2636
|
+
FILLO_SLOTS,
|
|
1855
2637
|
FilloClient,
|
|
1856
2638
|
FilloError,
|
|
2639
|
+
FilloJsxError,
|
|
2640
|
+
JSX_BLOCK_COMPONENTS,
|
|
2641
|
+
JSX_BLOCK_SPECS,
|
|
1857
2642
|
PHONE_COUNTRIES,
|
|
1858
2643
|
Sha1,
|
|
1859
2644
|
allFields,
|
|
1860
2645
|
assembleForm,
|
|
2646
|
+
codeFormFromJsx,
|
|
1861
2647
|
contentHash,
|
|
1862
2648
|
countryByDialCode,
|
|
1863
2649
|
countryByIso,
|
|
2650
|
+
countryByTimeZone,
|
|
1864
2651
|
createBlock,
|
|
1865
2652
|
createClient,
|
|
1866
2653
|
createEmptyForm,
|
|
@@ -1871,23 +2658,32 @@ export {
|
|
|
1871
2658
|
flagEmoji,
|
|
1872
2659
|
formatAnswer,
|
|
1873
2660
|
formatNational,
|
|
2661
|
+
isAutoSubmitBlock,
|
|
1874
2662
|
isBlockVisible,
|
|
1875
2663
|
isCodeForm,
|
|
1876
2664
|
isField,
|
|
2665
|
+
isFilloError,
|
|
1877
2666
|
isPossiblePhone,
|
|
2667
|
+
needsExplicitSubmit,
|
|
1878
2668
|
normalizeFormSchema,
|
|
1879
2669
|
normalizeFormTheme,
|
|
1880
2670
|
parsePhone,
|
|
1881
2671
|
pipeBlock,
|
|
1882
2672
|
prefillFromParams,
|
|
1883
2673
|
provisionWorkspace,
|
|
2674
|
+
resolveSlotClass,
|
|
2675
|
+
resolveStrings,
|
|
1884
2676
|
resolveText,
|
|
2677
|
+
schemaFromJsx,
|
|
1885
2678
|
sha1Base64,
|
|
2679
|
+
shouldAutoSubmit,
|
|
2680
|
+
slotClass,
|
|
1886
2681
|
syncCodeForm,
|
|
1887
2682
|
toE164,
|
|
1888
2683
|
validateField,
|
|
1889
2684
|
validateFormSchema,
|
|
1890
2685
|
validateResponse,
|
|
1891
2686
|
visibleBlocks,
|
|
1892
|
-
visibleFields
|
|
2687
|
+
visibleFields,
|
|
2688
|
+
when
|
|
1893
2689
|
};
|