@fieldia/core 0.26.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -3
- package/package.json +1 -1
- package/schema/page.schema.json +230 -24
- package/src/index.d.mts +10 -2
- package/src/index.d.ts +10 -2
- package/src/index.js +26 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +11 -2
- package/src/index.mjs.map +1 -1
- package/src/lib/expression/app-functions.d.mts +47 -0
- package/src/lib/expression/app-functions.d.ts +47 -0
- package/src/lib/expression/app-functions.js +64 -0
- package/src/lib/expression/app-functions.js.map +1 -0
- package/src/lib/expression/app-functions.mjs +58 -0
- package/src/lib/expression/app-functions.mjs.map +1 -0
- package/src/lib/expression/dates.d.mts +21 -0
- package/src/lib/expression/dates.d.ts +21 -0
- package/src/lib/expression/dates.js +73 -0
- package/src/lib/expression/dates.js.map +1 -0
- package/src/lib/expression/dates.mjs +67 -0
- package/src/lib/expression/dates.mjs.map +1 -0
- package/src/lib/expression/evaluator.js +12 -6
- package/src/lib/expression/evaluator.js.map +1 -1
- package/src/lib/expression/evaluator.mjs +12 -6
- package/src/lib/expression/evaluator.mjs.map +1 -1
- package/src/lib/expression/expression.d.mts +7 -3
- package/src/lib/expression/expression.d.ts +7 -3
- package/src/lib/expression/expression.js +8 -2
- package/src/lib/expression/expression.js.map +1 -1
- package/src/lib/expression/expression.mjs +7 -2
- package/src/lib/expression/expression.mjs.map +1 -1
- package/src/lib/expression/functions-part.d.mts +7 -0
- package/src/lib/expression/functions-part.d.ts +7 -0
- package/src/lib/expression/functions-part.js +13 -0
- package/src/lib/expression/functions-part.js.map +1 -0
- package/src/lib/expression/functions-part.mjs +8 -0
- package/src/lib/expression/functions-part.mjs.map +1 -0
- package/src/lib/expression/functions.d.mts +60 -3
- package/src/lib/expression/functions.d.ts +60 -3
- package/src/lib/expression/functions.js +109 -15
- package/src/lib/expression/functions.js.map +1 -1
- package/src/lib/expression/functions.mjs +103 -16
- package/src/lib/expression/functions.mjs.map +1 -1
- package/src/lib/expression/kinds.d.mts +31 -0
- package/src/lib/expression/kinds.d.ts +31 -0
- package/src/lib/expression/kinds.js +159 -0
- package/src/lib/expression/kinds.js.map +1 -0
- package/src/lib/expression/kinds.mjs +151 -0
- package/src/lib/expression/kinds.mjs.map +1 -0
- package/src/lib/expression/modifier.d.mts +4 -3
- package/src/lib/expression/modifier.d.ts +4 -3
- package/src/lib/expression/modifier.js +4 -3
- package/src/lib/expression/modifier.js.map +1 -1
- package/src/lib/expression/modifier.mjs +4 -3
- package/src/lib/expression/modifier.mjs.map +1 -1
- package/src/lib/expression/more-functions.d.mts +8 -0
- package/src/lib/expression/more-functions.d.ts +8 -0
- package/src/lib/expression/more-functions.js +235 -0
- package/src/lib/expression/more-functions.js.map +1 -0
- package/src/lib/expression/more-functions.mjs +230 -0
- package/src/lib/expression/more-functions.mjs.map +1 -0
- package/src/lib/expression/parser.d.mts +4 -1
- package/src/lib/expression/parser.d.ts +4 -1
- package/src/lib/expression/parser.js +6 -3
- package/src/lib/expression/parser.js.map +1 -1
- package/src/lib/expression/parser.mjs +6 -3
- package/src/lib/expression/parser.mjs.map +1 -1
- package/src/lib/expression/reads.d.mts +2 -2
- package/src/lib/expression/reads.d.ts +2 -2
- package/src/lib/expression/reads.js +6 -4
- package/src/lib/expression/reads.js.map +1 -1
- package/src/lib/expression/reads.mjs +6 -4
- package/src/lib/expression/reads.mjs.map +1 -1
- package/src/lib/format/actions.d.mts +16 -16
- package/src/lib/format/actions.d.ts +16 -16
- package/src/lib/format/check-page.d.mts +7 -2
- package/src/lib/format/check-page.d.ts +7 -2
- package/src/lib/format/check-page.js +10 -4
- package/src/lib/format/check-page.js.map +1 -1
- package/src/lib/format/check-page.mjs +10 -4
- package/src/lib/format/check-page.mjs.map +1 -1
- package/src/lib/format/choice-options.d.mts +34 -0
- package/src/lib/format/choice-options.d.ts +34 -0
- package/src/lib/format/choice-options.js +60 -0
- package/src/lib/format/choice-options.js.map +1 -0
- package/src/lib/format/choice-options.mjs +51 -0
- package/src/lib/format/choice-options.mjs.map +1 -0
- package/src/lib/format/endings-checks.d.mts +8 -0
- package/src/lib/format/endings-checks.d.ts +8 -0
- package/src/lib/format/endings-checks.js +23 -0
- package/src/lib/format/endings-checks.js.map +1 -0
- package/src/lib/format/endings-checks.mjs +20 -0
- package/src/lib/format/endings-checks.mjs.map +1 -0
- package/src/lib/format/endings.d.mts +40 -0
- package/src/lib/format/endings.d.ts +40 -0
- package/src/lib/format/endings.js +15 -0
- package/src/lib/format/endings.js.map +1 -0
- package/src/lib/format/endings.mjs +12 -0
- package/src/lib/format/endings.mjs.map +1 -0
- package/src/lib/format/field.d.mts +151 -16
- package/src/lib/format/field.d.ts +151 -16
- package/src/lib/format/field.js +64 -11
- package/src/lib/format/field.js.map +1 -1
- package/src/lib/format/field.mjs +63 -10
- package/src/lib/format/field.mjs.map +1 -1
- package/src/lib/format/head-checks.d.mts +2 -0
- package/src/lib/format/head-checks.d.ts +2 -0
- package/src/lib/format/head-checks.js +2 -59
- package/src/lib/format/head-checks.js.map +1 -1
- package/src/lib/format/head-checks.mjs +1 -58
- package/src/lib/format/head-checks.mjs.map +1 -1
- package/src/lib/format/later-checks.d.mts +17 -0
- package/src/lib/format/later-checks.d.ts +17 -0
- package/src/lib/format/later-checks.js +20 -0
- package/src/lib/format/later-checks.js.map +1 -0
- package/src/lib/format/later-checks.mjs +17 -0
- package/src/lib/format/later-checks.mjs.map +1 -0
- package/src/lib/format/layout.d.mts +264 -190
- package/src/lib/format/layout.d.ts +264 -190
- package/src/lib/format/layout.js +34 -29
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +14 -4
- package/src/lib/format/layout.mjs.map +1 -1
- package/src/lib/format/layout.types.d.mts +2 -1
- package/src/lib/format/layout.types.d.ts +2 -1
- package/src/lib/format/limits-checks.d.mts +14 -0
- package/src/lib/format/limits-checks.d.ts +14 -0
- package/src/lib/format/limits-checks.js +36 -0
- package/src/lib/format/limits-checks.js.map +1 -0
- package/src/lib/format/limits-checks.mjs +33 -0
- package/src/lib/format/limits-checks.mjs.map +1 -0
- package/src/lib/format/limits.d.mts +24 -0
- package/src/lib/format/limits.d.ts +24 -0
- package/src/lib/format/limits.js +35 -0
- package/src/lib/format/limits.js.map +1 -0
- package/src/lib/format/limits.mjs +32 -0
- package/src/lib/format/limits.mjs.map +1 -0
- package/src/lib/format/list-checks.d.mts +44 -0
- package/src/lib/format/list-checks.d.ts +44 -0
- package/src/lib/format/list-checks.js +340 -0
- package/src/lib/format/list-checks.js.map +1 -0
- package/src/lib/format/list-checks.mjs +336 -0
- package/src/lib/format/list-checks.mjs.map +1 -0
- package/src/lib/format/map-checks.d.mts +33 -0
- package/src/lib/format/map-checks.d.ts +33 -0
- package/src/lib/format/map-checks.js +180 -0
- package/src/lib/format/map-checks.js.map +1 -0
- package/src/lib/format/map-checks.mjs +175 -0
- package/src/lib/format/map-checks.mjs.map +1 -0
- package/src/lib/format/mapchecks-part.d.mts +5 -0
- package/src/lib/format/mapchecks-part.d.ts +5 -0
- package/src/lib/format/mapchecks-part.js +11 -0
- package/src/lib/format/mapchecks-part.js.map +1 -0
- package/src/lib/format/mapchecks-part.mjs +6 -0
- package/src/lib/format/mapchecks-part.mjs.map +1 -0
- package/src/lib/format/option-checks.d.mts +16 -0
- package/src/lib/format/option-checks.d.ts +16 -0
- package/src/lib/format/option-checks.js +39 -0
- package/src/lib/format/option-checks.js.map +1 -0
- package/src/lib/format/option-checks.mjs +36 -0
- package/src/lib/format/option-checks.mjs.map +1 -0
- package/src/lib/format/options-part.d.mts +6 -0
- package/src/lib/format/options-part.d.ts +6 -0
- package/src/lib/format/options-part.js +11 -0
- package/src/lib/format/options-part.js.map +1 -0
- package/src/lib/format/options-part.mjs +7 -0
- package/src/lib/format/options-part.mjs.map +1 -0
- package/src/lib/format/page.d.mts +154 -71
- package/src/lib/format/page.d.ts +154 -71
- package/src/lib/format/page.js +5 -0
- package/src/lib/format/page.js.map +1 -1
- package/src/lib/format/page.mjs +5 -0
- package/src/lib/format/page.mjs.map +1 -1
- package/src/lib/format/part-checks.d.mts +20 -0
- package/src/lib/format/part-checks.d.ts +20 -0
- package/src/lib/format/part-checks.js +3 -0
- package/src/lib/format/part-checks.js.map +1 -0
- package/src/lib/format/part-checks.mjs +2 -0
- package/src/lib/format/part-checks.mjs.map +1 -0
- package/src/lib/format/references.d.mts +71 -65
- package/src/lib/format/references.d.ts +71 -65
- package/src/lib/format/references.js +194 -631
- package/src/lib/format/references.js.map +1 -1
- package/src/lib/format/references.mjs +194 -631
- package/src/lib/format/references.mjs.map +1 -1
- package/src/lib/format/seats-checks.d.mts +3 -0
- package/src/lib/format/seats-checks.d.ts +3 -0
- package/src/lib/format/seats-checks.js +13 -0
- package/src/lib/format/seats-checks.js.map +1 -0
- package/src/lib/format/seats-checks.mjs +10 -0
- package/src/lib/format/seats-checks.mjs.map +1 -0
- package/src/lib/format/sheet-head-checks.d.mts +4 -0
- package/src/lib/format/sheet-head-checks.d.ts +4 -0
- package/src/lib/format/sheet-head-checks.js +68 -0
- package/src/lib/format/sheet-head-checks.js.map +1 -0
- package/src/lib/format/sheet-head-checks.mjs +65 -0
- package/src/lib/format/sheet-head-checks.mjs.map +1 -0
- package/src/lib/format/signature-checks.d.mts +7 -0
- package/src/lib/format/signature-checks.d.ts +7 -0
- package/src/lib/format/signature-checks.js +32 -0
- package/src/lib/format/signature-checks.js.map +1 -0
- package/src/lib/format/signature-checks.mjs +29 -0
- package/src/lib/format/signature-checks.mjs.map +1 -0
- package/src/lib/format/signature-options.d.mts +24 -0
- package/src/lib/format/signature-options.d.ts +24 -0
- package/src/lib/format/signature-options.js +30 -0
- package/src/lib/format/signature-options.js.map +1 -0
- package/src/lib/format/signature-options.mjs +26 -0
- package/src/lib/format/signature-options.mjs.map +1 -0
- package/src/lib/format/slots-checks.d.mts +4 -0
- package/src/lib/format/slots-checks.d.ts +4 -0
- package/src/lib/format/slots-checks.js +21 -0
- package/src/lib/format/slots-checks.js.map +1 -0
- package/src/lib/format/slots-checks.mjs +18 -0
- package/src/lib/format/slots-checks.mjs.map +1 -0
- package/src/lib/format/statusbar-checks.d.mts +8 -0
- package/src/lib/format/statusbar-checks.d.ts +8 -0
- package/src/lib/format/statusbar-checks.js +35 -0
- package/src/lib/format/statusbar-checks.js.map +1 -0
- package/src/lib/format/statusbar-checks.mjs +32 -0
- package/src/lib/format/statusbar-checks.mjs.map +1 -0
- package/src/lib/format/table-checks.d.mts +21 -0
- package/src/lib/format/table-checks.d.ts +21 -0
- package/src/lib/format/table-checks.js +86 -0
- package/src/lib/format/table-checks.js.map +1 -0
- package/src/lib/format/table-checks.mjs +82 -0
- package/src/lib/format/table-checks.mjs.map +1 -0
- package/src/lib/format/translate.js +2 -2
- package/src/lib/format/translate.js.map +1 -1
- package/src/lib/format/translate.mjs +2 -2
- package/src/lib/format/translate.mjs.map +1 -1
- package/src/lib/format/upgrade.d.mts +7 -3
- package/src/lib/format/upgrade.d.ts +7 -3
- package/src/lib/format/upgrade.js +7 -3
- package/src/lib/format/upgrade.js.map +1 -1
- package/src/lib/format/upgrade.mjs +7 -3
- package/src/lib/format/upgrade.mjs.map +1 -1
- package/src/lib/format/validate.d.mts +18 -2
- package/src/lib/format/validate.d.ts +18 -2
- package/src/lib/format/validate.js +36 -3
- package/src/lib/format/validate.js.map +1 -1
- package/src/lib/format/validate.mjs +35 -3
- package/src/lib/format/validate.mjs.map +1 -1
- package/src/lib/format/version.d.mts +2 -2
- package/src/lib/format/version.d.ts +2 -2
- package/src/lib/format/version.js +2 -2
- package/src/lib/format/version.js.map +1 -1
- package/src/lib/format/version.mjs +2 -2
- package/src/lib/format/version.mjs.map +1 -1
- package/src/lib/format/words.d.mts +6 -2
- package/src/lib/format/words.d.ts +6 -2
- package/src/lib/format/words.js +13 -7
- package/src/lib/format/words.js.map +1 -1
- package/src/lib/format/words.mjs +13 -7
- package/src/lib/format/words.mjs.map +1 -1
- package/src/lib/parts.d.mts +45 -0
- package/src/lib/parts.d.ts +45 -0
- package/src/lib/parts.js +60 -0
- package/src/lib/parts.js.map +1 -0
- package/src/lib/parts.mjs +53 -0
- package/src/lib/parts.mjs.map +1 -0
- package/src/lib/record/cells.d.mts +3 -3
- package/src/lib/record/cells.d.ts +3 -3
- package/src/lib/record/cells.js +16 -15
- package/src/lib/record/cells.js.map +1 -1
- package/src/lib/record/cells.mjs +16 -15
- package/src/lib/record/cells.mjs.map +1 -1
- package/src/lib/record/check.js +3 -1
- package/src/lib/record/check.js.map +1 -1
- package/src/lib/record/check.mjs +3 -1
- package/src/lib/record/check.mjs.map +1 -1
- package/src/lib/record/compute.d.mts +16 -5
- package/src/lib/record/compute.d.ts +16 -5
- package/src/lib/record/compute.js +57 -16
- package/src/lib/record/compute.js.map +1 -1
- package/src/lib/record/compute.mjs +57 -16
- package/src/lib/record/compute.mjs.map +1 -1
- package/src/lib/record/data-source.d.mts +140 -3
- package/src/lib/record/data-source.d.ts +140 -3
- package/src/lib/record/data-source.js.map +1 -1
- package/src/lib/record/data-source.mjs.map +1 -1
- package/src/lib/record/day-words.d.mts +10 -0
- package/src/lib/record/day-words.d.ts +10 -0
- package/src/lib/record/day-words.js +23 -0
- package/src/lib/record/day-words.js.map +1 -0
- package/src/lib/record/day-words.mjs +20 -0
- package/src/lib/record/day-words.mjs.map +1 -0
- package/src/lib/record/drafts.d.mts +19 -0
- package/src/lib/record/drafts.d.ts +19 -0
- package/src/lib/record/drafts.js +35 -0
- package/src/lib/record/drafts.js.map +1 -0
- package/src/lib/record/drafts.mjs +31 -0
- package/src/lib/record/drafts.mjs.map +1 -0
- package/src/lib/record/env.d.mts +5 -1
- package/src/lib/record/env.d.ts +5 -1
- package/src/lib/record/env.js +9 -1
- package/src/lib/record/env.js.map +1 -1
- package/src/lib/record/env.mjs +8 -1
- package/src/lib/record/env.mjs.map +1 -1
- package/src/lib/record/form-limits.d.mts +78 -0
- package/src/lib/record/form-limits.d.ts +78 -0
- package/src/lib/record/form-limits.js +230 -0
- package/src/lib/record/form-limits.js.map +1 -0
- package/src/lib/record/form-limits.mjs +223 -0
- package/src/lib/record/form-limits.mjs.map +1 -0
- package/src/lib/record/form.d.mts +180 -9
- package/src/lib/record/form.d.ts +180 -9
- package/src/lib/record/form.js +858 -90
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +858 -90
- package/src/lib/record/form.mjs.map +1 -1
- package/src/lib/record/limits-part.d.mts +6 -0
- package/src/lib/record/limits-part.d.ts +6 -0
- package/src/lib/record/limits-part.js +12 -0
- package/src/lib/record/limits-part.js.map +1 -0
- package/src/lib/record/limits-part.mjs +7 -0
- package/src/lib/record/limits-part.mjs.map +1 -0
- package/src/lib/record/limits.js +4 -2
- package/src/lib/record/limits.js.map +1 -1
- package/src/lib/record/limits.mjs +4 -2
- package/src/lib/record/limits.mjs.map +1 -1
- package/src/lib/record/line-choices.d.mts +30 -0
- package/src/lib/record/line-choices.d.ts +30 -0
- package/src/lib/record/line-choices.js +42 -0
- package/src/lib/record/line-choices.js.map +1 -0
- package/src/lib/record/line-choices.mjs +36 -0
- package/src/lib/record/line-choices.mjs.map +1 -0
- package/src/lib/record/locales/ar.js +11 -0
- package/src/lib/record/locales/ar.js.map +1 -1
- package/src/lib/record/locales/ar.mjs +11 -0
- package/src/lib/record/locales/ar.mjs.map +1 -1
- package/src/lib/record/locales/de.js +11 -0
- package/src/lib/record/locales/de.js.map +1 -1
- package/src/lib/record/locales/de.mjs +11 -0
- package/src/lib/record/locales/de.mjs.map +1 -1
- package/src/lib/record/locales/fr.js +11 -0
- package/src/lib/record/locales/fr.js.map +1 -1
- package/src/lib/record/locales/fr.mjs +11 -0
- package/src/lib/record/locales/fr.mjs.map +1 -1
- package/src/lib/record/memory-data-source.d.mts +44 -3
- package/src/lib/record/memory-data-source.d.ts +44 -3
- package/src/lib/record/memory-data-source.js +102 -40
- package/src/lib/record/memory-data-source.js.map +1 -1
- package/src/lib/record/memory-data-source.mjs +101 -40
- package/src/lib/record/memory-data-source.mjs.map +1 -1
- package/src/lib/record/memory-kept.d.mts +20 -0
- package/src/lib/record/memory-kept.d.ts +20 -0
- package/src/lib/record/memory-kept.js +46 -0
- package/src/lib/record/memory-kept.js.map +1 -0
- package/src/lib/record/memory-kept.mjs +43 -0
- package/src/lib/record/memory-kept.mjs.map +1 -0
- package/src/lib/record/memory-later.d.mts +19 -0
- package/src/lib/record/memory-later.d.ts +19 -0
- package/src/lib/record/memory-later.js +39 -0
- package/src/lib/record/memory-later.js.map +1 -0
- package/src/lib/record/memory-later.mjs +35 -0
- package/src/lib/record/memory-later.mjs.map +1 -0
- package/src/lib/record/memory-part.d.mts +7 -0
- package/src/lib/record/memory-part.d.ts +7 -0
- package/src/lib/record/memory-part.js +13 -0
- package/src/lib/record/memory-part.js.map +1 -0
- package/src/lib/record/memory-part.mjs +8 -0
- package/src/lib/record/memory-part.mjs.map +1 -0
- package/src/lib/record/messages.d.mts +27 -2
- package/src/lib/record/messages.d.ts +27 -2
- package/src/lib/record/messages.js +13 -4
- package/src/lib/record/messages.js.map +1 -1
- package/src/lib/record/messages.mjs +13 -4
- package/src/lib/record/messages.mjs.map +1 -1
- package/src/lib/record/option-rules.d.mts +56 -0
- package/src/lib/record/option-rules.d.ts +56 -0
- package/src/lib/record/option-rules.js +143 -0
- package/src/lib/record/option-rules.js.map +1 -0
- package/src/lib/record/option-rules.mjs +140 -0
- package/src/lib/record/option-rules.mjs.map +1 -0
- package/src/lib/record/record-words.d.mts +13 -1
- package/src/lib/record/record-words.d.ts +13 -1
- package/src/lib/record/record-words.js +29 -1
- package/src/lib/record/record-words.js.map +1 -1
- package/src/lib/record/record-words.mjs +27 -1
- package/src/lib/record/record-words.mjs.map +1 -1
- package/src/lib/record/rules.d.mts +2 -2
- package/src/lib/record/rules.d.ts +2 -2
- package/src/lib/record/rules.js +3 -3
- package/src/lib/record/rules.js.map +1 -1
- package/src/lib/record/rules.mjs +3 -3
- package/src/lib/record/rules.mjs.map +1 -1
- package/src/lib/record/run.js +9 -7
- package/src/lib/record/run.js.map +1 -1
- package/src/lib/record/run.mjs +9 -7
- package/src/lib/record/run.mjs.map +1 -1
- package/src/lib/record/search.d.mts +12 -2
- package/src/lib/record/search.d.ts +12 -2
- package/src/lib/record/search.js +29 -4
- package/src/lib/record/search.js.map +1 -1
- package/src/lib/record/search.mjs +29 -4
- package/src/lib/record/search.mjs.map +1 -1
- package/src/lib/record/set-when.d.mts +3 -3
- package/src/lib/record/set-when.d.ts +3 -3
- package/src/lib/record/set-when.js +9 -9
- package/src/lib/record/set-when.js.map +1 -1
- package/src/lib/record/set-when.mjs +9 -9
- package/src/lib/record/set-when.mjs.map +1 -1
- package/src/lib/record/signed.d.mts +21 -0
- package/src/lib/record/signed.d.ts +21 -0
- package/src/lib/record/signed.js +62 -0
- package/src/lib/record/signed.js.map +1 -0
- package/src/lib/record/signed.mjs +57 -0
- package/src/lib/record/signed.mjs.map +1 -0
- package/src/lib/record/taking.d.mts +60 -0
- package/src/lib/record/taking.d.ts +60 -0
- package/src/lib/record/taking.js +58 -0
- package/src/lib/record/taking.js.map +1 -0
- package/src/lib/record/taking.mjs +50 -0
- package/src/lib/record/taking.mjs.map +1 -0
- package/src/lib/record/value-map.js +11 -4
- package/src/lib/record/value-map.js.map +1 -1
- package/src/lib/record/value-map.mjs +11 -4
- package/src/lib/record/value-map.mjs.map +1 -1
- package/src/lib/record/values.d.mts +20 -0
- package/src/lib/record/values.d.ts +20 -0
- package/src/lib/record/values.js.map +1 -1
- package/src/lib/record/values.mjs.map +1 -1
- package/src/lib/record/waits.d.mts +13 -6
- package/src/lib/record/waits.d.ts +13 -6
- package/src/lib/record/waits.js +9 -12
- package/src/lib/record/waits.js.map +1 -1
- package/src/lib/record/waits.mjs +9 -12
- package/src/lib/record/waits.mjs.map +1 -1
- package/src/runtime.d.mts +17 -6
- package/src/runtime.d.ts +17 -6
- package/src/runtime.js +45 -3
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +17 -1
- package/src/runtime.mjs.map +1 -1
package/src/lib/record/form.js
CHANGED
|
@@ -4,9 +4,13 @@ exports.unsavedWork = unsavedWork;
|
|
|
4
4
|
exports.workAtRisk = workAtRisk;
|
|
5
5
|
exports.createForm = createForm;
|
|
6
6
|
exports.notANumber = notANumber;
|
|
7
|
+
const choice_options_1 = require("../format/choice-options");
|
|
8
|
+
const words_1 = require("../format/words");
|
|
7
9
|
const upgrade_1 = require("../format/upgrade");
|
|
8
10
|
const zoned_1 = require("../format/zoned");
|
|
9
11
|
const modifier_1 = require("../expression/modifier");
|
|
12
|
+
const functions_1 = require("../expression/functions");
|
|
13
|
+
const app_functions_1 = require("../expression/app-functions");
|
|
10
14
|
const zones_1 = require("./zones");
|
|
11
15
|
const check_1 = require("./check");
|
|
12
16
|
const inputs_1 = require("./inputs");
|
|
@@ -20,10 +24,14 @@ const roles_1 = require("./roles");
|
|
|
20
24
|
const cells_1 = require("./cells");
|
|
21
25
|
const messages_1 = require("./messages");
|
|
22
26
|
const record_words_1 = require("./record-words");
|
|
27
|
+
const line_choices_1 = require("./line-choices");
|
|
23
28
|
const data_source_1 = require("./data-source");
|
|
29
|
+
const taking_1 = require("./taking");
|
|
30
|
+
const drafts_1 = require("./drafts");
|
|
24
31
|
const waits_1 = require("./waits");
|
|
25
32
|
const uploads_1 = require("./uploads");
|
|
26
33
|
const scheduler_1 = require("./scheduler");
|
|
34
|
+
const signed_1 = require("./signed");
|
|
27
35
|
const run_1 = require("./run");
|
|
28
36
|
const values_1 = require("./values");
|
|
29
37
|
/**
|
|
@@ -83,16 +91,53 @@ function innerForm(options, within, place, outer) {
|
|
|
83
91
|
const page = (0, zoned_1.zonedPage)(options.page);
|
|
84
92
|
const scheduler = options.scheduler ?? scheduler_1.hostScheduler;
|
|
85
93
|
// A field's label small inside a message's sentence where the page asks (`lowerLabels`).
|
|
86
|
-
|
|
94
|
+
// …and a day written in the page's order where it says one (`look.dateOrder`): "can't be after Oct 20, 2026".
|
|
95
|
+
const dateOrder = page.look?.dateOrder;
|
|
96
|
+
const messages = page.lowerLabels || dateOrder ? { ...(options.messages ?? messages_1.MESSAGES.en), lowerLabels: page.lowerLabels, dateOrder } : (options.messages ?? messages_1.MESSAGES.en);
|
|
87
97
|
/** One of the form's sentences in the words the page names its records by (`records`), else as the language says it plainly. */
|
|
88
98
|
const named = (noun, plain) => (0, record_words_1.recordSentence)(page.records, noun, plain, messages.locale);
|
|
89
|
-
|
|
99
|
+
/** The app's functions the page's expressions call: a name none may call is refused now. */
|
|
100
|
+
const table = (0, app_functions_1.functionTable)(options.functions);
|
|
101
|
+
/**
|
|
102
|
+
* The answers of the app's functions that ask, for the record shown (`ExpressionEnv.answer`), by function and
|
|
103
|
+
* values: each asked once, `asking` until it comes. Another record shown, they start again: an answer for the last
|
|
104
|
+
* one is laid on none.
|
|
105
|
+
*/
|
|
106
|
+
let answers = new Map();
|
|
107
|
+
/** Those on their way: a save waits for them. */
|
|
108
|
+
const asking = new Set();
|
|
109
|
+
/** Everything the form waits on (`track`): there before the form's first values are worked out, which may ask. */
|
|
110
|
+
const pending = new Set();
|
|
111
|
+
function answer(key, asked) {
|
|
112
|
+
const kept = answers.get(key);
|
|
113
|
+
if (kept !== undefined || !asked)
|
|
114
|
+
return kept;
|
|
115
|
+
const map = answers;
|
|
116
|
+
map.set(key, 'asking');
|
|
117
|
+
const land = (found) => {
|
|
118
|
+
asking.delete(going);
|
|
119
|
+
if (map !== answers || disposed)
|
|
120
|
+
return;
|
|
121
|
+
map.set(key, found);
|
|
122
|
+
answered();
|
|
123
|
+
};
|
|
124
|
+
const going = Promise.resolve(asked).then((value) => land({ value: value === undefined ? null : value }), (error) => land({ value: null, failed: error instanceof Error ? error.message : String(error) }));
|
|
125
|
+
asking.add(going);
|
|
126
|
+
track(going);
|
|
127
|
+
return 'asking';
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* What every expression of the form is handed of the app's — and, on a page calling Fieldia's functions whose bodies
|
|
131
|
+
* come in the functions part before it has come, the answers they wait for it with, as an app's function that asks.
|
|
132
|
+
*/
|
|
133
|
+
const app = table ? { functions: table, answer } : !functions_1.functionsPart.loaded && (0, functions_1.callsPartFunctions)(page) ? { answer } : {};
|
|
134
|
+
const index = indexLayout(page.layout, table);
|
|
90
135
|
/** Each field node's answer rules, read once. */
|
|
91
136
|
const nodeRules = new Map();
|
|
92
137
|
for (const node of index.values()) {
|
|
93
138
|
const rules = node.source.validate;
|
|
94
139
|
if (node.kind === 'field' && rules)
|
|
95
|
-
nodeRules.set(node.id, (0, rules_1.compileRules)(rules));
|
|
140
|
+
nodeRules.set(node.id, (0, rules_1.compileRules)(rules, table));
|
|
96
141
|
}
|
|
97
142
|
/** Tables of lines with rules of their own, by their field node: each read once. */
|
|
98
143
|
const tables = new Map();
|
|
@@ -101,14 +146,14 @@ function innerForm(options, within, place, outer) {
|
|
|
101
146
|
if (node.kind !== 'field' || page.fields[source.field]?.type !== 'one2many')
|
|
102
147
|
continue;
|
|
103
148
|
if (source.cells || source.rowTones || source.rowBold !== undefined || source.rowButtons || source.lineDelete !== undefined)
|
|
104
|
-
tables.set(node.id, { field: source.field, compiled: (0, cells_1.compileTable)(source) });
|
|
149
|
+
tables.set(node.id, { field: source.field, compiled: (0, cells_1.compileTable)(source, table) });
|
|
105
150
|
}
|
|
106
151
|
/** Field nodes whose value takes a tone or bold, by id: each read once. */
|
|
107
152
|
const fieldTones = new Map();
|
|
108
153
|
for (const node of index.values()) {
|
|
109
154
|
const source = node.source;
|
|
110
155
|
if (node.kind === 'field' && (source.tones || source.bold !== undefined))
|
|
111
|
-
fieldTones.set(node.id, (0, cells_1.compileFieldTone)(source));
|
|
156
|
+
fieldTones.set(node.id, (0, cells_1.compileFieldTone)(source, table));
|
|
112
157
|
}
|
|
113
158
|
/** The tables whose lines load in an order of their own fields, by the field: the first node to give one. */
|
|
114
159
|
const lineOrders = new Map();
|
|
@@ -136,7 +181,7 @@ function innerForm(options, within, place, outer) {
|
|
|
136
181
|
const placeholders = new Map();
|
|
137
182
|
const placeholderOf = (id, own) => {
|
|
138
183
|
if (own.placeholderWhen?.length)
|
|
139
|
-
placeholders.set(id, { fixed: own.placeholder, when: own.placeholderWhen.map((item) => ({ text: item.text, holds: (0, modifier_1.compileModifier)(item.when) })) });
|
|
184
|
+
placeholders.set(id, { fixed: own.placeholder, when: own.placeholderWhen.map((item) => ({ text: item.text, holds: (0, modifier_1.compileModifier)(item.when, table) })) });
|
|
140
185
|
};
|
|
141
186
|
for (const node of index.values())
|
|
142
187
|
if (node.kind === 'field' && 'type' in node.source)
|
|
@@ -146,9 +191,11 @@ function innerForm(options, within, place, outer) {
|
|
|
146
191
|
/** Whether any rule only warns: without one, there are never warnings to look for. */
|
|
147
192
|
const warns = [...nodeRules.values()].some((rules) => rules.some((compiled) => compiled.rule.level === 'warning'));
|
|
148
193
|
const steps = page.layout.type === 'wizard' ? page.layout.children.map((step) => step.id) : [];
|
|
149
|
-
const
|
|
194
|
+
const storeKey = () => `fieldia:draft:${page.id}:${state.recordId ?? 'new'}`;
|
|
150
195
|
/** Today, on the clock of the page's time zone (the browser's own when it names none). */
|
|
151
196
|
const today = () => (0, zones_1.dayIn)(options.now?.() ?? new Date(), page.timeZone);
|
|
197
|
+
/** Whether a field's own words — a label, a choice's — show a value: found once, so a page with none checks as before. */
|
|
198
|
+
const worded = /\{[A-Za-z_]\w*(\|[^{}]*)?\}/.test(JSON.stringify(page.fields));
|
|
152
199
|
const clock = () => (options.now?.() ?? new Date()).getTime();
|
|
153
200
|
/** What a page's bot check saw — its hidden box filled, the captcha's token — and when the page was shown or started again. */
|
|
154
201
|
let bot = { honeypot: false, captcha: null, since: clock() };
|
|
@@ -168,10 +215,10 @@ function innerForm(options, within, place, outer) {
|
|
|
168
215
|
/** Whether the record is being edited now: not while a view locks it, nor when its source lets the person only read it. */
|
|
169
216
|
const editingNow = () => editing && !(started && state.can?.edit === false);
|
|
170
217
|
const about = () => ({ recordId: started ? state.recordId : (options.recordId ?? null), user: options.user, editing: editingNow(), context: options.context, parent: parentContext });
|
|
171
|
-
const computed = (0, compute_1.compileComputed)(page, today, about);
|
|
218
|
+
const computed = (0, compute_1.compileComputed)(page, today, about, app);
|
|
172
219
|
/** Each value worked out from others — by the page, or by the app as the lines change — and what it waits for. */
|
|
173
|
-
const workedOut = (0, waits_1.workedOutFrom)(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent);
|
|
174
|
-
const setWhen = (0, set_when_1.compileSetWhen)(page, today, about);
|
|
220
|
+
const workedOut = (0, waits_1.workedOutFrom)(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent, table);
|
|
221
|
+
const setWhen = (0, set_when_1.compileSetWhen)(page, today, about, app);
|
|
175
222
|
/** The saved forms placed on the page, by the part's id: each one's answers sit under its name, as JSON. */
|
|
176
223
|
const parts = new Map();
|
|
177
224
|
for (const node of index.values())
|
|
@@ -223,7 +270,7 @@ function innerForm(options, within, place, outer) {
|
|
|
223
270
|
const values = (0, values_1.initialValues)(fields, today());
|
|
224
271
|
if (isNew) {
|
|
225
272
|
const about = { recordId: null, user: options.user, context: options.context, parent: parentContext };
|
|
226
|
-
Object.assign(values, (0, value_map_1.firstValues)(page.fields, mapScope(values, page.fields, (0, env_1.recordContext)(values, page.fields, about), (0, env_1.expressionEnv)(values, page.fields, today))));
|
|
273
|
+
Object.assign(values, (0, value_map_1.firstValues)(page.fields, mapScope(values, page.fields, (0, env_1.recordContext)(values, page.fields, about), (0, env_1.expressionEnv)(values, page.fields, today, app))));
|
|
227
274
|
}
|
|
228
275
|
return given ? ordered({ ...values, ...(0, values_1.structuredCopy)(options.values ?? {}) }) : values;
|
|
229
276
|
}
|
|
@@ -252,7 +299,7 @@ function innerForm(options, within, place, outer) {
|
|
|
252
299
|
function lineScope(field, values) {
|
|
253
300
|
const def = lineField(field);
|
|
254
301
|
const lineFields = def.fields;
|
|
255
|
-
return mapScope(values, lineFields, (0, env_1.lineContext)(values, lineFields, reading().context), { today }, { values: state.values, fields: page.fields });
|
|
302
|
+
return mapScope(values, lineFields, (0, env_1.lineContext)(values, lineFields, reading().context), { ...app, today }, { values: state.values, fields: page.fields });
|
|
256
303
|
}
|
|
257
304
|
function tableOf(nodeId) {
|
|
258
305
|
const table = tables.get(nodeId);
|
|
@@ -272,7 +319,7 @@ function innerForm(options, within, place, outer) {
|
|
|
272
319
|
if (!line)
|
|
273
320
|
throw new Error(`"${table.field}" has no line "${key}"`);
|
|
274
321
|
const def = lineField(table.field);
|
|
275
|
-
return table.compiled.line((0, env_1.lineContext)(line.values, def.fields, reading().context, line.id ?? null), { today }, userRoles);
|
|
322
|
+
return table.compiled.line((0, env_1.lineContext)(line.values, def.fields, reading().context, line.id ?? null), { ...app, today }, userRoles);
|
|
276
323
|
}
|
|
277
324
|
function createValuesOf(field, line) {
|
|
278
325
|
if (!line) {
|
|
@@ -285,7 +332,9 @@ function innerForm(options, within, place, outer) {
|
|
|
285
332
|
return {};
|
|
286
333
|
return (0, value_map_1.readLooseMap)(sub.createValues, lineScope(line.lines, found.values));
|
|
287
334
|
}
|
|
288
|
-
|
|
335
|
+
/** The values worked out from an app's function that asks with no answer yet, as the values shown last settled. */
|
|
336
|
+
let askedNow = new Map();
|
|
337
|
+
let baseline = computed.apply(startingValues(), askedNow);
|
|
289
338
|
/** The setWhen conditions holding for the values as they last settled: one starts to hold only against these. */
|
|
290
339
|
let holding = setWhen.holding(baseline);
|
|
291
340
|
let state = {
|
|
@@ -312,6 +361,12 @@ function innerForm(options, within, place, outer) {
|
|
|
312
361
|
unsent: [],
|
|
313
362
|
leaving: false,
|
|
314
363
|
unread: {},
|
|
364
|
+
sent: null,
|
|
365
|
+
asked: Object.fromEntries(askedNow),
|
|
366
|
+
takenOut: {},
|
|
367
|
+
// Worked out once the form has its state, as the limits part comes (`limitsCode`).
|
|
368
|
+
limits: null,
|
|
369
|
+
resume: null,
|
|
315
370
|
};
|
|
316
371
|
started = true;
|
|
317
372
|
/**
|
|
@@ -484,7 +539,6 @@ function innerForm(options, within, place, outer) {
|
|
|
484
539
|
let onchangeSeq = 0;
|
|
485
540
|
let draftTimer = null;
|
|
486
541
|
let autosaveTimer = null;
|
|
487
|
-
const pending = new Set();
|
|
488
542
|
const listeners = new Set();
|
|
489
543
|
/** The app's listeners, by event. */
|
|
490
544
|
const events = new Map();
|
|
@@ -498,10 +552,30 @@ function innerForm(options, within, place, outer) {
|
|
|
498
552
|
const userRoles = [...(options.user?.roles ?? [])];
|
|
499
553
|
/** The latest load of each field's choices: an answer to an older one is let go. */
|
|
500
554
|
const choiceLoads = {};
|
|
555
|
+
/** The tables' columns whose choices are the app's: none on most pages, and then nothing below is followed. */
|
|
556
|
+
const listed = (0, line_choices_1.listedColumns)(page.fields);
|
|
557
|
+
/** Whether the values set now are a change someone made (`writeValues`), after which a line's choice no longer offered is cleared. */
|
|
558
|
+
let edit = false;
|
|
501
559
|
function set(patch) {
|
|
502
560
|
const was = state.values;
|
|
561
|
+
const choicesWas = state.choices;
|
|
503
562
|
const shown = state.errors;
|
|
563
|
+
const edited = edit;
|
|
564
|
+
edit = false;
|
|
565
|
+
const stepWas = state.step;
|
|
504
566
|
state = { ...state, ...patch };
|
|
567
|
+
// A line's choices go with their line.
|
|
568
|
+
if (state.values !== was)
|
|
569
|
+
state = { ...state, choices: choicesNow(state.choices) };
|
|
570
|
+
// What was said taken out is said while it is true: an option offered again, or picked again, is said no more.
|
|
571
|
+
if ((state.values !== was || state.choices !== choicesWas) && Object.keys(state.takenOut).length) {
|
|
572
|
+
const takenOut = takenOutTrue();
|
|
573
|
+
if (takenOut !== state.takenOut)
|
|
574
|
+
state = { ...state, takenOut };
|
|
575
|
+
}
|
|
576
|
+
// What waits for an app's answer, as the values shown last settled.
|
|
577
|
+
if (patch.values !== undefined && patch.asked === undefined && !sameAsked(state.asked, askedNow))
|
|
578
|
+
state = { ...state, asked: Object.fromEntries(askedNow) };
|
|
505
579
|
// The person's changes the last save did not send, worked out again from the values as they are now, and from the
|
|
506
580
|
// text they left that does not read.
|
|
507
581
|
if (patch.unsent === undefined && (patch.values !== undefined || patch.unread !== undefined)) {
|
|
@@ -517,15 +591,21 @@ function innerForm(options, within, place, outer) {
|
|
|
517
591
|
listener(state);
|
|
518
592
|
// Choices that change with a value that just changed are asked for again.
|
|
519
593
|
if (state.values !== was) {
|
|
594
|
+
// After someone's change, a choice its new choices do not offer is taken out and said, as a line's is.
|
|
520
595
|
for (const name in state.choices)
|
|
521
|
-
if (fromList(name)
|
|
522
|
-
loadChoices(name);
|
|
596
|
+
if (!name.includes('.') && fromList(name)?.dependsOn?.some((other) => was[other] !== state.values[other]))
|
|
597
|
+
loadChoices(name, edited);
|
|
523
598
|
for (const name in state.definitions)
|
|
524
599
|
if (fromRecord(name).dependsOn?.some((other) => was[other] !== state.values[other]))
|
|
525
600
|
loadDefinitions(name);
|
|
601
|
+
if (listed.length)
|
|
602
|
+
followLines(was, edited);
|
|
526
603
|
}
|
|
527
604
|
if (showing && state.step !== told)
|
|
528
605
|
tellStep(entering);
|
|
606
|
+
// A wizard's step entered with answers typed is kept in the draft, so it opens there again.
|
|
607
|
+
if (state.step !== stepWas && showing && state.dirty.length)
|
|
608
|
+
scheduleDraft();
|
|
529
609
|
}
|
|
530
610
|
function emit(event, payload) {
|
|
531
611
|
const heard = events.get(event);
|
|
@@ -548,6 +628,9 @@ function innerForm(options, within, place, outer) {
|
|
|
548
628
|
/** Another record is shown: nothing of the last one's is the person's work any more, and its files on their way stop. */
|
|
549
629
|
function newGeneration() {
|
|
550
630
|
generation++;
|
|
631
|
+
// Another record's lines ask the source anew.
|
|
632
|
+
lineAsks = new Map();
|
|
633
|
+
answers = new Map();
|
|
551
634
|
personal.clear();
|
|
552
635
|
personalLines.clear();
|
|
553
636
|
filed.clear();
|
|
@@ -672,16 +755,129 @@ function innerForm(options, within, place, outer) {
|
|
|
672
755
|
entering = was;
|
|
673
756
|
}
|
|
674
757
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
758
|
+
/** The app's list a choice's options come from; none for one whose options are its own or carried. */
|
|
759
|
+
const fromList = (name) => (0, choice_options_1.optionsList)(page.fields[name]?.optionsFrom);
|
|
760
|
+
// ---- a table line's choices from the app's lists (line-choices.ts) ----------------------------------------------
|
|
761
|
+
/** A line's choices asked of the source, by what they are asked by (`askKey`), for the record shown: lines alike share one. */
|
|
762
|
+
let lineAsks = new Map();
|
|
763
|
+
/** What each line cell's choices were last asked by. */
|
|
764
|
+
const askedBy = new Map();
|
|
765
|
+
const listedAt = (table, column) => listed.find((one) => one.table === table && one.column === column);
|
|
766
|
+
const lineAt = (table, key) => (state.values[table] ?? []).find((line) => line.key === key);
|
|
767
|
+
/** A line's choices from the source: asked once for every line whose columns they change with hold the same values. */
|
|
768
|
+
function askLine(one, values) {
|
|
769
|
+
const key = (0, line_choices_1.askKey)(one, values, state.values);
|
|
770
|
+
const asks = lineAsks;
|
|
771
|
+
let ask = asks.get(key);
|
|
772
|
+
if (ask)
|
|
773
|
+
return ask;
|
|
774
|
+
const source = options.dataSource;
|
|
775
|
+
const request = { list: one.list, values: (0, values_1.structuredCopy)(values), parent: (0, values_1.structuredCopy)(state.values) };
|
|
776
|
+
ask = source?.options ? source.options(request) : Promise.reject(new Error(`No list "${one.list}"`));
|
|
777
|
+
asks.set(key, ask);
|
|
778
|
+
// One that failed is asked again next time.
|
|
779
|
+
ask.catch(() => asks.get(key) === ask && asks.delete(key));
|
|
780
|
+
return ask;
|
|
781
|
+
}
|
|
782
|
+
/** The choices as the values are now: a line's go with their line. */
|
|
783
|
+
function choicesNow(choices) {
|
|
784
|
+
let kept = null;
|
|
785
|
+
for (const path of Object.keys(choices)) {
|
|
786
|
+
if (valueAt(state.values, path) !== gone)
|
|
787
|
+
continue;
|
|
788
|
+
kept ??= { ...choices };
|
|
789
|
+
delete kept[path];
|
|
790
|
+
askedBy.delete(path);
|
|
791
|
+
}
|
|
792
|
+
return kept ?? choices;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Values changed: each line's choices loaded are asked again where what they change with changed on their line (a
|
|
796
|
+
* record shown with the same line, Discard, a draft); after a change someone made (`edited`), judged too — and so is a
|
|
797
|
+
* line holding a choice whose choices nobody asked for yet, so none is kept that its columns no longer offer.
|
|
798
|
+
*/
|
|
799
|
+
function followLines(was, edited) {
|
|
800
|
+
// Choices that change with nothing are asked once.
|
|
801
|
+
for (const one of listed.filter((column) => column.own.length || column.parent.length)) {
|
|
802
|
+
for (const line of state.values[one.table] ?? []) {
|
|
803
|
+
const path = `${one.table}.${line.key}.${one.column}`;
|
|
804
|
+
const by = (0, line_choices_1.askKey)(one, line.values, state.values);
|
|
805
|
+
const old = !(path in state.choices) && edited && !(0, values_1.isEmpty)(one.def, line.values[one.column]) && (was[one.table] ?? []).find((l) => l.key === line.key);
|
|
806
|
+
if (path in state.choices ? by !== askedBy.get(path) : old && by !== (0, line_choices_1.askKey)(one, old.values, was))
|
|
807
|
+
loadChoices(path, edited);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Load a field's choices, or a line cell's by its path (`table.key.column`). `judge`, after a change someone made of
|
|
813
|
+
* what they change with: a choice they no longer offer is taken out and said, on the record they were asked for,
|
|
814
|
+
* while a line still asks by the same.
|
|
815
|
+
*/
|
|
816
|
+
function loadChoices(name, judge = false) {
|
|
817
|
+
const cell = (0, line_choices_1.cellOf)(name);
|
|
818
|
+
const one = cell && listedAt(cell.table, cell.column);
|
|
819
|
+
const line = cell && lineAt(cell.table, cell.key);
|
|
820
|
+
if (cell && (!one || !line))
|
|
821
|
+
throw new Error(`"${name}" is no line's choice from the app's lists`);
|
|
822
|
+
const from = one ?? fromList(name);
|
|
823
|
+
if (!from)
|
|
824
|
+
throw new Error(`"${name}" takes no choices from the app's lists`);
|
|
825
|
+
const { list } = from;
|
|
678
826
|
const load = (choiceLoads[name] = (choiceLoads[name] ?? 0) + 1);
|
|
679
|
-
const
|
|
827
|
+
const by = one && line ? (0, line_choices_1.askKey)(one, line.values, state.values) : '';
|
|
828
|
+
if (cell)
|
|
829
|
+
askedBy.set(name, by);
|
|
830
|
+
// A line's answer lands while its line is there.
|
|
831
|
+
const put = (choices) => load === choiceLoads[name] && (!cell || name in state.choices) && set({ choices: { ...state.choices, [name]: choices } });
|
|
680
832
|
// What was shown stays while the next ones load, so the list does not flash empty.
|
|
681
833
|
const shown = state.choices[name]?.options ?? null;
|
|
682
|
-
|
|
834
|
+
const loading = { options: shown, loading: true };
|
|
835
|
+
// A line's cell asks first as it is drawn, inside its table's drawing: told with the next change, never drawn again
|
|
836
|
+
// inside it. Asked again, as a column they change with changes, it says it is loading at once.
|
|
837
|
+
if (cell && !(name in state.choices))
|
|
838
|
+
state = { ...state, choices: { ...state.choices, [name]: loading } };
|
|
839
|
+
else
|
|
840
|
+
set({ choices: { ...state.choices, [name]: loading } });
|
|
683
841
|
const source = options.dataSource;
|
|
684
|
-
|
|
842
|
+
const onRecord = shownNow();
|
|
843
|
+
const ask = one && line
|
|
844
|
+
? askLine(one, line.values)
|
|
845
|
+
: source?.options
|
|
846
|
+
? // A line's own form (a dialog) asks as its line does: the record it is on as parent.
|
|
847
|
+
source.options({ list, values: (0, values_1.structuredCopy)(state.values), ...(options.parent ? { parent: (0, values_1.structuredCopy)(options.parent.values) } : {}) })
|
|
848
|
+
: Promise.reject(new Error(`No list "${list}"`));
|
|
849
|
+
void track(ask.then((loaded) => {
|
|
850
|
+
if (load !== choiceLoads[name])
|
|
851
|
+
return;
|
|
852
|
+
const now = cell && lineAt(cell.table, cell.key);
|
|
853
|
+
const held = !cell ? state.values[name] : now && (0, line_choices_1.askKey)(one, now.values, state.values) === by ? now.values[cell.column] : null;
|
|
854
|
+
const lost = judge && onRecord() ? (0, line_choices_1.notOffered)(one?.def ?? page.fields[name], held, loaded) : [];
|
|
855
|
+
// Taken out and said: the words stay until its value changes again.
|
|
856
|
+
if (lost.length)
|
|
857
|
+
clearChoices(cell ? cell.table : name, cell, lost, namedChoices(lost, shown, loaded));
|
|
858
|
+
put({ options: loaded });
|
|
859
|
+
// Options of the app's list with places, never counted yet: how many took each is asked.
|
|
860
|
+
if (!cell && loaded.some((option) => option.seats !== undefined) && (!limitsKit || limitsKit.uncounted(name)))
|
|
861
|
+
void track(limitsCode().then((kit) => kit?.ask()));
|
|
862
|
+
}, (error) => put({ options: shown, error: error.message ?? String(error) })));
|
|
863
|
+
}
|
|
864
|
+
/** Choices by their labels, as the choices shown before or now name them. */
|
|
865
|
+
const namedChoices = (lost, shown, loaded) => lost.map((value) => [...(shown ?? []), ...loaded].find((option) => option.value === value)?.label ?? String(value));
|
|
866
|
+
/**
|
|
867
|
+
* A choice — a field's, or a line's (`key`) column — less what its choices no longer offer, worked out from someone's
|
|
868
|
+
* change, and said as every choice taken out is (`FormState.takenOut`): their claim stays.
|
|
869
|
+
*/
|
|
870
|
+
function clearChoices(field, line, lost, labels) {
|
|
871
|
+
const without = (value) => (Array.isArray(value) ? value.filter((v) => !lost.includes(v)) : null);
|
|
872
|
+
const values = state.values;
|
|
873
|
+
const written = line
|
|
874
|
+
? (values[field] ?? []).map((l) => (l.key === line.key ? { ...l, values: { ...l.values, [line.column]: without(l.values[line.column]) } } : l))
|
|
875
|
+
: without(values[field]);
|
|
876
|
+
const path = line ? `${field}.${line.key}.${line.column}` : field;
|
|
877
|
+
takenOutOf.set(path, lost.map((value, i) => ({ value, label: labels[i] })));
|
|
878
|
+
writeValues({ ...values, [field]: written }, {}, false, { [path]: takenOutWords(labels) });
|
|
879
|
+
afterEdit(field);
|
|
880
|
+
changed(field, 'step', { keep: true });
|
|
685
881
|
}
|
|
686
882
|
const fromRecord = (name) => page.fields[name].definitionsFrom;
|
|
687
883
|
const definitionLoads = {};
|
|
@@ -701,8 +897,23 @@ function innerForm(options, within, place, outer) {
|
|
|
701
897
|
set({ definitions: { ...state.definitions, [name]: { definitions } } });
|
|
702
898
|
await track(options.dataSource?.saveDefinitions?.({ list, values: (0, values_1.structuredCopy)(state.values), definitions }) ?? Promise.resolve());
|
|
703
899
|
}
|
|
704
|
-
/**
|
|
900
|
+
/**
|
|
901
|
+
* A field as its value is checked: a list's choices are those loaded, and until they are, any choice may be one; its
|
|
902
|
+
* label and its choices' words with their `{field}`s filled in, as the page shows them.
|
|
903
|
+
*/
|
|
705
904
|
function checked(name) {
|
|
905
|
+
const def = checkedAsLoaded(name);
|
|
906
|
+
return worded ? shownWords(def) : def;
|
|
907
|
+
}
|
|
908
|
+
/** A field whose words show values — its label, its choices' — with them filled in now, else as it is. */
|
|
909
|
+
function shownWords(def) {
|
|
910
|
+
const options = def.type === 'selection' && def.options.some((option) => option.label.includes('{')) ? def.options.map((option) => ({ ...option, label: wordsNow(option.label) })) : null;
|
|
911
|
+
if (!def.label.includes('{') && !options)
|
|
912
|
+
return def;
|
|
913
|
+
return { ...def, label: wordsNow(def.label), ...(options ? { options } : {}) };
|
|
914
|
+
}
|
|
915
|
+
/** A field as its value is checked, its choices those loaded from the app's list. */
|
|
916
|
+
function checkedAsLoaded(name) {
|
|
706
917
|
const def = page.fields[name];
|
|
707
918
|
const loaded = state.choices[name]?.options;
|
|
708
919
|
if (def.type === 'properties' && def.definitionsFrom) {
|
|
@@ -710,7 +921,105 @@ function innerForm(options, within, place, outer) {
|
|
|
710
921
|
const definitions = state.definitions[name]?.definitions;
|
|
711
922
|
return definitions ? { ...def, definitions } : def;
|
|
712
923
|
}
|
|
713
|
-
|
|
924
|
+
// Carried from an earlier question: any of its options, and its own after them, is one of this one's.
|
|
925
|
+
if (def.type === 'selection' && (0, choice_options_1.optionsField)(def.optionsFrom))
|
|
926
|
+
return { ...def, options: everyOption(name), optionsFrom: undefined };
|
|
927
|
+
return def.type === 'selection' && (0, choice_options_1.optionsList)(def.optionsFrom) && loaded ? { ...def, options: loaded, optionsFrom: undefined } : def;
|
|
928
|
+
}
|
|
929
|
+
/** Every option a choice may hold, whatever its conditions: its own, its list's once loaded, or those of the question it carries them from and its own. */
|
|
930
|
+
function everyOption(name, seen = []) {
|
|
931
|
+
const def = page.fields[name];
|
|
932
|
+
if (def?.type !== 'selection')
|
|
933
|
+
return [];
|
|
934
|
+
const carried = (0, choice_options_1.optionsField)(def.optionsFrom);
|
|
935
|
+
if (carried && !seen.includes(carried.field))
|
|
936
|
+
return [...everyOption(carried.field, [...seen, name]).filter((o) => !o.exclusive), ...def.options];
|
|
937
|
+
return [...((fromList(name) && state.choices[name]?.options) || def.options)];
|
|
938
|
+
}
|
|
939
|
+
// ---- options by condition, and carried from an earlier question (the options part: option-rules.ts) -----------
|
|
940
|
+
/** The options' rules made for this form from the options part, once it came: none before, nor on a page with none. */
|
|
941
|
+
let optionKit = null;
|
|
942
|
+
/**
|
|
943
|
+
* The options' rules, made from their part as it comes — at once where it has come — or null on a page with none, or
|
|
944
|
+
* where the part could not come. Values written before they came are judged as they come: a chosen option the values
|
|
945
|
+
* no longer offer, since the form was made, is taken out and said.
|
|
946
|
+
*/
|
|
947
|
+
function optionsCode() {
|
|
948
|
+
if (optionKit || !(0, choice_options_1.hasOptionRules)(page))
|
|
949
|
+
return Promise.resolve(optionKit);
|
|
950
|
+
if (choice_options_1.optionRulesPart.loaded)
|
|
951
|
+
return Promise.resolve(optionsMade(choice_options_1.optionRulesPart.loaded));
|
|
952
|
+
const from = state.values;
|
|
953
|
+
return choice_options_1.optionRulesPart.load().then((part) => {
|
|
954
|
+
const kit = optionsMade(part);
|
|
955
|
+
if (kit && !disposed && state.values !== from)
|
|
956
|
+
writeValues(state.values, {}, false, {}, from);
|
|
957
|
+
return kit;
|
|
958
|
+
}, () => null);
|
|
959
|
+
}
|
|
960
|
+
function optionsMade(part) {
|
|
961
|
+
optionKit ??= part.optionRules({ page, table, app, state: () => state, today, about, reading, settle, words: (labels) => takenOutWords(labels) });
|
|
962
|
+
return optionKit;
|
|
963
|
+
}
|
|
964
|
+
if ((0, choice_options_1.hasOptionRules)(page))
|
|
965
|
+
void track(optionsCode());
|
|
966
|
+
/** A table line's choice's options as they are offered on that line: its list's once loaded, or its own, by their conditions once they came. */
|
|
967
|
+
function lineOffered(lines, line, column, record) {
|
|
968
|
+
if (optionKit)
|
|
969
|
+
return optionKit.lineOffered(lines, line, column, record);
|
|
970
|
+
const sub = page.fields[lines].fields[column];
|
|
971
|
+
return [...(((0, choice_options_1.optionsList)(sub.optionsFrom) && state.choices[`${lines}.${line.key}.${column}`]?.options) || sub.options)];
|
|
972
|
+
}
|
|
973
|
+
/** Options in words, as a sentence joins them: "Garden and Pool". */
|
|
974
|
+
function listedLabels(labels) {
|
|
975
|
+
try {
|
|
976
|
+
return new Intl.ListFormat(messages.locale, { type: 'conjunction' }).format(labels);
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
return labels.join(', ');
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* What is said of options taken out of an answer, by their labels — an answer they show (`{branch}`) as it reads now:
|
|
984
|
+
* "Garden is no longer offered, so it was taken out".
|
|
985
|
+
*/
|
|
986
|
+
const takenOutWords = (labels) => (0, messages_1.fillMessage)(messages, labels.length > 1 ? messages.notOfferedNowMany : messages.notOfferedNow, { name: listedLabels(labels.map((label) => (label.includes('{') ? wordsNow(label) : label))) });
|
|
987
|
+
/** The options each note of `takenOut` names, by its path: a note goes once none of them is still out. */
|
|
988
|
+
const takenOutOf = new Map();
|
|
989
|
+
/**
|
|
990
|
+
* The notes of options taken out (`FormState.takenOut`) that are still true for the values and the choices now: one
|
|
991
|
+
* whose options are all offered again — the type that offers them picked again, the treatment ticked again above, the
|
|
992
|
+
* app's list asked again offering it — or picked again goes; one with some still out names those. A list being asked
|
|
993
|
+
* again says nothing yet. The same notes, when none changed.
|
|
994
|
+
*/
|
|
995
|
+
function takenOutTrue() {
|
|
996
|
+
let kept = null;
|
|
997
|
+
for (const path of Object.keys(state.takenOut)) {
|
|
998
|
+
const options = takenOutOf.get(path);
|
|
999
|
+
const held = valueAt(state.values, path);
|
|
1000
|
+
if (!options || held === gone || state.choices[path]?.loading)
|
|
1001
|
+
continue;
|
|
1002
|
+
const offered = form.offered(path);
|
|
1003
|
+
if (!offered)
|
|
1004
|
+
continue;
|
|
1005
|
+
const chosen = [held].flat();
|
|
1006
|
+
const left = options.filter((option) => !chosen.includes(option.value) && !offered.some((one) => one.value === option.value));
|
|
1007
|
+
if (left.length === options.length)
|
|
1008
|
+
continue;
|
|
1009
|
+
kept ??= { ...state.takenOut };
|
|
1010
|
+
if (left.length) {
|
|
1011
|
+
takenOutOf.set(path, left);
|
|
1012
|
+
kept[path] = takenOutWords(left.map((option) => option.label));
|
|
1013
|
+
}
|
|
1014
|
+
else
|
|
1015
|
+
delete kept[path];
|
|
1016
|
+
}
|
|
1017
|
+
return kept ?? state.takenOut;
|
|
1018
|
+
}
|
|
1019
|
+
/** A line's field as its value is checked: its choices from the app's list, those loaded for its line. */
|
|
1020
|
+
function checkedLine(path, def) {
|
|
1021
|
+
const loaded = def.type === 'selection' && (0, choice_options_1.optionsList)(def.optionsFrom) ? state.choices[path]?.options : null;
|
|
1022
|
+
return loaded ? { ...def, options: loaded, optionsFrom: undefined } : def;
|
|
714
1023
|
}
|
|
715
1024
|
/** A properties field's problems, each at its property's path (`field.property`), as its row shows them. */
|
|
716
1025
|
function propertyProblems(name) {
|
|
@@ -729,12 +1038,57 @@ function innerForm(options, within, place, outer) {
|
|
|
729
1038
|
promise.then(done, done);
|
|
730
1039
|
return promise;
|
|
731
1040
|
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Words with each `{field}` showing the value `values` hold (the form's now, unless given), as the app's viewer
|
|
1043
|
+
* writes it (`valueWords`), else plainly; `other` answers a name first (an ending's `{id}`). A name that is no field
|
|
1044
|
+
* shows nothing.
|
|
1045
|
+
*/
|
|
1046
|
+
function wordsNow(text, values = state.values, other) {
|
|
1047
|
+
if (!text.includes('{'))
|
|
1048
|
+
return text;
|
|
1049
|
+
return (0, words_1.fillValues)(text, (name) => {
|
|
1050
|
+
const own = other?.(name);
|
|
1051
|
+
if (own !== undefined)
|
|
1052
|
+
return own;
|
|
1053
|
+
const def = page.fields[name];
|
|
1054
|
+
if (!def)
|
|
1055
|
+
return '';
|
|
1056
|
+
return options.valueWords ? options.valueWords(def, values[name], values) : wordsOfValue(def, values[name]);
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Each ending's condition, read once — with Fieldia's functions and the app's own, as every condition of the page —
|
|
1061
|
+
* null for the default (no `when`), and one that does not read never holds.
|
|
1062
|
+
*/
|
|
1063
|
+
const endingConditions = (page.endings ?? []).map((ending) => {
|
|
1064
|
+
if (ending.when === undefined)
|
|
1065
|
+
return null;
|
|
1066
|
+
try {
|
|
1067
|
+
return (0, modifier_1.compileModifier)(ending.when, table);
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
return (0, modifier_1.compileModifier)(false);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
/** The ending for the response sent: the first whose condition holds on it — its answers, what the source added, its id — else the default. */
|
|
1074
|
+
function endingNow() {
|
|
1075
|
+
const sent = state.sent;
|
|
1076
|
+
const endings = page.endings;
|
|
1077
|
+
if (!sent || !endings?.length)
|
|
1078
|
+
return null;
|
|
1079
|
+
const values = sent.values;
|
|
1080
|
+
const context = (0, env_1.recordContext)(values, page.fields, { ...about(), recordId: sent.id });
|
|
1081
|
+
// An app's function that asks its server is asked as any condition asks it: it holds once answered.
|
|
1082
|
+
const env = (0, env_1.expressionEnv)(values, page.fields, today, app);
|
|
1083
|
+
const at = endingConditions.findIndex((condition) => !condition || condition.evaluate(context, env));
|
|
1084
|
+
return at === -1 ? null : endings[at];
|
|
1085
|
+
}
|
|
732
1086
|
/** The values as expressions read them, and what else they read: the record's id, the person, the lines' rows, today. */
|
|
733
1087
|
function reading() {
|
|
734
1088
|
if (contextCache?.values !== state.values || contextCache.recordId !== state.recordId || contextCache.editing !== editingNow()) {
|
|
735
1089
|
const values = state.values;
|
|
736
1090
|
const context = (0, env_1.recordContext)(values, page.fields, about());
|
|
737
|
-
contextCache = { values, recordId: state.recordId, editing: editingNow(), context, env: (0, env_1.expressionEnv)(values, page.fields, today) };
|
|
1091
|
+
contextCache = { values, recordId: state.recordId, editing: editingNow(), context, env: (0, env_1.expressionEnv)(values, page.fields, today, app) };
|
|
738
1092
|
}
|
|
739
1093
|
return contextCache;
|
|
740
1094
|
}
|
|
@@ -772,6 +1126,10 @@ function innerForm(options, within, place, outer) {
|
|
|
772
1126
|
if ((node.kind === 'field' || node.kind === 'form') && (can.edit === false || (node.field !== undefined && can.readonlyFields?.includes(node.field) === true)))
|
|
773
1127
|
readonly = true;
|
|
774
1128
|
}
|
|
1129
|
+
// An answer a signature kept signed: locked until the signature is taken away and signed again.
|
|
1130
|
+
const answer = answerOf(node);
|
|
1131
|
+
if (answer !== undefined && signedNow().has(answer))
|
|
1132
|
+
readonly = true;
|
|
775
1133
|
return {
|
|
776
1134
|
invisible,
|
|
777
1135
|
readonly: readonly || (def !== undefined && lockedField(def)),
|
|
@@ -779,6 +1137,17 @@ function innerForm(options, within, place, outer) {
|
|
|
779
1137
|
...(node.pressed ? { pressed: node.pressed.evaluate(ctx, env) } : {}),
|
|
780
1138
|
};
|
|
781
1139
|
}
|
|
1140
|
+
/** The answer a field node shows, by its field — or a saved form placed on the page, by the name its answers are kept under. */
|
|
1141
|
+
function answerOf(node) {
|
|
1142
|
+
return node.kind === 'field' ? node.field : node.kind === 'form' ? node.source.name : undefined;
|
|
1143
|
+
}
|
|
1144
|
+
/** The answers signatures signed, as the values hold them now: worked out once for each change of the values. */
|
|
1145
|
+
let signedCache = null;
|
|
1146
|
+
function signedNow() {
|
|
1147
|
+
if (signedCache?.values !== state.values)
|
|
1148
|
+
signedCache = { values: state.values, answers: (0, signed_1.signedBy)(page, state.values) };
|
|
1149
|
+
return signedCache.answers;
|
|
1150
|
+
}
|
|
782
1151
|
/** Field nodes — or saved forms placed on the page — inside `root` (or the whole page), visible right now; `field`'s alone, when named. */
|
|
783
1152
|
function visibleFieldNodes(root, kind = 'field', field) {
|
|
784
1153
|
const inside = (node) => {
|
|
@@ -807,7 +1176,7 @@ function innerForm(options, within, place, outer) {
|
|
|
807
1176
|
return {};
|
|
808
1177
|
const name = node.field;
|
|
809
1178
|
const def = page.fields[name];
|
|
810
|
-
const label = node.source.label ?? def.label;
|
|
1179
|
+
const label = wordsNow(node.source.label ?? def.label);
|
|
811
1180
|
const { context: ctx, env } = reading();
|
|
812
1181
|
return (0, rules_1.checkRules)(def, label, state.values[name], rules, { context: ctx, env, now: options.now?.() ?? new Date(), messages });
|
|
813
1182
|
}
|
|
@@ -820,10 +1189,35 @@ function innerForm(options, within, place, outer) {
|
|
|
820
1189
|
/** The widget a field is shown with where it keeps its value its own way (a per cent): the first of its nodes'. */
|
|
821
1190
|
const shownWidget = (shown) => shown.map((node) => node.source.widget).find((widget) => widget === 'percentage');
|
|
822
1191
|
/** A field node's error now: its field's own rules first, then its answer rules. */
|
|
1192
|
+
/** A path's field's label: a field's, or a line's cell's (`field.lineKey.subfield`). */
|
|
1193
|
+
function labelOfPath(path) {
|
|
1194
|
+
const parts = path.split('.');
|
|
1195
|
+
const def = page.fields[parts[0]];
|
|
1196
|
+
if (parts.length >= 3 && def?.type === 'one2many')
|
|
1197
|
+
return def.fields[parts[parts.length - 1]]?.label ?? path;
|
|
1198
|
+
return def?.label ?? path;
|
|
1199
|
+
}
|
|
823
1200
|
function nodeError(node) {
|
|
824
1201
|
const name = node.field;
|
|
825
1202
|
return unticked(node) ?? (0, inputs_1.checkInput)(page.fields[name], node.source, state.values[name], messages) ?? (0, check_1.checkValue)(checked(name), state.values[name], nodeState(node.id).required, messages, today(), node.source.widget) ?? ruleCheck(node).error;
|
|
826
1203
|
}
|
|
1204
|
+
/** What a check would refuse now (`collectErrors`), for the state it was found for. */
|
|
1205
|
+
let refusals = null;
|
|
1206
|
+
/**
|
|
1207
|
+
* Whether the form refuses the answer at a path — a field's, or a table line's cell (`field.lineKey.subfield`) — as a
|
|
1208
|
+
* check would now: one it shows that holds a value its rules do not take (under its least, past today, a pattern not
|
|
1209
|
+
* met, an answer rule's error). An empty answer is none, required or not: there is nothing worked out from it to wait.
|
|
1210
|
+
*/
|
|
1211
|
+
function refusedAt(path) {
|
|
1212
|
+
if (refusals?.state !== state)
|
|
1213
|
+
refusals = { state, errors: collectErrors() };
|
|
1214
|
+
if (!refusals.errors[path])
|
|
1215
|
+
return false;
|
|
1216
|
+
const parts = path.split('.');
|
|
1217
|
+
const def = page.fields[parts[0]];
|
|
1218
|
+
const held = valueAt(state.values, path);
|
|
1219
|
+
return held !== gone && !(0, values_1.isEmpty)((parts.length > 2 && def?.type === 'one2many' ? def.fields[parts[parts.length - 1]] : def), held);
|
|
1220
|
+
}
|
|
827
1221
|
/** Warnings for the visible fields of the page, without publishing them. */
|
|
828
1222
|
function collectWarnings() {
|
|
829
1223
|
const warnings = {};
|
|
@@ -868,7 +1262,7 @@ function innerForm(options, within, place, outer) {
|
|
|
868
1262
|
return undefined;
|
|
869
1263
|
const at = (v) => (def.type === 'datetime' ? Date.parse(v) : Date.parse(v.slice(0, 10)));
|
|
870
1264
|
if (at(value) < at(start))
|
|
871
|
-
return (0, messages_1.fillMessage)(messages, messages.endBeforeStart, { end: def.label, start: page.fields[node.field].label });
|
|
1265
|
+
return (0, messages_1.fillMessage)(messages, messages.endBeforeStart, { end: wordsNow(def.label), start: wordsNow(page.fields[node.field].label) });
|
|
872
1266
|
return undefined;
|
|
873
1267
|
}
|
|
874
1268
|
/** Errors for the visible fields under `root`, without publishing them. */
|
|
@@ -906,7 +1300,7 @@ function innerForm(options, within, place, outer) {
|
|
|
906
1300
|
const cell = ruled[sub];
|
|
907
1301
|
if (cell?.invisible || (tables.has(node.id) && tables.get(node.id).compiled.hidden(sub, reading().context, reading().env, userRoles)))
|
|
908
1302
|
continue;
|
|
909
|
-
const lineMessage = (0, check_1.checkValue)(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
|
|
1303
|
+
const lineMessage = (0, check_1.checkValue)(checkedLine(`${name}.${line.key}.${sub}`, subDef), line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
|
|
910
1304
|
if (lineMessage)
|
|
911
1305
|
errors[`${name}.${line.key}.${sub}`] = lineMessage;
|
|
912
1306
|
}
|
|
@@ -917,6 +1311,16 @@ function innerForm(options, within, place, outer) {
|
|
|
917
1311
|
for (const [path, unread] of Object.entries(state.unread))
|
|
918
1312
|
if (seen.has(path.split('.')[0]))
|
|
919
1313
|
errors[path] = unread.message;
|
|
1314
|
+
// A value an app's function is asked for answers for its field: still asked, or why it did not come.
|
|
1315
|
+
for (const [path, found] of Object.entries(state.asked)) {
|
|
1316
|
+
if (!seen.has(path.split('.')[0]) || errors[path])
|
|
1317
|
+
continue;
|
|
1318
|
+
const label = labelOfPath(path);
|
|
1319
|
+
errors[path] = found.failed === undefined ? (0, messages_1.fillMessage)(messages, messages.stillWorkedOut, { label }) : (0, messages_1.fillMessage)(messages, messages.notWorkedOut, { label, problem: found.failed });
|
|
1320
|
+
}
|
|
1321
|
+
// A choice held whose places filled up since it was picked: never sent, said under it.
|
|
1322
|
+
if (limitsKit)
|
|
1323
|
+
Object.assign(errors, limitsKit.fullErrors(seen, errors));
|
|
920
1324
|
// A saved form placed here answers for its fields by its own page's rules.
|
|
921
1325
|
for (const node of visibleFieldNodes(root, 'form')) {
|
|
922
1326
|
const part = attached.get(node.id);
|
|
@@ -941,6 +1345,13 @@ function innerForm(options, within, place, outer) {
|
|
|
941
1345
|
const line = (values[field] ?? []).find((l) => l.key === key);
|
|
942
1346
|
return line ? line.values[sub] : gone;
|
|
943
1347
|
}
|
|
1348
|
+
/** A value — a field's, or a table line's by its cell's path — the same in both: a line that is gone, none. */
|
|
1349
|
+
function sameAt(a, b, path) {
|
|
1350
|
+
if (!path.includes('.'))
|
|
1351
|
+
return a[path] === b[path];
|
|
1352
|
+
const now = valueAt(a, path);
|
|
1353
|
+
return now !== gone && JSON.stringify(now ?? null) === JSON.stringify(valueAt(b, path) ?? null);
|
|
1354
|
+
}
|
|
944
1355
|
/** Unread text whose field has since been written another way — a value typed that reads, the app's — is let go. */
|
|
945
1356
|
function unreadKept(values) {
|
|
946
1357
|
let kept = null;
|
|
@@ -981,7 +1392,7 @@ function innerForm(options, within, place, outer) {
|
|
|
981
1392
|
const value = valueAt(state.values, path);
|
|
982
1393
|
if (!sub || value === gone || (0, values_1.isEmpty)(sub, value))
|
|
983
1394
|
return undefined;
|
|
984
|
-
return (0, check_1.checkValue)(sub, value, false, messages, today());
|
|
1395
|
+
return (0, check_1.checkValue)(checkedLine(path, sub), value, false, messages, today());
|
|
985
1396
|
}
|
|
986
1397
|
/**
|
|
987
1398
|
* Answers changed from outside a saved form — loaded, put back, set — handed
|
|
@@ -1010,25 +1421,77 @@ function innerForm(options, within, place, outer) {
|
|
|
1010
1421
|
* few rounds at most, so rules that set each other cannot run forever.
|
|
1011
1422
|
*/
|
|
1012
1423
|
function settle(written) {
|
|
1013
|
-
let
|
|
1424
|
+
let asked = new Map();
|
|
1425
|
+
let values = computed.apply(written, asked);
|
|
1014
1426
|
for (let round = 0; round < 8; round++) {
|
|
1015
1427
|
const result = setWhen.apply(values, holding);
|
|
1016
1428
|
holding = result.holding;
|
|
1017
1429
|
if (result.values === values)
|
|
1018
1430
|
break;
|
|
1019
|
-
|
|
1431
|
+
asked = new Map();
|
|
1432
|
+
values = computed.apply(result.values, asked);
|
|
1020
1433
|
}
|
|
1434
|
+
askedNow = asked;
|
|
1021
1435
|
return values;
|
|
1022
1436
|
}
|
|
1023
1437
|
/** Values to start again from, as loaded or restored: conditions holding in them hold already, and set nothing. */
|
|
1024
1438
|
function startFrom(values) {
|
|
1025
|
-
const
|
|
1439
|
+
const asked = new Map();
|
|
1440
|
+
const worked = computed.apply(values, asked);
|
|
1441
|
+
askedNow = asked;
|
|
1026
1442
|
holding = setWhen.holding(worked);
|
|
1027
1443
|
return worked;
|
|
1028
1444
|
}
|
|
1029
|
-
/**
|
|
1030
|
-
|
|
1031
|
-
|
|
1445
|
+
/**
|
|
1446
|
+
* An app's function answered: the values worked out again with its answer, and the record as loaded with them, so
|
|
1447
|
+
* an answer is never a change of the person's. Values no one has changed start again from it — no rule starts to
|
|
1448
|
+
* hold on what only came late — and changed ones settle as any change does.
|
|
1449
|
+
*/
|
|
1450
|
+
function answered() {
|
|
1451
|
+
baseline = computed.apply(baseline);
|
|
1452
|
+
if (state.dirty.length)
|
|
1453
|
+
return writeValues(state.values);
|
|
1454
|
+
const values = startFrom(state.values);
|
|
1455
|
+
set({ values, dirty: dirtyFields(values) });
|
|
1456
|
+
}
|
|
1457
|
+
/** Every answer on its way, and those that come of them, waited for: what a save does before it checks. */
|
|
1458
|
+
async function asksSettled() {
|
|
1459
|
+
while (asking.size)
|
|
1460
|
+
await Promise.all([...asking]);
|
|
1461
|
+
}
|
|
1462
|
+
/** Answers that failed asked again — a save's try — and waited for. */
|
|
1463
|
+
async function askAgain() {
|
|
1464
|
+
let failed = false;
|
|
1465
|
+
for (const [key, found] of answers) {
|
|
1466
|
+
if (found === 'asking' || found.failed === undefined)
|
|
1467
|
+
continue;
|
|
1468
|
+
answers.delete(key);
|
|
1469
|
+
failed = true;
|
|
1470
|
+
}
|
|
1471
|
+
if (failed)
|
|
1472
|
+
writeValues(state.values);
|
|
1473
|
+
await asksSettled();
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Write values someone or something changed. `fresh` values are a starting point instead (a restored draft). `said`:
|
|
1477
|
+
* options taken out of an answer by the write, by path, with their words (`FormState.takenOut`); `from`, the values
|
|
1478
|
+
* those the write no longer offers were offered in (the form's now, unless said).
|
|
1479
|
+
*/
|
|
1480
|
+
function writeValues(written, extra = {}, fresh = false, said = {}, from = state.values) {
|
|
1481
|
+
let settled = fresh ? startFrom(written) : settle(written);
|
|
1482
|
+
// A choice written again says no more what was taken out of it — a line's, nor once its line is gone; options a
|
|
1483
|
+
// change no longer offers are taken out now.
|
|
1484
|
+
let takenOut = fresh ? {} : { ...Object.fromEntries(Object.entries(state.takenOut).filter(([path]) => sameAt(written, state.values, path))), ...said };
|
|
1485
|
+
const out = fresh || !optionKit ? null : optionKit.takeOut(from, settled);
|
|
1486
|
+
if (out) {
|
|
1487
|
+
settled = out.values;
|
|
1488
|
+
takenOut = { ...takenOut, ...out.said };
|
|
1489
|
+
for (const [path, gone] of Object.entries(out.gone))
|
|
1490
|
+
takenOutOf.set(path, gone);
|
|
1491
|
+
}
|
|
1492
|
+
if (Object.keys(takenOut).length || Object.keys(state.takenOut).length)
|
|
1493
|
+
extra = { takenOut, ...extra };
|
|
1494
|
+
const values = syncParts(settled, fresh ? 'fresh' : 'edit');
|
|
1032
1495
|
// Text left unread in a box goes once its field is written another way, and its words with it.
|
|
1033
1496
|
const unread = unreadKept(values);
|
|
1034
1497
|
const before = state;
|
|
@@ -1054,6 +1517,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1054
1517
|
}
|
|
1055
1518
|
if (Object.keys(words).length)
|
|
1056
1519
|
errors = { ...errors, ...words };
|
|
1520
|
+
// A change someone made: a line's choice its columns no longer offer is cleared (followLines); a draft restored is none.
|
|
1521
|
+
edit = !fresh;
|
|
1057
1522
|
set({ values, dirty: dirtyFields(values), errors, warnings: withValues(values, collectWarnings), unread, ...extra });
|
|
1058
1523
|
}
|
|
1059
1524
|
/** Once errors are on screen, keep them current as the person fixes things. */
|
|
@@ -1076,24 +1541,84 @@ function innerForm(options, within, place, outer) {
|
|
|
1076
1541
|
kept[key] ??= unstoredNow[key];
|
|
1077
1542
|
return kept;
|
|
1078
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Whether drafts are kept on the app's server: its source keeps them, and the page offers "Continue later" (or the
|
|
1546
|
+
* form continues one by its key). A page without either keeps its drafts where it always did.
|
|
1547
|
+
*/
|
|
1548
|
+
const onServer = () => !!options.dataSource?.saveDraft && !!options.dataSource.loadDraft && (page.continueLater === true || !!options.resume);
|
|
1549
|
+
/** Whether drafts are kept: the app gave a store, or they are kept on the server. */
|
|
1550
|
+
const draftsOn = () => !!options.drafts || onServer();
|
|
1551
|
+
/** The key of the draft kept on the server for the record shown; null until the source gave one. */
|
|
1552
|
+
let serverKey = null;
|
|
1553
|
+
/** Counted each time the draft is let go (sent, discarded, another record shown): a keeping answered after is dropped. */
|
|
1554
|
+
let draftEpoch = 0;
|
|
1555
|
+
/** The draft kept on the server last asked to be kept: the next waits for it, so it carries the key it gave. */
|
|
1556
|
+
let draftGoing = Promise.resolve();
|
|
1557
|
+
/** How many drafts are on their way to the server. */
|
|
1558
|
+
let keeping = 0;
|
|
1559
|
+
/** The draft found as the form opened, offered to restore (`state.draft`). */
|
|
1560
|
+
let found = null;
|
|
1561
|
+
/** The key the form was given to continue from (`resume`), until it is opened. */
|
|
1562
|
+
let resumeKey = outer ? undefined : options.resume;
|
|
1079
1563
|
function scheduleDraft() {
|
|
1080
|
-
|
|
1081
|
-
if (!drafts)
|
|
1564
|
+
if (!draftsOn())
|
|
1082
1565
|
return;
|
|
1083
1566
|
if (draftTimer !== null)
|
|
1084
1567
|
scheduler.clearTimeout(draftTimer);
|
|
1085
1568
|
draftTimer = scheduler.setTimeout(() => {
|
|
1086
1569
|
draftTimer = null;
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1570
|
+
// On the server, a failure is kept in the browser's store instead, where there is one: the form goes on.
|
|
1571
|
+
void track(keepDraft(false).catch(() => keepHere(draftNow())));
|
|
1572
|
+
}, options.drafts?.delayMs ?? (onServer() ? 1500 : 400));
|
|
1573
|
+
}
|
|
1574
|
+
/** The draft of what is typed now: the answers, the wizard's step and the steps passed over, text that does not read. */
|
|
1575
|
+
function draftNow() {
|
|
1576
|
+
// Where files are stored apart, never a file's bytes: they would fill the browser's store (uploads.ts). Without, kept as always.
|
|
1577
|
+
const values = options.dataSource?.upload ? draftOf(state.values) : (0, values_1.structuredCopy)(state.values);
|
|
1578
|
+
return {
|
|
1579
|
+
values,
|
|
1580
|
+
...(steps.length ? { step: state.step, skipped: [...state.skipped] } : {}),
|
|
1581
|
+
...(Object.keys(state.unread).length ? { unread: (0, values_1.structuredCopy)(state.unread) } : {}),
|
|
1582
|
+
answered: Object.keys(fields).filter((name) => state.dirty.includes(name)),
|
|
1583
|
+
savedAt: new Date(clock()).toISOString(),
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
/** A draft in the browser's store, where the app gave one: a store that is full keeps none. */
|
|
1587
|
+
function keepHere(kept) {
|
|
1588
|
+
try {
|
|
1589
|
+
options.drafts?.store.setItem(storeKey(), JSON.stringify(kept));
|
|
1590
|
+
}
|
|
1591
|
+
catch {
|
|
1592
|
+
// The draft is a convenience: the form goes on without it.
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Keep what is typed now: on the server where the source keeps drafts — one at a time, each carrying the key the last
|
|
1597
|
+
* was given; its key in the browser's store — else in the browser's store. The server's answer, or null.
|
|
1598
|
+
*/
|
|
1599
|
+
function keepDraft(later) {
|
|
1600
|
+
const draft = draftNow();
|
|
1601
|
+
const source = options.dataSource;
|
|
1602
|
+
if (!onServer() || !source?.saveDraft) {
|
|
1603
|
+
keepHere(draft);
|
|
1604
|
+
return Promise.resolve(null);
|
|
1605
|
+
}
|
|
1606
|
+
const shown = shownNow();
|
|
1607
|
+
const epoch = draftEpoch;
|
|
1608
|
+
keeping++;
|
|
1609
|
+
const going = draftGoing.then(() => source.saveDraft({ pageId: page.id, model: page.data.kind === 'record' ? page.data.model : '', recordId: state.recordId, key: epoch === draftEpoch ? serverKey : null, draft, ...(later ? { later } : {}) }));
|
|
1610
|
+
draftGoing = going.then(() => void keeping--, () => void keeping--);
|
|
1611
|
+
return going.then((saved) => {
|
|
1612
|
+
// Kept for a record no longer shown, or for answers sent or let go meanwhile: the source has it; nothing is laid here.
|
|
1613
|
+
if (!shown() || epoch !== draftEpoch)
|
|
1614
|
+
return saved;
|
|
1615
|
+
serverKey = saved.key;
|
|
1616
|
+
if (state.resume?.key !== saved.key || state.resume.missing)
|
|
1617
|
+
set({ resume: { key: saved.key } });
|
|
1618
|
+
keepHere({ savedAt: draft.savedAt, key: saved.key });
|
|
1619
|
+
emit('draft', { key: saved.key, savedAt: draft.savedAt, later, saved });
|
|
1620
|
+
return saved;
|
|
1621
|
+
});
|
|
1097
1622
|
}
|
|
1098
1623
|
/** The values with a draft's over them: a field it left out — a file's bytes — as the record holds it, in a saved form placed here too. */
|
|
1099
1624
|
function withDraft(kept) {
|
|
@@ -1105,32 +1630,144 @@ function innerForm(options, within, place, outer) {
|
|
|
1105
1630
|
}
|
|
1106
1631
|
return values;
|
|
1107
1632
|
}
|
|
1633
|
+
/** A draft laid on the form: its answers as a starting point, its wizard step (or the nearest before it that applies), its text that does not read. */
|
|
1634
|
+
function applyDraft(draft) {
|
|
1635
|
+
found = null;
|
|
1636
|
+
writeValues(withDraft(draft.values ?? {}), { draft: null }, true);
|
|
1637
|
+
const known = (draft.skipped ?? []).filter((id) => steps.includes(id));
|
|
1638
|
+
// Text typed that did not read stays as typed, said as it was, where its field is still on the page.
|
|
1639
|
+
const unread = Object.fromEntries(Object.entries(draft.unread ?? {}).filter(([path]) => !!page.fields[path.split('.')[0]] && typeof draft.unread?.[path]?.text === 'string'));
|
|
1640
|
+
const patch = {
|
|
1641
|
+
...(steps.length ? { skipped: known, step: (0, drafts_1.resumeStep)(steps, form.steps(), draft.step) } : {}),
|
|
1642
|
+
...(Object.keys(unread).length ? { unread: { ...state.unread, ...unread } } : {}),
|
|
1643
|
+
// Its boxes were left with it: why each does not read is said at once, as it was where it was typed.
|
|
1644
|
+
...(Object.keys(unread).length ? { errors: { ...state.errors, ...Object.fromEntries(Object.entries(unread).map(([path, kept]) => [path, kept.message])) } } : {}),
|
|
1645
|
+
};
|
|
1646
|
+
if (Object.keys(patch).length)
|
|
1647
|
+
set(patch);
|
|
1648
|
+
}
|
|
1108
1649
|
function forgetDraft() {
|
|
1109
1650
|
if (draftTimer !== null)
|
|
1110
1651
|
scheduler.clearTimeout(draftTimer);
|
|
1111
1652
|
draftTimer = null;
|
|
1112
|
-
|
|
1653
|
+
found = null;
|
|
1654
|
+
draftEpoch++;
|
|
1655
|
+
serverKey = null;
|
|
1656
|
+
if (state.resume)
|
|
1657
|
+
set({ resume: null });
|
|
1658
|
+
options.drafts?.store.removeItem(storeKey());
|
|
1659
|
+
}
|
|
1660
|
+
/** A draft found: laid at once (`restore: "auto"`, or a draft continued from its key over nothing typed), else offered. */
|
|
1661
|
+
function offer(draft, auto = options.drafts?.restore === 'auto') {
|
|
1662
|
+
// What the person typed while it was asked for is theirs: the draft is offered, never laid over it.
|
|
1663
|
+
if (auto && !state.dirty.length && !Object.keys(state.unread).length)
|
|
1664
|
+
return applyDraft(draft);
|
|
1665
|
+
found = draft;
|
|
1666
|
+
set({ draft: { savedAt: draft.savedAt } });
|
|
1113
1667
|
}
|
|
1668
|
+
/**
|
|
1669
|
+
* The draft kept for the record shown, offered as it opens: the one the form was given to continue from (`resume`),
|
|
1670
|
+
* else the browser's store's — on the server by its key, or in the store. A draft asked of the server is laid only
|
|
1671
|
+
* on the record it was asked for.
|
|
1672
|
+
*/
|
|
1114
1673
|
function offerDraft() {
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1674
|
+
const source = options.dataSource;
|
|
1675
|
+
const given = resumeKey;
|
|
1676
|
+
resumeKey = undefined;
|
|
1677
|
+
const kept = given ? null : (0, drafts_1.draftRead)(options.drafts?.store.getItem(storeKey()) ?? null);
|
|
1678
|
+
if (!given && !kept)
|
|
1117
1679
|
return;
|
|
1118
|
-
|
|
1119
|
-
|
|
1680
|
+
if (kept && !('key' in kept))
|
|
1681
|
+
return offer(kept);
|
|
1682
|
+
const key = given ?? kept.key;
|
|
1683
|
+
if (!source?.loadDraft || !onServer())
|
|
1120
1684
|
return;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1685
|
+
const shown = shownNow();
|
|
1686
|
+
if (given)
|
|
1687
|
+
set({ status: 'loading' });
|
|
1688
|
+
return track(source.loadDraft({ pageId: page.id, key }).then((draft) => {
|
|
1689
|
+
if (!shown())
|
|
1690
|
+
return;
|
|
1691
|
+
if (given)
|
|
1692
|
+
set({ status: 'ready' });
|
|
1693
|
+
if (!draft) {
|
|
1694
|
+
// Sent, or gone: the form starts empty, and says so where it was asked to continue it.
|
|
1695
|
+
options.drafts?.store.removeItem(storeKey());
|
|
1696
|
+
if (given)
|
|
1697
|
+
set({ resume: { key, missing: true } });
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
serverKey = key;
|
|
1701
|
+
set({ resume: { key } });
|
|
1702
|
+
offer(draft, given ? true : undefined);
|
|
1703
|
+
}, (error) => {
|
|
1704
|
+
if (shown() && given)
|
|
1705
|
+
set({ status: 'error', error: error?.message ?? String(error) });
|
|
1706
|
+
}));
|
|
1707
|
+
}
|
|
1708
|
+
// ---- when the page takes answers, and each choice's places (the limits part: form-limits.ts) --------------------
|
|
1709
|
+
/** The limits made for this form from the limits part, once it came: none before, nor on a page with neither limits nor places. */
|
|
1710
|
+
let limitsKit = null;
|
|
1711
|
+
/** Whether the record shown was loaded once: its counts are asked then, its own choices not counted against it. */
|
|
1712
|
+
let loadedOnce = false;
|
|
1713
|
+
/** Whether the form keeps limits: a page with them or with places of its own, or a list's choices that came with places. */
|
|
1714
|
+
const keepsLimits = () => !outer && ((0, taking_1.hasLimits)(page) || Object.values(state.choices).some((choices) => choices.options?.some((option) => option.seats !== undefined)));
|
|
1715
|
+
/**
|
|
1716
|
+
* The limits, made from their part as it comes — at once where it has come — shown, and asked of the source as the
|
|
1717
|
+
* form opens (or once its record is loaded); null where the form keeps none, or the part could not come: the source's
|
|
1718
|
+
* submit decides then.
|
|
1719
|
+
*/
|
|
1720
|
+
function limitsCode() {
|
|
1721
|
+
if (limitsKit || !keepsLimits())
|
|
1722
|
+
return Promise.resolve(limitsKit);
|
|
1723
|
+
if (taking_1.formLimitsPart.loaded)
|
|
1724
|
+
return Promise.resolve(limitsMade(taking_1.formLimitsPart.loaded));
|
|
1725
|
+
return taking_1.formLimitsPart.load().then(limitsMade, () => null);
|
|
1726
|
+
}
|
|
1727
|
+
function limitsMade(part) {
|
|
1728
|
+
if (limitsKit || disposed)
|
|
1729
|
+
return limitsKit;
|
|
1730
|
+
const kit = part.formLimits({
|
|
1731
|
+
page,
|
|
1732
|
+
dataSource: options.dataSource,
|
|
1733
|
+
user: options.user,
|
|
1734
|
+
messages,
|
|
1735
|
+
scheduler,
|
|
1736
|
+
state: () => state,
|
|
1737
|
+
set,
|
|
1738
|
+
clock,
|
|
1739
|
+
started: () => started,
|
|
1740
|
+
shownNow,
|
|
1741
|
+
generation: () => generation,
|
|
1742
|
+
replaced: () => named(messages.recordReplacedNoun, messages.recordReplaced),
|
|
1743
|
+
listened: () => listeners.size > 0,
|
|
1744
|
+
disposed: () => disposed,
|
|
1745
|
+
refuse(name, message) {
|
|
1746
|
+
// Said until the field changes from what it holds, as a refused save's words are.
|
|
1747
|
+
serverErrors[name] = { message, value: JSON.stringify(state.values[name] ?? null) };
|
|
1748
|
+
set({ errors: { ...state.errors, [name]: message } });
|
|
1749
|
+
},
|
|
1750
|
+
});
|
|
1751
|
+
limitsKit = kit;
|
|
1752
|
+
kit.show();
|
|
1753
|
+
if (kit.asks() && (state.recordId == null || loadedOnce))
|
|
1754
|
+
void track(kit.ask());
|
|
1755
|
+
return kit;
|
|
1756
|
+
}
|
|
1757
|
+
/** Ask the source's `limits` again and show what it tells; without the limits part, nothing to ask. */
|
|
1758
|
+
const askLimits = () => (limitsKit ? limitsKit.ask() : Promise.resolve());
|
|
1759
|
+
/** Whether the form asks the source for counts. */
|
|
1760
|
+
const asksLimits = () => !!limitsKit?.asks();
|
|
1761
|
+
/**
|
|
1762
|
+
* Whether the page takes these answers now, asked of the source again just before they go — the limits part waited
|
|
1763
|
+
* for where the form keeps limits: why not, as a save's problem, or null.
|
|
1764
|
+
*/
|
|
1765
|
+
async function stillTaking(asked) {
|
|
1766
|
+
const kit = await limitsCode();
|
|
1767
|
+
return kit ? kit.stillTaking(asked) : null;
|
|
1133
1768
|
}
|
|
1769
|
+
/** A choice of a full option, picked by the person, refused under its field: the field keeps what it held. */
|
|
1770
|
+
const refusedFull = (name, value) => !!limitsKit?.refusedFull(name, value);
|
|
1134
1771
|
function scheduleAutosave() {
|
|
1135
1772
|
const autosave = options.autosave;
|
|
1136
1773
|
// A record the person may not change is never saved unasked: it would only be refused.
|
|
@@ -1271,6 +1908,20 @@ function innerForm(options, within, place, outer) {
|
|
|
1271
1908
|
await waiting;
|
|
1272
1909
|
if (asked !== generation)
|
|
1273
1910
|
return elsewhere();
|
|
1911
|
+
// Options by condition waited for where they have not come: what is saved never holds one they would take out.
|
|
1912
|
+
if (!optionKit && (0, choice_options_1.hasOptionRules)(page)) {
|
|
1913
|
+
await optionsCode();
|
|
1914
|
+
if (asked !== generation)
|
|
1915
|
+
return elsewhere();
|
|
1916
|
+
}
|
|
1917
|
+
// Values the app's functions are asked for are waited for — those that failed asked again — so what is saved is worked out.
|
|
1918
|
+
if (asking.size || Object.values(state.asked).some((found) => found.failed !== undefined)) {
|
|
1919
|
+
set({ status: 'saving', error: null, saveProblem: null });
|
|
1920
|
+
await askAgain();
|
|
1921
|
+
if (asked !== generation)
|
|
1922
|
+
return elsewhere();
|
|
1923
|
+
set({ status: 'ready' });
|
|
1924
|
+
}
|
|
1274
1925
|
// Files still on their way are waited for first — a saved form's placed here too: the state says how many, so the page can say why it waits.
|
|
1275
1926
|
if (filesGoing()) {
|
|
1276
1927
|
set({ status: 'saving', error: null, saveProblem: null });
|
|
@@ -1292,6 +1943,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1292
1943
|
return { reason: 'save', ...(before.message !== undefined ? { message: before.message } : {}) };
|
|
1293
1944
|
}
|
|
1294
1945
|
}
|
|
1946
|
+
// Whether the page still takes answers — its dates, its counts asked again — and each choice held still has room.
|
|
1947
|
+
const refused = await stillTaking(asked);
|
|
1948
|
+
if (refused)
|
|
1949
|
+
return refused;
|
|
1295
1950
|
// A file that did not reach the store stops it, under its field, with the form's own checks.
|
|
1296
1951
|
const errors = { ...collectErrors(), ...fileProblems() };
|
|
1297
1952
|
if (Object.keys(errors).length) {
|
|
@@ -1301,6 +1956,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1301
1956
|
const source = options.dataSource;
|
|
1302
1957
|
serverErrors = {};
|
|
1303
1958
|
set({ status: 'saving', errors: {}, error: null, saveProblem: null, conflict: null });
|
|
1959
|
+
// A draft being kept on the server goes first, so the send names the draft it ends; none is kept after it.
|
|
1960
|
+
if (draftTimer !== null)
|
|
1961
|
+
scheduler.clearTimeout(draftTimer);
|
|
1962
|
+
draftTimer = null;
|
|
1963
|
+
if (keeping)
|
|
1964
|
+
await draftGoing;
|
|
1965
|
+
const draft = serverKey ? { draft: serverKey } : {};
|
|
1304
1966
|
/** The generation the record was in when this save went to the source. */
|
|
1305
1967
|
let at = generation;
|
|
1306
1968
|
try {
|
|
@@ -1319,8 +1981,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1319
1981
|
const sent = shownValues();
|
|
1320
1982
|
const before = state.values;
|
|
1321
1983
|
at = generation;
|
|
1984
|
+
const resetAt = resets;
|
|
1322
1985
|
const seen = botReport();
|
|
1323
|
-
|
|
1986
|
+
// What the source answered: the id it gave the response, and the values it added — what an ending reads.
|
|
1987
|
+
const answer = (await source.submit({ pageId: page.id, values: sent, ...(seen ? { bot: seen } : {}), ...draft })) ?? {};
|
|
1324
1988
|
// The captcha's token is spent once sent: the next send waits for a new one.
|
|
1325
1989
|
if (seen?.captcha)
|
|
1326
1990
|
bot.captcha = null;
|
|
@@ -1330,8 +1994,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1330
1994
|
forgetDraft();
|
|
1331
1995
|
// What it sent is what an answer changed since is told against: still to send.
|
|
1332
1996
|
sentAs = before;
|
|
1333
|
-
|
|
1997
|
+
const added = answer.values && typeof answer.values === 'object' ? (0, values_1.structuredCopy)(answer.values) : {};
|
|
1998
|
+
// Started again meanwhile (`reset`): a new response is on screen, and no ending is the old one's.
|
|
1999
|
+
set({ status: 'saved', unsent: unsentNow(), ...(resets === resetAt ? { sent: { id: answer.id ?? null, values: { ...(0, values_1.structuredCopy)(sent), ...added } } } : {}) });
|
|
1334
2000
|
emit('send', { values: sent, ...(seen ? { bot: seen } : {}) });
|
|
2001
|
+
// Counted again: one response fewer to take, this person's sent.
|
|
2002
|
+
if (asksLimits())
|
|
2003
|
+
void track(askLimits());
|
|
1335
2004
|
void track(moment('on.afterSave', page.on?.afterSave, chain, false));
|
|
1336
2005
|
return null;
|
|
1337
2006
|
}
|
|
@@ -1349,6 +2018,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1349
2018
|
values: sentValues(),
|
|
1350
2019
|
// The version it was opened at, for a source that refuses a save over someone else's.
|
|
1351
2020
|
...(state.recordId != null && state.version ? { version: state.version } : {}),
|
|
2021
|
+
...draft,
|
|
1352
2022
|
});
|
|
1353
2023
|
// Answered for a record no longer shown — another opened, or this one loaded again: the source has it, and the
|
|
1354
2024
|
// form lays nothing of it over the record it shows now.
|
|
@@ -1624,7 +2294,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1624
2294
|
return;
|
|
1625
2295
|
// Lines load in their tables' orders; where a person moves them after, they stay.
|
|
1626
2296
|
baseline = syncParts(startFrom(ordered({ ...(0, values_1.initialValues)(fields, today()), ...loaded })), 'start');
|
|
1627
|
-
set({ status: 'ready', values: (0, values_1.structuredCopy)(baseline), dirty: [], errors: {}, warnings: withValues(baseline, collectWarnings), version: version ?? null, can: can ?? null, unread: {} });
|
|
2297
|
+
set({ status: 'ready', values: (0, values_1.structuredCopy)(baseline), dirty: [], errors: {}, warnings: withValues(baseline, collectWarnings), version: version ?? null, can: can ?? null, unread: {}, takenOut: {} });
|
|
2298
|
+
// Each choice's places, for this record: its own choices are its own.
|
|
2299
|
+
loadedOnce = true;
|
|
2300
|
+
if (asksLimits())
|
|
2301
|
+
void track(askLimits());
|
|
1628
2302
|
if (again)
|
|
1629
2303
|
return;
|
|
1630
2304
|
offerDraft();
|
|
@@ -1653,7 +2327,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1653
2327
|
told = null;
|
|
1654
2328
|
showing = false;
|
|
1655
2329
|
clearFiles();
|
|
1656
|
-
|
|
2330
|
+
// The draft kept for the record shown before is that one's.
|
|
2331
|
+
found = null;
|
|
2332
|
+
serverKey = null;
|
|
2333
|
+
draftEpoch++;
|
|
2334
|
+
const fresh = { dirty: [], errors: {}, warning: null, warningField: null, error: null, draft: null, step: steps[0] ?? null, skipped: [], saveProblem: null, version: null, can: null, conflict: null, setAside: null, unsent: [], unread: {}, takenOut: {}, resume: null };
|
|
1657
2335
|
if (id != null) {
|
|
1658
2336
|
set({ ...fresh, recordId: id, status: 'idle' });
|
|
1659
2337
|
await load();
|
|
@@ -1663,6 +2341,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1663
2341
|
baseline = startFrom(startingValues(true, false));
|
|
1664
2342
|
const values = syncParts((0, values_1.structuredCopy)(baseline), 'start');
|
|
1665
2343
|
set({ ...fresh, recordId: null, status: 'ready', values, warnings: withValues(values, collectWarnings) });
|
|
2344
|
+
if (asksLimits())
|
|
2345
|
+
void track(askLimits());
|
|
1666
2346
|
offerDraft();
|
|
1667
2347
|
await opening();
|
|
1668
2348
|
return true;
|
|
@@ -2023,10 +2703,18 @@ function innerForm(options, within, place, outer) {
|
|
|
2023
2703
|
}
|
|
2024
2704
|
const form = {
|
|
2025
2705
|
page,
|
|
2706
|
+
...(table ? { functions: table } : {}),
|
|
2026
2707
|
getState: () => state,
|
|
2027
2708
|
subscribe(listener) {
|
|
2028
2709
|
listeners.add(listener);
|
|
2029
|
-
|
|
2710
|
+
// Listened to: the page opening or closing is said as it comes.
|
|
2711
|
+
if (listeners.size === 1)
|
|
2712
|
+
limitsKit?.heard(true);
|
|
2713
|
+
return () => {
|
|
2714
|
+
listeners.delete(listener);
|
|
2715
|
+
if (!listeners.size)
|
|
2716
|
+
limitsKit?.heard(false);
|
|
2717
|
+
};
|
|
2030
2718
|
},
|
|
2031
2719
|
load() {
|
|
2032
2720
|
const go = letGo();
|
|
@@ -2034,6 +2722,9 @@ function innerForm(options, within, place, outer) {
|
|
|
2034
2722
|
},
|
|
2035
2723
|
setValue(name, value, how) {
|
|
2036
2724
|
fieldDef(name);
|
|
2725
|
+
// A choice whose places are all taken is shown, and refused when the person picks it: said under its field.
|
|
2726
|
+
if ((how?.by ?? 'person') === 'person' && refusedFull(name, value))
|
|
2727
|
+
return;
|
|
2037
2728
|
writeValues({ ...state.values, [name]: (0, values_1.structuredCopy)(value) });
|
|
2038
2729
|
afterEdit(name);
|
|
2039
2730
|
changed(name, how?.by ?? 'person');
|
|
@@ -2059,13 +2750,38 @@ function innerForm(options, within, place, outer) {
|
|
|
2059
2750
|
// Nothing of the record changed; what shows of it may have.
|
|
2060
2751
|
set({});
|
|
2061
2752
|
},
|
|
2062
|
-
fieldReadonly: (name) => lockedField(fieldDef(name)) || state.can?.edit === false || state.can?.readonlyFields?.includes(name) === true,
|
|
2063
|
-
loadChoices,
|
|
2753
|
+
fieldReadonly: (name) => lockedField(fieldDef(name)) || state.can?.edit === false || state.can?.readonlyFields?.includes(name) === true || signedNow().has(name),
|
|
2754
|
+
loadChoices: (field) => loadChoices(field),
|
|
2064
2755
|
loadDefinitions,
|
|
2065
2756
|
addDefinition,
|
|
2757
|
+
lineChoices(field, key, subfield, values = {}) {
|
|
2758
|
+
const one = listedAt(field, subfield);
|
|
2759
|
+
const line = key === null ? one && { values: newLineValues(field) } : lineAt(field, key);
|
|
2760
|
+
if (!one || !line)
|
|
2761
|
+
throw new Error(`"${field}.${key}.${subfield}" is no line's choice from the app's lists`);
|
|
2762
|
+
return track(askLine(one, { ...line.values, ...values }));
|
|
2763
|
+
},
|
|
2764
|
+
offered(name) {
|
|
2765
|
+
const cell = (0, line_choices_1.cellOf)(name);
|
|
2766
|
+
if (!cell)
|
|
2767
|
+
return page.fields[name]?.type === 'selection' ? (optionKit ? optionKit.offered(name, state.values, reading()) : everyOption(name)) : null;
|
|
2768
|
+
const lines = page.fields[cell.table];
|
|
2769
|
+
const line = lineAt(cell.table, cell.key);
|
|
2770
|
+
return lines?.type === 'one2many' && lines.fields[cell.column]?.type === 'selection' && line ? lineOffered(cell.table, line, cell.column, reading().context) : null;
|
|
2771
|
+
},
|
|
2772
|
+
offersChange(name) {
|
|
2773
|
+
const cell = (0, line_choices_1.cellOf)(name);
|
|
2774
|
+
const lines = cell ? page.fields[cell.table] : undefined;
|
|
2775
|
+
return (0, choice_options_1.changesOptions)(cell ? (lines?.type === 'one2many' ? lines.fields[cell.column] : undefined) : page.fields[name]);
|
|
2776
|
+
},
|
|
2066
2777
|
waitsFor(name) {
|
|
2778
|
+
// What it is worked out from first: an answer that does not read, or that the form refuses, is never asked about.
|
|
2067
2779
|
const from = workedOut.get(name);
|
|
2068
|
-
|
|
2780
|
+
const waits = from ? (0, waits_1.waitingOn)(page, from, state.values, state.unread, refusedAt) : null;
|
|
2781
|
+
if (waits)
|
|
2782
|
+
return waits;
|
|
2783
|
+
const found = state.asked[name];
|
|
2784
|
+
return found ? { field: name, asking: true, ...(found.failed !== undefined ? { failed: found.failed } : {}) } : null;
|
|
2069
2785
|
},
|
|
2070
2786
|
problem(name) {
|
|
2071
2787
|
fieldDef(name);
|
|
@@ -2145,6 +2861,9 @@ function innerForm(options, within, place, outer) {
|
|
|
2145
2861
|
save: () => track(save()),
|
|
2146
2862
|
canUpload: () => typeof options.dataSource?.upload === 'function',
|
|
2147
2863
|
today,
|
|
2864
|
+
now: () => options.now?.() ?? new Date(),
|
|
2865
|
+
user: () => options.user ?? null,
|
|
2866
|
+
shownFields: () => [...new Set([...visibleFieldNodes(), ...visibleFieldNodes(undefined, 'form')].map((node) => answerOf(node)).filter((name) => !!name))],
|
|
2148
2867
|
recordReads: (id, values, fields) => (0, env_1.recordContext)(values, fields ?? page.fields, { recordId: id, user: options.user, editing: false, context: options.context }),
|
|
2149
2868
|
canFindSlots: () => typeof options.dataSource?.slots === 'function',
|
|
2150
2869
|
findSlots(field, ask) {
|
|
@@ -2179,6 +2898,22 @@ function innerForm(options, within, place, outer) {
|
|
|
2179
2898
|
const others = (matches ?? []).filter((match) => match && String(match.id) !== String(recordId));
|
|
2180
2899
|
return others.every((match) => typeof match.score === 'number') ? others.sort((a, b) => b.score - a.score) : others;
|
|
2181
2900
|
},
|
|
2901
|
+
keepsNotDuplicates: () => !!(page.data.kind === 'record' && options.dataSource?.notDuplicates && options.dataSource.markNotDuplicate),
|
|
2902
|
+
async notDuplicates() {
|
|
2903
|
+
const [source, model] = recordSource('notDuplicates');
|
|
2904
|
+
const recordId = state.recordId;
|
|
2905
|
+
if (recordId === null)
|
|
2906
|
+
return [];
|
|
2907
|
+
const ids = await source.notDuplicates({ model, recordId });
|
|
2908
|
+
return Array.isArray(ids) ? ids.filter((id) => (typeof id === 'string' || typeof id === 'number') && String(id) !== String(recordId)) : [];
|
|
2909
|
+
},
|
|
2910
|
+
async markNotDuplicate(matchId) {
|
|
2911
|
+
const [source, model] = recordSource('markNotDuplicate');
|
|
2912
|
+
const recordId = state.recordId;
|
|
2913
|
+
if (recordId === null)
|
|
2914
|
+
throw new Error('A new record has no id to keep it by');
|
|
2915
|
+
await source.markNotDuplicate({ model, recordId, matchId });
|
|
2916
|
+
},
|
|
2182
2917
|
canMerge: () => !!(page.data.kind === 'record' && options.dataSource?.merge && options.dataSource.load),
|
|
2183
2918
|
async readRecord(id) {
|
|
2184
2919
|
const [source, model] = recordSource('load');
|
|
@@ -2219,7 +2954,19 @@ function innerForm(options, within, place, outer) {
|
|
|
2219
2954
|
// Started again: a new response, timed from now.
|
|
2220
2955
|
bot = { ...bot, honeypot: false, since: clock() };
|
|
2221
2956
|
const values = syncParts((0, values_1.structuredCopy)(baseline), 'start');
|
|
2222
|
-
set({ values, dirty: [], errors: {}, warnings: withValues(values, collectWarnings), warning: null, warningField: null, draft: null, step: steps[0] ?? null, skipped: [], saveProblem: null, conflict: null, unsent: [], unread: {} });
|
|
2957
|
+
set({ values, dirty: [], errors: {}, warnings: withValues(values, collectWarnings), warning: null, warningField: null, draft: null, step: steps[0] ?? null, skipped: [], saveProblem: null, conflict: null, unsent: [], unread: {}, takenOut: {}, sent: null });
|
|
2958
|
+
// Started again: whether the page takes another response, and each choice's places, asked again.
|
|
2959
|
+
if (asksLimits())
|
|
2960
|
+
void track(askLimits());
|
|
2961
|
+
},
|
|
2962
|
+
words: (text) => wordsNow(text),
|
|
2963
|
+
ending: endingNow,
|
|
2964
|
+
endingWords(text) {
|
|
2965
|
+
const sent = state.sent;
|
|
2966
|
+
if (!sent)
|
|
2967
|
+
return wordsNow(text);
|
|
2968
|
+
// The response's id where the page has no field of that name: "Your booking is {id}".
|
|
2969
|
+
return wordsNow(text, sent.values, (name) => (name === 'id' && !page.fields['id'] ? String(sent.id ?? '') : undefined));
|
|
2223
2970
|
},
|
|
2224
2971
|
changes,
|
|
2225
2972
|
addLine: (field, values = {}) => addLine(field, values, 'person'),
|
|
@@ -2480,16 +3227,27 @@ function innerForm(options, within, place, outer) {
|
|
|
2480
3227
|
return () => void heard.delete(listener);
|
|
2481
3228
|
},
|
|
2482
3229
|
restoreDraft() {
|
|
2483
|
-
const
|
|
2484
|
-
if (
|
|
2485
|
-
|
|
2486
|
-
const draft = JSON.parse(raw);
|
|
2487
|
-
writeValues(withDraft(draft.values), { draft: null }, true);
|
|
3230
|
+
const kept = found ?? (0, drafts_1.draftRead)(options.drafts?.store.getItem(storeKey()) ?? null);
|
|
3231
|
+
if (kept && 'values' in kept)
|
|
3232
|
+
applyDraft(kept);
|
|
2488
3233
|
},
|
|
2489
3234
|
discardDraft() {
|
|
2490
3235
|
forgetDraft();
|
|
2491
3236
|
set({ draft: null });
|
|
2492
3237
|
},
|
|
3238
|
+
canContinueLater: () => page.continueLater === true && onServer() && page.layout.type !== 'list',
|
|
3239
|
+
async continueLater() {
|
|
3240
|
+
if (!form.canContinueLater())
|
|
3241
|
+
throw new Error('No continueLater, or no saveDraft and loadDraft');
|
|
3242
|
+
// What is typed is read first, as a save reads it: text that does not read is kept as typed, said again as it opens.
|
|
3243
|
+
await (readTyped() ?? Promise.resolve());
|
|
3244
|
+
if (draftTimer !== null)
|
|
3245
|
+
scheduler.clearTimeout(draftTimer);
|
|
3246
|
+
draftTimer = null;
|
|
3247
|
+
const saved = await track(keepDraft(true));
|
|
3248
|
+
return saved;
|
|
3249
|
+
},
|
|
3250
|
+
refreshLimits: () => askLimits(),
|
|
2493
3251
|
async reloadTheirs() {
|
|
2494
3252
|
// A save on its way first: its answer is not dropped by the load.
|
|
2495
3253
|
const asked = shownNow();
|
|
@@ -2616,6 +3374,8 @@ function innerForm(options, within, place, outer) {
|
|
|
2616
3374
|
scheduler,
|
|
2617
3375
|
messages,
|
|
2618
3376
|
now: options.now,
|
|
3377
|
+
// Its words that show its answers read them as this form's do.
|
|
3378
|
+
...(options.valueWords ? { valueWords: options.valueWords } : {}),
|
|
2619
3379
|
}, chain,
|
|
2620
3380
|
// Its files are the record's: stored for it, under the part's name.
|
|
2621
3381
|
{ model: where.model, recordId: where.recordId, under: `${where.under}${part.name}.` },
|
|
@@ -2659,6 +3419,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2659
3419
|
scheduler.clearTimeout(draftTimer);
|
|
2660
3420
|
if (autosaveTimer !== null)
|
|
2661
3421
|
scheduler.clearTimeout(autosaveTimer);
|
|
3422
|
+
limitsKit?.dispose();
|
|
2662
3423
|
listeners.clear();
|
|
2663
3424
|
events.clear();
|
|
2664
3425
|
for (const part of attached.values())
|
|
@@ -2667,11 +3428,16 @@ function innerForm(options, within, place, outer) {
|
|
|
2667
3428
|
};
|
|
2668
3429
|
// A record's starting values may already break a warning rule; nothing listens yet.
|
|
2669
3430
|
state = { ...state, warnings: collectWarnings() };
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3431
|
+
// When the page takes answers — at once where the limits part has come, else as soon as it does — its counts and each
|
|
3432
|
+
// choice's places as the source tells them: a record's own once it is loaded, its own choices not counted against it.
|
|
3433
|
+
if (keepsLimits())
|
|
3434
|
+
void track(limitsCode());
|
|
3435
|
+
// A new form has its values now — a draft continued from its key once it is laid — and opens once whoever made it
|
|
3436
|
+
// has had the chance to listen.
|
|
3437
|
+
if (state.recordId == null) {
|
|
3438
|
+
const continued = offerDraft();
|
|
3439
|
+
void track(Promise.resolve(continued).then(opening));
|
|
3440
|
+
}
|
|
2675
3441
|
return {
|
|
2676
3442
|
form,
|
|
2677
3443
|
take(values, how) {
|
|
@@ -2681,7 +3447,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2681
3447
|
filed.clear();
|
|
2682
3448
|
baseline = startFrom({ ...(0, values_1.initialValues)(page.fields, today()), ...(0, values_1.structuredCopy)(values) });
|
|
2683
3449
|
const start = syncParts((0, values_1.structuredCopy)(baseline), 'start');
|
|
2684
|
-
set({ values: start, dirty: [], errors: {}, warnings: withValues(start, collectWarnings), warning: null, warningField: null, unread: {} });
|
|
3450
|
+
set({ values: start, dirty: [], errors: {}, warnings: withValues(start, collectWarnings), warning: null, warningField: null, unread: {}, takenOut: {} });
|
|
2685
3451
|
},
|
|
2686
3452
|
errorsNow: () => collectErrors(),
|
|
2687
3453
|
typing: typingNow,
|
|
@@ -2705,6 +3471,8 @@ function innerForm(options, within, place, outer) {
|
|
|
2705
3471
|
const sameHow = (a, b) => !!a.changes === !!b.changes && !!a.discard === !!b.discard;
|
|
2706
3472
|
/** Two lists of names the same, in the same order. */
|
|
2707
3473
|
const sameList = (a, b) => a.length === b.length && a.every((name, i) => name === b[i]);
|
|
3474
|
+
/** Whether what waits for an app's answer is the same as before: the same paths, each waiting or failed for the same reason. */
|
|
3475
|
+
const sameAsked = (shown, now) => Object.keys(shown).length === now.size && [...now].every(([path, found]) => shown[path] !== undefined && shown[path].failed === found.failed);
|
|
2708
3476
|
/** A file on its way in a saved form placed in another, as that one names it: its id and its field under the part's name. */
|
|
2709
3477
|
function underPart(name, upload) {
|
|
2710
3478
|
return { ...upload, id: `${name}/${upload.id}`, ...(upload.line ? { line: { ...upload.line, field: `${name}.${upload.line.field}` } } : { field: `${name}.${upload.field}` }) };
|
|
@@ -2815,7 +3583,7 @@ function linkOps(before, now) {
|
|
|
2815
3583
|
...before.filter((r) => !has.has(r.id)).map((r) => ({ op: 'unlink', id: r.id })),
|
|
2816
3584
|
];
|
|
2817
3585
|
}
|
|
2818
|
-
function indexLayout(root) {
|
|
3586
|
+
function indexLayout(root, functions) {
|
|
2819
3587
|
const index = new Map();
|
|
2820
3588
|
const add = (source, parent, kind, modifiers) => {
|
|
2821
3589
|
const roles = source.roles;
|
|
@@ -2827,10 +3595,10 @@ function indexLayout(root) {
|
|
|
2827
3595
|
kind,
|
|
2828
3596
|
field: modifiers.field,
|
|
2829
3597
|
...(roles?.length ? { roles } : {}),
|
|
2830
|
-
invisible: (0, modifier_1.compileModifier)(modifiers.invisible),
|
|
2831
|
-
readonly: (0, modifier_1.compileModifier)(modifiers.readonly),
|
|
2832
|
-
required: (0, modifier_1.compileModifier)(modifiers.required),
|
|
2833
|
-
...(pressed !== undefined ? { pressed: (0, modifier_1.compileModifier)(pressed) } : {}),
|
|
3598
|
+
invisible: (0, modifier_1.compileModifier)(modifiers.invisible, functions),
|
|
3599
|
+
readonly: (0, modifier_1.compileModifier)(modifiers.readonly, functions),
|
|
3600
|
+
required: (0, modifier_1.compileModifier)(modifiers.required, functions),
|
|
3601
|
+
...(pressed !== undefined ? { pressed: (0, modifier_1.compileModifier)(pressed, functions) } : {}),
|
|
2834
3602
|
...(does.length ? { does } : {}),
|
|
2835
3603
|
source,
|
|
2836
3604
|
});
|