@fieldia/core 0.26.1 → 0.27.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 +307 -3
- package/package.json +1 -1
- package/schema/page.schema.json +227 -23
- package/src/index.d.mts +8 -2
- package/src/index.d.ts +8 -2
- package/src/index.js +22 -1
- package/src/index.js.map +1 -1
- package/src/index.mjs +9 -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 +241 -188
- package/src/lib/format/layout.d.ts +241 -188
- package/src/lib/format/layout.js +33 -28
- package/src/lib/format/layout.js.map +1 -1
- package/src/lib/format/layout.mjs +13 -3
- 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 +147 -71
- package/src/lib/format/page.d.ts +147 -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 +6 -3
- package/src/lib/format/upgrade.d.ts +6 -3
- package/src/lib/format/upgrade.js +6 -3
- package/src/lib/format/upgrade.js.map +1 -1
- package/src/lib/format/upgrade.mjs +6 -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/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/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 +81 -0
- package/src/lib/record/form-limits.d.ts +81 -0
- package/src/lib/record/form-limits.js +231 -0
- package/src/lib/record/form-limits.js.map +1 -0
- package/src/lib/record/form-limits.mjs +224 -0
- package/src/lib/record/form-limits.mjs.map +1 -0
- package/src/lib/record/form.d.mts +173 -4
- package/src/lib/record/form.d.ts +173 -4
- package/src/lib/record/form.js +792 -88
- package/src/lib/record/form.js.map +1 -1
- package/src/lib/record/form.mjs +792 -88
- 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/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 +10 -0
- package/src/lib/record/locales/ar.js.map +1 -1
- package/src/lib/record/locales/ar.mjs +10 -0
- package/src/lib/record/locales/ar.mjs.map +1 -1
- package/src/lib/record/locales/de.js +10 -0
- package/src/lib/record/locales/de.js.map +1 -1
- package/src/lib/record/locales/de.mjs +10 -0
- package/src/lib/record/locales/de.mjs.map +1 -1
- package/src/lib/record/locales/fr.js +10 -0
- package/src/lib/record/locales/fr.js.map +1 -1
- package/src/lib/record/locales/fr.mjs +10 -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 +17 -0
- package/src/lib/record/messages.d.ts +17 -0
- package/src/lib/record/messages.js +10 -0
- package/src/lib/record/messages.js.map +1 -1
- package/src/lib/record/messages.mjs +10 -0
- package/src/lib/record/messages.mjs.map +1 -1
- package/src/lib/record/option-rules.d.mts +50 -0
- package/src/lib/record/option-rules.d.ts +50 -0
- package/src/lib/record/option-rules.js +137 -0
- package/src/lib/record/option-rules.js.map +1 -0
- package/src/lib/record/option-rules.mjs +134 -0
- package/src/lib/record/option-rules.mjs.map +1 -0
- 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 +6 -1
- package/src/lib/record/waits.d.ts +6 -1
- package/src/lib/record/waits.js +2 -2
- package/src/lib/record/waits.js.map +1 -1
- package/src/lib/record/waits.mjs +2 -2
- package/src/lib/record/waits.mjs.map +1 -1
- package/src/runtime.d.mts +16 -5
- package/src/runtime.d.ts +16 -5
- package/src/runtime.js +41 -3
- package/src/runtime.js.map +1 -1
- package/src/runtime.mjs +13 -0
- package/src/runtime.mjs.map +1 -1
package/src/lib/record/form.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { changesOptions, hasOptionRules, optionRulesPart, optionsField, optionsList } from '../format/choice-options.mjs';
|
|
2
|
+
import { fillValues } from '../format/words.mjs';
|
|
1
3
|
import { upgradePage } from '../format/upgrade.mjs';
|
|
2
4
|
import { zonedPage } from '../format/zoned.mjs';
|
|
3
5
|
import { compileModifier } from '../expression/modifier.mjs';
|
|
6
|
+
import { callsPartFunctions, functionsPart } from '../expression/functions.mjs';
|
|
7
|
+
import { functionTable } from '../expression/app-functions.mjs';
|
|
4
8
|
import { dayIn, ownZone } from './zones.mjs';
|
|
5
9
|
import { checkProperties, checkValue } from './check.mjs';
|
|
6
10
|
import { checkInput } from './inputs.mjs';
|
|
@@ -14,10 +18,14 @@ import { rolesAllow } from './roles.mjs';
|
|
|
14
18
|
import { compileFieldTone, compileTable } from './cells.mjs';
|
|
15
19
|
import { fillMessage, MESSAGES } from './messages.mjs';
|
|
16
20
|
import { recordSentence } from './record-words.mjs';
|
|
21
|
+
import { askKey, cellOf, listedColumns, notOffered } from './line-choices.mjs';
|
|
17
22
|
import { loadedRecordOf, saveProblemOf, } from './data-source.mjs';
|
|
23
|
+
import { formLimitsPart, hasLimits } from './taking.mjs';
|
|
24
|
+
import { draftRead, resumeStep } from './drafts.mjs';
|
|
18
25
|
import { waitingOn, workedOutFrom } from './waits.mjs';
|
|
19
26
|
import { createUploads, fileBytesIn, uploadGoing, withoutFileBytes } from './uploads.mjs';
|
|
20
27
|
import { hostScheduler } from './scheduler.mjs';
|
|
28
|
+
import { signedBy } from './signed.mjs';
|
|
21
29
|
import { createMoments, createRunner } from './run.mjs';
|
|
22
30
|
import { emptyValue, initialValues, isEmpty, lineKind, structuredCopy, } from './values.mjs';
|
|
23
31
|
/**
|
|
@@ -80,13 +88,48 @@ function innerForm(options, within, place, outer) {
|
|
|
80
88
|
const messages = page.lowerLabels ? { ...(options.messages ?? MESSAGES.en), lowerLabels: true } : (options.messages ?? MESSAGES.en);
|
|
81
89
|
/** One of the form's sentences in the words the page names its records by (`records`), else as the language says it plainly. */
|
|
82
90
|
const named = (noun, plain) => recordSentence(page.records, noun, plain, messages.locale);
|
|
83
|
-
|
|
91
|
+
/** The app's functions the page's expressions call: a name none may call is refused now. */
|
|
92
|
+
const table = functionTable(options.functions);
|
|
93
|
+
/**
|
|
94
|
+
* The answers of the app's functions that ask, for the record shown (`ExpressionEnv.answer`), by function and
|
|
95
|
+
* values: each asked once, `asking` until it comes. Another record shown, they start again: an answer for the last
|
|
96
|
+
* one is laid on none.
|
|
97
|
+
*/
|
|
98
|
+
let answers = new Map();
|
|
99
|
+
/** Those on their way: a save waits for them. */
|
|
100
|
+
const asking = new Set();
|
|
101
|
+
/** Everything the form waits on (`track`): there before the form's first values are worked out, which may ask. */
|
|
102
|
+
const pending = new Set();
|
|
103
|
+
function answer(key, asked) {
|
|
104
|
+
const kept = answers.get(key);
|
|
105
|
+
if (kept !== undefined || !asked)
|
|
106
|
+
return kept;
|
|
107
|
+
const map = answers;
|
|
108
|
+
map.set(key, 'asking');
|
|
109
|
+
const land = (found) => {
|
|
110
|
+
asking.delete(going);
|
|
111
|
+
if (map !== answers || disposed)
|
|
112
|
+
return;
|
|
113
|
+
map.set(key, found);
|
|
114
|
+
answered();
|
|
115
|
+
};
|
|
116
|
+
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) }));
|
|
117
|
+
asking.add(going);
|
|
118
|
+
track(going);
|
|
119
|
+
return 'asking';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* What every expression of the form is handed of the app's — and, on a page calling Fieldia's functions whose bodies
|
|
123
|
+
* come in the functions part before it has come, the answers they wait for it with, as an app's function that asks.
|
|
124
|
+
*/
|
|
125
|
+
const app = table ? { functions: table, answer } : !functionsPart.loaded && callsPartFunctions(page) ? { answer } : {};
|
|
126
|
+
const index = indexLayout(page.layout, table);
|
|
84
127
|
/** Each field node's answer rules, read once. */
|
|
85
128
|
const nodeRules = new Map();
|
|
86
129
|
for (const node of index.values()) {
|
|
87
130
|
const rules = node.source.validate;
|
|
88
131
|
if (node.kind === 'field' && rules)
|
|
89
|
-
nodeRules.set(node.id, compileRules(rules));
|
|
132
|
+
nodeRules.set(node.id, compileRules(rules, table));
|
|
90
133
|
}
|
|
91
134
|
/** Tables of lines with rules of their own, by their field node: each read once. */
|
|
92
135
|
const tables = new Map();
|
|
@@ -95,14 +138,14 @@ function innerForm(options, within, place, outer) {
|
|
|
95
138
|
if (node.kind !== 'field' || page.fields[source.field]?.type !== 'one2many')
|
|
96
139
|
continue;
|
|
97
140
|
if (source.cells || source.rowTones || source.rowBold !== undefined || source.rowButtons || source.lineDelete !== undefined)
|
|
98
|
-
tables.set(node.id, { field: source.field, compiled: compileTable(source) });
|
|
141
|
+
tables.set(node.id, { field: source.field, compiled: compileTable(source, table) });
|
|
99
142
|
}
|
|
100
143
|
/** Field nodes whose value takes a tone or bold, by id: each read once. */
|
|
101
144
|
const fieldTones = new Map();
|
|
102
145
|
for (const node of index.values()) {
|
|
103
146
|
const source = node.source;
|
|
104
147
|
if (node.kind === 'field' && (source.tones || source.bold !== undefined))
|
|
105
|
-
fieldTones.set(node.id, compileFieldTone(source));
|
|
148
|
+
fieldTones.set(node.id, compileFieldTone(source, table));
|
|
106
149
|
}
|
|
107
150
|
/** The tables whose lines load in an order of their own fields, by the field: the first node to give one. */
|
|
108
151
|
const lineOrders = new Map();
|
|
@@ -130,7 +173,7 @@ function innerForm(options, within, place, outer) {
|
|
|
130
173
|
const placeholders = new Map();
|
|
131
174
|
const placeholderOf = (id, own) => {
|
|
132
175
|
if (own.placeholderWhen?.length)
|
|
133
|
-
placeholders.set(id, { fixed: own.placeholder, when: own.placeholderWhen.map((item) => ({ text: item.text, holds: compileModifier(item.when) })) });
|
|
176
|
+
placeholders.set(id, { fixed: own.placeholder, when: own.placeholderWhen.map((item) => ({ text: item.text, holds: compileModifier(item.when, table) })) });
|
|
134
177
|
};
|
|
135
178
|
for (const node of index.values())
|
|
136
179
|
if (node.kind === 'field' && 'type' in node.source)
|
|
@@ -140,9 +183,11 @@ function innerForm(options, within, place, outer) {
|
|
|
140
183
|
/** Whether any rule only warns: without one, there are never warnings to look for. */
|
|
141
184
|
const warns = [...nodeRules.values()].some((rules) => rules.some((compiled) => compiled.rule.level === 'warning'));
|
|
142
185
|
const steps = page.layout.type === 'wizard' ? page.layout.children.map((step) => step.id) : [];
|
|
143
|
-
const
|
|
186
|
+
const storeKey = () => `fieldia:draft:${page.id}:${state.recordId ?? 'new'}`;
|
|
144
187
|
/** Today, on the clock of the page's time zone (the browser's own when it names none). */
|
|
145
188
|
const today = () => dayIn(options.now?.() ?? new Date(), page.timeZone);
|
|
189
|
+
/** Whether a field's own words — a label, a choice's — show a value: found once, so a page with none checks as before. */
|
|
190
|
+
const worded = /\{[A-Za-z_]\w*(\|[^{}]*)?\}/.test(JSON.stringify(page.fields));
|
|
146
191
|
const clock = () => (options.now?.() ?? new Date()).getTime();
|
|
147
192
|
/** What a page's bot check saw — its hidden box filled, the captcha's token — and when the page was shown or started again. */
|
|
148
193
|
let bot = { honeypot: false, captcha: null, since: clock() };
|
|
@@ -162,10 +207,10 @@ function innerForm(options, within, place, outer) {
|
|
|
162
207
|
/** Whether the record is being edited now: not while a view locks it, nor when its source lets the person only read it. */
|
|
163
208
|
const editingNow = () => editing && !(started && state.can?.edit === false);
|
|
164
209
|
const about = () => ({ recordId: started ? state.recordId : (options.recordId ?? null), user: options.user, editing: editingNow(), context: options.context, parent: parentContext });
|
|
165
|
-
const computed = compileComputed(page, today, about);
|
|
210
|
+
const computed = compileComputed(page, today, about, app);
|
|
166
211
|
/** Each value worked out from others — by the page, or by the app as the lines change — and what it waits for. */
|
|
167
|
-
const workedOut = workedOutFrom(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent);
|
|
168
|
-
const setWhen = compileSetWhen(page, today, about);
|
|
212
|
+
const workedOut = workedOutFrom(page, !!options.dataSource?.onchange && page.data.kind === 'record', !!options.parent, table);
|
|
213
|
+
const setWhen = compileSetWhen(page, today, about, app);
|
|
169
214
|
/** The saved forms placed on the page, by the part's id: each one's answers sit under its name, as JSON. */
|
|
170
215
|
const parts = new Map();
|
|
171
216
|
for (const node of index.values())
|
|
@@ -217,7 +262,7 @@ function innerForm(options, within, place, outer) {
|
|
|
217
262
|
const values = initialValues(fields, today());
|
|
218
263
|
if (isNew) {
|
|
219
264
|
const about = { recordId: null, user: options.user, context: options.context, parent: parentContext };
|
|
220
|
-
Object.assign(values, firstValues(page.fields, mapScope(values, page.fields, recordContext(values, page.fields, about), expressionEnv(values, page.fields, today))));
|
|
265
|
+
Object.assign(values, firstValues(page.fields, mapScope(values, page.fields, recordContext(values, page.fields, about), expressionEnv(values, page.fields, today, app))));
|
|
221
266
|
}
|
|
222
267
|
return given ? ordered({ ...values, ...structuredCopy(options.values ?? {}) }) : values;
|
|
223
268
|
}
|
|
@@ -246,7 +291,7 @@ function innerForm(options, within, place, outer) {
|
|
|
246
291
|
function lineScope(field, values) {
|
|
247
292
|
const def = lineField(field);
|
|
248
293
|
const lineFields = def.fields;
|
|
249
|
-
return mapScope(values, lineFields, lineContext(values, lineFields, reading().context), { today }, { values: state.values, fields: page.fields });
|
|
294
|
+
return mapScope(values, lineFields, lineContext(values, lineFields, reading().context), { ...app, today }, { values: state.values, fields: page.fields });
|
|
250
295
|
}
|
|
251
296
|
function tableOf(nodeId) {
|
|
252
297
|
const table = tables.get(nodeId);
|
|
@@ -266,7 +311,7 @@ function innerForm(options, within, place, outer) {
|
|
|
266
311
|
if (!line)
|
|
267
312
|
throw new Error(`"${table.field}" has no line "${key}"`);
|
|
268
313
|
const def = lineField(table.field);
|
|
269
|
-
return table.compiled.line(lineContext(line.values, def.fields, reading().context, line.id ?? null), { today }, userRoles);
|
|
314
|
+
return table.compiled.line(lineContext(line.values, def.fields, reading().context, line.id ?? null), { ...app, today }, userRoles);
|
|
270
315
|
}
|
|
271
316
|
function createValuesOf(field, line) {
|
|
272
317
|
if (!line) {
|
|
@@ -279,7 +324,9 @@ function innerForm(options, within, place, outer) {
|
|
|
279
324
|
return {};
|
|
280
325
|
return readLooseMap(sub.createValues, lineScope(line.lines, found.values));
|
|
281
326
|
}
|
|
282
|
-
|
|
327
|
+
/** The values worked out from an app's function that asks with no answer yet, as the values shown last settled. */
|
|
328
|
+
let askedNow = new Map();
|
|
329
|
+
let baseline = computed.apply(startingValues(), askedNow);
|
|
283
330
|
/** The setWhen conditions holding for the values as they last settled: one starts to hold only against these. */
|
|
284
331
|
let holding = setWhen.holding(baseline);
|
|
285
332
|
let state = {
|
|
@@ -306,6 +353,12 @@ function innerForm(options, within, place, outer) {
|
|
|
306
353
|
unsent: [],
|
|
307
354
|
leaving: false,
|
|
308
355
|
unread: {},
|
|
356
|
+
sent: null,
|
|
357
|
+
asked: Object.fromEntries(askedNow),
|
|
358
|
+
takenOut: {},
|
|
359
|
+
// Worked out once the form has its state, as the limits part comes (`limitsCode`).
|
|
360
|
+
limits: null,
|
|
361
|
+
resume: null,
|
|
309
362
|
};
|
|
310
363
|
started = true;
|
|
311
364
|
/**
|
|
@@ -478,7 +531,6 @@ function innerForm(options, within, place, outer) {
|
|
|
478
531
|
let onchangeSeq = 0;
|
|
479
532
|
let draftTimer = null;
|
|
480
533
|
let autosaveTimer = null;
|
|
481
|
-
const pending = new Set();
|
|
482
534
|
const listeners = new Set();
|
|
483
535
|
/** The app's listeners, by event. */
|
|
484
536
|
const events = new Map();
|
|
@@ -492,10 +544,23 @@ function innerForm(options, within, place, outer) {
|
|
|
492
544
|
const userRoles = [...(options.user?.roles ?? [])];
|
|
493
545
|
/** The latest load of each field's choices: an answer to an older one is let go. */
|
|
494
546
|
const choiceLoads = {};
|
|
547
|
+
/** The tables' columns whose choices are the app's: none on most pages, and then nothing below is followed. */
|
|
548
|
+
const listed = listedColumns(page.fields);
|
|
549
|
+
/** Whether the values set now are a change someone made (`writeValues`), after which a line's choice no longer offered is cleared. */
|
|
550
|
+
let edit = false;
|
|
495
551
|
function set(patch) {
|
|
496
552
|
const was = state.values;
|
|
497
553
|
const shown = state.errors;
|
|
554
|
+
const edited = edit;
|
|
555
|
+
edit = false;
|
|
556
|
+
const stepWas = state.step;
|
|
498
557
|
state = { ...state, ...patch };
|
|
558
|
+
// A line's choices go with their line.
|
|
559
|
+
if (state.values !== was)
|
|
560
|
+
state = { ...state, choices: choicesNow(state.choices) };
|
|
561
|
+
// What waits for an app's answer, as the values shown last settled.
|
|
562
|
+
if (patch.values !== undefined && patch.asked === undefined && !sameAsked(state.asked, askedNow))
|
|
563
|
+
state = { ...state, asked: Object.fromEntries(askedNow) };
|
|
499
564
|
// The person's changes the last save did not send, worked out again from the values as they are now, and from the
|
|
500
565
|
// text they left that does not read.
|
|
501
566
|
if (patch.unsent === undefined && (patch.values !== undefined || patch.unread !== undefined)) {
|
|
@@ -511,15 +576,21 @@ function innerForm(options, within, place, outer) {
|
|
|
511
576
|
listener(state);
|
|
512
577
|
// Choices that change with a value that just changed are asked for again.
|
|
513
578
|
if (state.values !== was) {
|
|
579
|
+
// After someone's change, a choice its new choices do not offer is taken out and said, as a line's is.
|
|
514
580
|
for (const name in state.choices)
|
|
515
|
-
if (fromList(name)
|
|
516
|
-
loadChoices(name);
|
|
581
|
+
if (!name.includes('.') && fromList(name)?.dependsOn?.some((other) => was[other] !== state.values[other]))
|
|
582
|
+
loadChoices(name, edited);
|
|
517
583
|
for (const name in state.definitions)
|
|
518
584
|
if (fromRecord(name).dependsOn?.some((other) => was[other] !== state.values[other]))
|
|
519
585
|
loadDefinitions(name);
|
|
586
|
+
if (listed.length)
|
|
587
|
+
followLines(was, edited);
|
|
520
588
|
}
|
|
521
589
|
if (showing && state.step !== told)
|
|
522
590
|
tellStep(entering);
|
|
591
|
+
// A wizard's step entered with answers typed is kept in the draft, so it opens there again.
|
|
592
|
+
if (state.step !== stepWas && showing && state.dirty.length)
|
|
593
|
+
scheduleDraft();
|
|
523
594
|
}
|
|
524
595
|
function emit(event, payload) {
|
|
525
596
|
const heard = events.get(event);
|
|
@@ -542,6 +613,9 @@ function innerForm(options, within, place, outer) {
|
|
|
542
613
|
/** Another record is shown: nothing of the last one's is the person's work any more, and its files on their way stop. */
|
|
543
614
|
function newGeneration() {
|
|
544
615
|
generation++;
|
|
616
|
+
// Another record's lines ask the source anew.
|
|
617
|
+
lineAsks = new Map();
|
|
618
|
+
answers = new Map();
|
|
545
619
|
personal.clear();
|
|
546
620
|
personalLines.clear();
|
|
547
621
|
filed.clear();
|
|
@@ -666,16 +740,127 @@ function innerForm(options, within, place, outer) {
|
|
|
666
740
|
entering = was;
|
|
667
741
|
}
|
|
668
742
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
743
|
+
/** The app's list a choice's options come from; none for one whose options are its own or carried. */
|
|
744
|
+
const fromList = (name) => optionsList(page.fields[name]?.optionsFrom);
|
|
745
|
+
// ---- a table line's choices from the app's lists (line-choices.ts) ----------------------------------------------
|
|
746
|
+
/** A line's choices asked of the source, by what they are asked by (`askKey`), for the record shown: lines alike share one. */
|
|
747
|
+
let lineAsks = new Map();
|
|
748
|
+
/** What each line cell's choices were last asked by. */
|
|
749
|
+
const askedBy = new Map();
|
|
750
|
+
const listedAt = (table, column) => listed.find((one) => one.table === table && one.column === column);
|
|
751
|
+
const lineAt = (table, key) => (state.values[table] ?? []).find((line) => line.key === key);
|
|
752
|
+
/** A line's choices from the source: asked once for every line whose columns they change with hold the same values. */
|
|
753
|
+
function askLine(one, values) {
|
|
754
|
+
const key = askKey(one, values, state.values);
|
|
755
|
+
const asks = lineAsks;
|
|
756
|
+
let ask = asks.get(key);
|
|
757
|
+
if (ask)
|
|
758
|
+
return ask;
|
|
759
|
+
const source = options.dataSource;
|
|
760
|
+
const request = { list: one.list, values: structuredCopy(values), parent: structuredCopy(state.values) };
|
|
761
|
+
ask = source?.options ? source.options(request) : Promise.reject(new Error(`No list "${one.list}"`));
|
|
762
|
+
asks.set(key, ask);
|
|
763
|
+
// One that failed is asked again next time.
|
|
764
|
+
ask.catch(() => asks.get(key) === ask && asks.delete(key));
|
|
765
|
+
return ask;
|
|
766
|
+
}
|
|
767
|
+
/** The choices as the values are now: a line's go with their line. */
|
|
768
|
+
function choicesNow(choices) {
|
|
769
|
+
let kept = null;
|
|
770
|
+
for (const path of Object.keys(choices)) {
|
|
771
|
+
if (valueAt(state.values, path) !== gone)
|
|
772
|
+
continue;
|
|
773
|
+
kept ??= { ...choices };
|
|
774
|
+
delete kept[path];
|
|
775
|
+
askedBy.delete(path);
|
|
776
|
+
}
|
|
777
|
+
return kept ?? choices;
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Values changed: each line's choices loaded are asked again where what they change with changed on their line (a
|
|
781
|
+
* record shown with the same line, Discard, a draft); after a change someone made (`edited`), judged too — and so is a
|
|
782
|
+
* line holding a choice whose choices nobody asked for yet, so none is kept that its columns no longer offer.
|
|
783
|
+
*/
|
|
784
|
+
function followLines(was, edited) {
|
|
785
|
+
// Choices that change with nothing are asked once.
|
|
786
|
+
for (const one of listed.filter((column) => column.own.length || column.parent.length)) {
|
|
787
|
+
for (const line of state.values[one.table] ?? []) {
|
|
788
|
+
const path = `${one.table}.${line.key}.${one.column}`;
|
|
789
|
+
const by = askKey(one, line.values, state.values);
|
|
790
|
+
const old = !(path in state.choices) && edited && !isEmpty(one.def, line.values[one.column]) && (was[one.table] ?? []).find((l) => l.key === line.key);
|
|
791
|
+
if (path in state.choices ? by !== askedBy.get(path) : old && by !== askKey(one, old.values, was))
|
|
792
|
+
loadChoices(path, edited);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Load a field's choices, or a line cell's by its path (`table.key.column`). `judge`, after a change someone made of
|
|
798
|
+
* what they change with: a choice they no longer offer is taken out and said, on the record they were asked for,
|
|
799
|
+
* while a line still asks by the same.
|
|
800
|
+
*/
|
|
801
|
+
function loadChoices(name, judge = false) {
|
|
802
|
+
const cell = cellOf(name);
|
|
803
|
+
const one = cell && listedAt(cell.table, cell.column);
|
|
804
|
+
const line = cell && lineAt(cell.table, cell.key);
|
|
805
|
+
if (cell && (!one || !line))
|
|
806
|
+
throw new Error(`"${name}" is no line's choice from the app's lists`);
|
|
807
|
+
const from = one ?? fromList(name);
|
|
808
|
+
if (!from)
|
|
809
|
+
throw new Error(`"${name}" takes no choices from the app's lists`);
|
|
810
|
+
const { list } = from;
|
|
672
811
|
const load = (choiceLoads[name] = (choiceLoads[name] ?? 0) + 1);
|
|
673
|
-
const
|
|
812
|
+
const by = one && line ? askKey(one, line.values, state.values) : '';
|
|
813
|
+
if (cell)
|
|
814
|
+
askedBy.set(name, by);
|
|
815
|
+
// A line's answer lands while its line is there.
|
|
816
|
+
const put = (choices) => load === choiceLoads[name] && (!cell || name in state.choices) && set({ choices: { ...state.choices, [name]: choices } });
|
|
674
817
|
// What was shown stays while the next ones load, so the list does not flash empty.
|
|
675
818
|
const shown = state.choices[name]?.options ?? null;
|
|
676
|
-
|
|
819
|
+
const loading = { options: shown, loading: true };
|
|
820
|
+
// A line's cell asks first as it is drawn, inside its table's drawing: told with the next change, never drawn again
|
|
821
|
+
// inside it. Asked again, as a column they change with changes, it says it is loading at once.
|
|
822
|
+
if (cell && !(name in state.choices))
|
|
823
|
+
state = { ...state, choices: { ...state.choices, [name]: loading } };
|
|
824
|
+
else
|
|
825
|
+
set({ choices: { ...state.choices, [name]: loading } });
|
|
677
826
|
const source = options.dataSource;
|
|
678
|
-
|
|
827
|
+
const onRecord = shownNow();
|
|
828
|
+
const ask = one && line
|
|
829
|
+
? askLine(one, line.values)
|
|
830
|
+
: source?.options
|
|
831
|
+
? // A line's own form (a dialog) asks as its line does: the record it is on as parent.
|
|
832
|
+
source.options({ list, values: structuredCopy(state.values), ...(options.parent ? { parent: structuredCopy(options.parent.values) } : {}) })
|
|
833
|
+
: Promise.reject(new Error(`No list "${list}"`));
|
|
834
|
+
void track(ask.then((loaded) => {
|
|
835
|
+
if (load !== choiceLoads[name])
|
|
836
|
+
return;
|
|
837
|
+
const now = cell && lineAt(cell.table, cell.key);
|
|
838
|
+
const held = !cell ? state.values[name] : now && askKey(one, now.values, state.values) === by ? now.values[cell.column] : null;
|
|
839
|
+
const lost = judge && onRecord() ? notOffered(one?.def ?? page.fields[name], held, loaded) : [];
|
|
840
|
+
// Taken out and said: the words stay until its value changes again.
|
|
841
|
+
if (lost.length)
|
|
842
|
+
clearChoices(cell ? cell.table : name, cell, lost, namedChoices(lost, shown, loaded));
|
|
843
|
+
put({ options: loaded });
|
|
844
|
+
// Options of the app's list with places, never counted yet: how many took each is asked.
|
|
845
|
+
if (!cell && loaded.some((option) => option.seats !== undefined) && (!limitsKit || limitsKit.uncounted(name)))
|
|
846
|
+
void track(limitsCode().then((kit) => kit?.ask()));
|
|
847
|
+
}, (error) => put({ options: shown, error: error.message ?? String(error) })));
|
|
848
|
+
}
|
|
849
|
+
/** Choices by their labels, as the choices shown before or now name them. */
|
|
850
|
+
const namedChoices = (lost, shown, loaded) => lost.map((value) => [...(shown ?? []), ...loaded].find((option) => option.value === value)?.label ?? String(value));
|
|
851
|
+
/**
|
|
852
|
+
* A choice — a field's, or a line's (`key`) column — less what its choices no longer offer, worked out from someone's
|
|
853
|
+
* change, and said as every choice taken out is (`FormState.takenOut`): their claim stays.
|
|
854
|
+
*/
|
|
855
|
+
function clearChoices(field, line, lost, labels) {
|
|
856
|
+
const without = (value) => (Array.isArray(value) ? value.filter((v) => !lost.includes(v)) : null);
|
|
857
|
+
const values = state.values;
|
|
858
|
+
const written = line
|
|
859
|
+
? (values[field] ?? []).map((l) => (l.key === line.key ? { ...l, values: { ...l.values, [line.column]: without(l.values[line.column]) } } : l))
|
|
860
|
+
: without(values[field]);
|
|
861
|
+
writeValues({ ...values, [field]: written }, {}, false, { [line ? `${field}.${line.key}.${line.column}` : field]: takenOutWords(labels) });
|
|
862
|
+
afterEdit(field);
|
|
863
|
+
changed(field, 'step', { keep: true });
|
|
679
864
|
}
|
|
680
865
|
const fromRecord = (name) => page.fields[name].definitionsFrom;
|
|
681
866
|
const definitionLoads = {};
|
|
@@ -695,8 +880,23 @@ function innerForm(options, within, place, outer) {
|
|
|
695
880
|
set({ definitions: { ...state.definitions, [name]: { definitions } } });
|
|
696
881
|
await track(options.dataSource?.saveDefinitions?.({ list, values: structuredCopy(state.values), definitions }) ?? Promise.resolve());
|
|
697
882
|
}
|
|
698
|
-
/**
|
|
883
|
+
/**
|
|
884
|
+
* A field as its value is checked: a list's choices are those loaded, and until they are, any choice may be one; its
|
|
885
|
+
* label and its choices' words with their `{field}`s filled in, as the page shows them.
|
|
886
|
+
*/
|
|
699
887
|
function checked(name) {
|
|
888
|
+
const def = checkedAsLoaded(name);
|
|
889
|
+
return worded ? shownWords(def) : def;
|
|
890
|
+
}
|
|
891
|
+
/** A field whose words show values — its label, its choices' — with them filled in now, else as it is. */
|
|
892
|
+
function shownWords(def) {
|
|
893
|
+
const options = def.type === 'selection' && def.options.some((option) => option.label.includes('{')) ? def.options.map((option) => ({ ...option, label: wordsNow(option.label) })) : null;
|
|
894
|
+
if (!def.label.includes('{') && !options)
|
|
895
|
+
return def;
|
|
896
|
+
return { ...def, label: wordsNow(def.label), ...(options ? { options } : {}) };
|
|
897
|
+
}
|
|
898
|
+
/** A field as its value is checked, its choices those loaded from the app's list. */
|
|
899
|
+
function checkedAsLoaded(name) {
|
|
700
900
|
const def = page.fields[name];
|
|
701
901
|
const loaded = state.choices[name]?.options;
|
|
702
902
|
if (def.type === 'properties' && def.definitionsFrom) {
|
|
@@ -704,7 +904,73 @@ function innerForm(options, within, place, outer) {
|
|
|
704
904
|
const definitions = state.definitions[name]?.definitions;
|
|
705
905
|
return definitions ? { ...def, definitions } : def;
|
|
706
906
|
}
|
|
707
|
-
|
|
907
|
+
// Carried from an earlier question: any of its options, and its own after them, is one of this one's.
|
|
908
|
+
if (def.type === 'selection' && optionsField(def.optionsFrom))
|
|
909
|
+
return { ...def, options: everyOption(name), optionsFrom: undefined };
|
|
910
|
+
return def.type === 'selection' && optionsList(def.optionsFrom) && loaded ? { ...def, options: loaded, optionsFrom: undefined } : def;
|
|
911
|
+
}
|
|
912
|
+
/** 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. */
|
|
913
|
+
function everyOption(name, seen = []) {
|
|
914
|
+
const def = page.fields[name];
|
|
915
|
+
if (def?.type !== 'selection')
|
|
916
|
+
return [];
|
|
917
|
+
const carried = optionsField(def.optionsFrom);
|
|
918
|
+
if (carried && !seen.includes(carried.field))
|
|
919
|
+
return [...everyOption(carried.field, [...seen, name]).filter((o) => !o.exclusive), ...def.options];
|
|
920
|
+
return [...((fromList(name) && state.choices[name]?.options) || def.options)];
|
|
921
|
+
}
|
|
922
|
+
// ---- options by condition, and carried from an earlier question (the options part: option-rules.ts) -----------
|
|
923
|
+
/** The options' rules made for this form from the options part, once it came: none before, nor on a page with none. */
|
|
924
|
+
let optionKit = null;
|
|
925
|
+
/**
|
|
926
|
+
* The options' rules, made from their part as it comes — at once where it has come — or null on a page with none, or
|
|
927
|
+
* where the part could not come. Values written before they came are judged as they come: a chosen option the values
|
|
928
|
+
* no longer offer, since the form was made, is taken out and said.
|
|
929
|
+
*/
|
|
930
|
+
function optionsCode() {
|
|
931
|
+
if (optionKit || !hasOptionRules(page))
|
|
932
|
+
return Promise.resolve(optionKit);
|
|
933
|
+
if (optionRulesPart.loaded)
|
|
934
|
+
return Promise.resolve(optionsMade(optionRulesPart.loaded));
|
|
935
|
+
const from = state.values;
|
|
936
|
+
return optionRulesPart.load().then((part) => {
|
|
937
|
+
const kit = optionsMade(part);
|
|
938
|
+
if (kit && !disposed && state.values !== from)
|
|
939
|
+
writeValues(state.values, {}, false, {}, from);
|
|
940
|
+
return kit;
|
|
941
|
+
}, () => null);
|
|
942
|
+
}
|
|
943
|
+
function optionsMade(part) {
|
|
944
|
+
optionKit ??= part.optionRules({ page, table, app, state: () => state, today, about, reading, settle, words: (labels) => takenOutWords(labels) });
|
|
945
|
+
return optionKit;
|
|
946
|
+
}
|
|
947
|
+
if (hasOptionRules(page))
|
|
948
|
+
void track(optionsCode());
|
|
949
|
+
/** 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. */
|
|
950
|
+
function lineOffered(lines, line, column, record) {
|
|
951
|
+
if (optionKit)
|
|
952
|
+
return optionKit.lineOffered(lines, line, column, record);
|
|
953
|
+
const sub = page.fields[lines].fields[column];
|
|
954
|
+
return [...((optionsList(sub.optionsFrom) && state.choices[`${lines}.${line.key}.${column}`]?.options) || sub.options)];
|
|
955
|
+
}
|
|
956
|
+
/** Options in words, as a sentence joins them: "Garden and Pool". */
|
|
957
|
+
function listedLabels(labels) {
|
|
958
|
+
try {
|
|
959
|
+
return new Intl.ListFormat(messages.locale, { type: 'conjunction' }).format(labels);
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
return labels.join(', ');
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* What is said of options taken out of an answer, by their labels — an answer they show (`{branch}`) as it reads now:
|
|
967
|
+
* "Garden is no longer offered, so it was taken out".
|
|
968
|
+
*/
|
|
969
|
+
const takenOutWords = (labels) => fillMessage(messages, labels.length > 1 ? messages.notOfferedNowMany : messages.notOfferedNow, { name: listedLabels(labels.map((label) => (label.includes('{') ? wordsNow(label) : label))) });
|
|
970
|
+
/** A line's field as its value is checked: its choices from the app's list, those loaded for its line. */
|
|
971
|
+
function checkedLine(path, def) {
|
|
972
|
+
const loaded = def.type === 'selection' && optionsList(def.optionsFrom) ? state.choices[path]?.options : null;
|
|
973
|
+
return loaded ? { ...def, options: loaded, optionsFrom: undefined } : def;
|
|
708
974
|
}
|
|
709
975
|
/** A properties field's problems, each at its property's path (`field.property`), as its row shows them. */
|
|
710
976
|
function propertyProblems(name) {
|
|
@@ -723,12 +989,57 @@ function innerForm(options, within, place, outer) {
|
|
|
723
989
|
promise.then(done, done);
|
|
724
990
|
return promise;
|
|
725
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Words with each `{field}` showing the value `values` hold (the form's now, unless given), as the app's viewer
|
|
994
|
+
* writes it (`valueWords`), else plainly; `other` answers a name first (an ending's `{id}`). A name that is no field
|
|
995
|
+
* shows nothing.
|
|
996
|
+
*/
|
|
997
|
+
function wordsNow(text, values = state.values, other) {
|
|
998
|
+
if (!text.includes('{'))
|
|
999
|
+
return text;
|
|
1000
|
+
return fillValues(text, (name) => {
|
|
1001
|
+
const own = other?.(name);
|
|
1002
|
+
if (own !== undefined)
|
|
1003
|
+
return own;
|
|
1004
|
+
const def = page.fields[name];
|
|
1005
|
+
if (!def)
|
|
1006
|
+
return '';
|
|
1007
|
+
return options.valueWords ? options.valueWords(def, values[name], values) : wordsOfValue(def, values[name]);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Each ending's condition, read once — with Fieldia's functions and the app's own, as every condition of the page —
|
|
1012
|
+
* null for the default (no `when`), and one that does not read never holds.
|
|
1013
|
+
*/
|
|
1014
|
+
const endingConditions = (page.endings ?? []).map((ending) => {
|
|
1015
|
+
if (ending.when === undefined)
|
|
1016
|
+
return null;
|
|
1017
|
+
try {
|
|
1018
|
+
return compileModifier(ending.when, table);
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
return compileModifier(false);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
/** The ending for the response sent: the first whose condition holds on it — its answers, what the source added, its id — else the default. */
|
|
1025
|
+
function endingNow() {
|
|
1026
|
+
const sent = state.sent;
|
|
1027
|
+
const endings = page.endings;
|
|
1028
|
+
if (!sent || !endings?.length)
|
|
1029
|
+
return null;
|
|
1030
|
+
const values = sent.values;
|
|
1031
|
+
const context = recordContext(values, page.fields, { ...about(), recordId: sent.id });
|
|
1032
|
+
// An app's function that asks its server is asked as any condition asks it: it holds once answered.
|
|
1033
|
+
const env = expressionEnv(values, page.fields, today, app);
|
|
1034
|
+
const at = endingConditions.findIndex((condition) => !condition || condition.evaluate(context, env));
|
|
1035
|
+
return at === -1 ? null : endings[at];
|
|
1036
|
+
}
|
|
726
1037
|
/** The values as expressions read them, and what else they read: the record's id, the person, the lines' rows, today. */
|
|
727
1038
|
function reading() {
|
|
728
1039
|
if (contextCache?.values !== state.values || contextCache.recordId !== state.recordId || contextCache.editing !== editingNow()) {
|
|
729
1040
|
const values = state.values;
|
|
730
1041
|
const context = recordContext(values, page.fields, about());
|
|
731
|
-
contextCache = { values, recordId: state.recordId, editing: editingNow(), context, env: expressionEnv(values, page.fields, today) };
|
|
1042
|
+
contextCache = { values, recordId: state.recordId, editing: editingNow(), context, env: expressionEnv(values, page.fields, today, app) };
|
|
732
1043
|
}
|
|
733
1044
|
return contextCache;
|
|
734
1045
|
}
|
|
@@ -766,6 +1077,10 @@ function innerForm(options, within, place, outer) {
|
|
|
766
1077
|
if ((node.kind === 'field' || node.kind === 'form') && (can.edit === false || (node.field !== undefined && can.readonlyFields?.includes(node.field) === true)))
|
|
767
1078
|
readonly = true;
|
|
768
1079
|
}
|
|
1080
|
+
// An answer a signature kept signed: locked until the signature is taken away and signed again.
|
|
1081
|
+
const answer = answerOf(node);
|
|
1082
|
+
if (answer !== undefined && signedNow().has(answer))
|
|
1083
|
+
readonly = true;
|
|
769
1084
|
return {
|
|
770
1085
|
invisible,
|
|
771
1086
|
readonly: readonly || (def !== undefined && lockedField(def)),
|
|
@@ -773,6 +1088,17 @@ function innerForm(options, within, place, outer) {
|
|
|
773
1088
|
...(node.pressed ? { pressed: node.pressed.evaluate(ctx, env) } : {}),
|
|
774
1089
|
};
|
|
775
1090
|
}
|
|
1091
|
+
/** 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. */
|
|
1092
|
+
function answerOf(node) {
|
|
1093
|
+
return node.kind === 'field' ? node.field : node.kind === 'form' ? node.source.name : undefined;
|
|
1094
|
+
}
|
|
1095
|
+
/** The answers signatures signed, as the values hold them now: worked out once for each change of the values. */
|
|
1096
|
+
let signedCache = null;
|
|
1097
|
+
function signedNow() {
|
|
1098
|
+
if (signedCache?.values !== state.values)
|
|
1099
|
+
signedCache = { values: state.values, answers: signedBy(page, state.values) };
|
|
1100
|
+
return signedCache.answers;
|
|
1101
|
+
}
|
|
776
1102
|
/** Field nodes — or saved forms placed on the page — inside `root` (or the whole page), visible right now; `field`'s alone, when named. */
|
|
777
1103
|
function visibleFieldNodes(root, kind = 'field', field) {
|
|
778
1104
|
const inside = (node) => {
|
|
@@ -801,7 +1127,7 @@ function innerForm(options, within, place, outer) {
|
|
|
801
1127
|
return {};
|
|
802
1128
|
const name = node.field;
|
|
803
1129
|
const def = page.fields[name];
|
|
804
|
-
const label = node.source.label ?? def.label;
|
|
1130
|
+
const label = wordsNow(node.source.label ?? def.label);
|
|
805
1131
|
const { context: ctx, env } = reading();
|
|
806
1132
|
return checkRules(def, label, state.values[name], rules, { context: ctx, env, now: options.now?.() ?? new Date(), messages });
|
|
807
1133
|
}
|
|
@@ -814,6 +1140,14 @@ function innerForm(options, within, place, outer) {
|
|
|
814
1140
|
/** The widget a field is shown with where it keeps its value its own way (a per cent): the first of its nodes'. */
|
|
815
1141
|
const shownWidget = (shown) => shown.map((node) => node.source.widget).find((widget) => widget === 'percentage');
|
|
816
1142
|
/** A field node's error now: its field's own rules first, then its answer rules. */
|
|
1143
|
+
/** A path's field's label: a field's, or a line's cell's (`field.lineKey.subfield`). */
|
|
1144
|
+
function labelOfPath(path) {
|
|
1145
|
+
const parts = path.split('.');
|
|
1146
|
+
const def = page.fields[parts[0]];
|
|
1147
|
+
if (parts.length >= 3 && def?.type === 'one2many')
|
|
1148
|
+
return def.fields[parts[parts.length - 1]]?.label ?? path;
|
|
1149
|
+
return def?.label ?? path;
|
|
1150
|
+
}
|
|
817
1151
|
function nodeError(node) {
|
|
818
1152
|
const name = node.field;
|
|
819
1153
|
return unticked(node) ?? checkInput(page.fields[name], node.source, state.values[name], messages) ?? checkValue(checked(name), state.values[name], nodeState(node.id).required, messages, today(), node.source.widget) ?? ruleCheck(node).error;
|
|
@@ -862,7 +1196,7 @@ function innerForm(options, within, place, outer) {
|
|
|
862
1196
|
return undefined;
|
|
863
1197
|
const at = (v) => (def.type === 'datetime' ? Date.parse(v) : Date.parse(v.slice(0, 10)));
|
|
864
1198
|
if (at(value) < at(start))
|
|
865
|
-
return fillMessage(messages, messages.endBeforeStart, { end: def.label, start: page.fields[node.field].label });
|
|
1199
|
+
return fillMessage(messages, messages.endBeforeStart, { end: wordsNow(def.label), start: wordsNow(page.fields[node.field].label) });
|
|
866
1200
|
return undefined;
|
|
867
1201
|
}
|
|
868
1202
|
/** Errors for the visible fields under `root`, without publishing them. */
|
|
@@ -900,7 +1234,7 @@ function innerForm(options, within, place, outer) {
|
|
|
900
1234
|
const cell = ruled[sub];
|
|
901
1235
|
if (cell?.invisible || (tables.has(node.id) && tables.get(node.id).compiled.hidden(sub, reading().context, reading().env, userRoles)))
|
|
902
1236
|
continue;
|
|
903
|
-
const lineMessage = checkValue(subDef, line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
|
|
1237
|
+
const lineMessage = checkValue(checkedLine(`${name}.${line.key}.${sub}`, subDef), line.values[sub], subDef.required === true || cell?.required === true, messages, today(), node.source.cells?.[sub]?.widget);
|
|
904
1238
|
if (lineMessage)
|
|
905
1239
|
errors[`${name}.${line.key}.${sub}`] = lineMessage;
|
|
906
1240
|
}
|
|
@@ -911,6 +1245,16 @@ function innerForm(options, within, place, outer) {
|
|
|
911
1245
|
for (const [path, unread] of Object.entries(state.unread))
|
|
912
1246
|
if (seen.has(path.split('.')[0]))
|
|
913
1247
|
errors[path] = unread.message;
|
|
1248
|
+
// A value an app's function is asked for answers for its field: still asked, or why it did not come.
|
|
1249
|
+
for (const [path, found] of Object.entries(state.asked)) {
|
|
1250
|
+
if (!seen.has(path.split('.')[0]) || errors[path])
|
|
1251
|
+
continue;
|
|
1252
|
+
const label = labelOfPath(path);
|
|
1253
|
+
errors[path] = found.failed === undefined ? fillMessage(messages, messages.stillWorkedOut, { label }) : fillMessage(messages, messages.notWorkedOut, { label, problem: found.failed });
|
|
1254
|
+
}
|
|
1255
|
+
// A choice held whose places filled up since it was picked: never sent, said under it.
|
|
1256
|
+
if (limitsKit)
|
|
1257
|
+
Object.assign(errors, limitsKit.fullErrors(seen, errors));
|
|
914
1258
|
// A saved form placed here answers for its fields by its own page's rules.
|
|
915
1259
|
for (const node of visibleFieldNodes(root, 'form')) {
|
|
916
1260
|
const part = attached.get(node.id);
|
|
@@ -935,6 +1279,13 @@ function innerForm(options, within, place, outer) {
|
|
|
935
1279
|
const line = (values[field] ?? []).find((l) => l.key === key);
|
|
936
1280
|
return line ? line.values[sub] : gone;
|
|
937
1281
|
}
|
|
1282
|
+
/** 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. */
|
|
1283
|
+
function sameAt(a, b, path) {
|
|
1284
|
+
if (!path.includes('.'))
|
|
1285
|
+
return a[path] === b[path];
|
|
1286
|
+
const now = valueAt(a, path);
|
|
1287
|
+
return now !== gone && JSON.stringify(now ?? null) === JSON.stringify(valueAt(b, path) ?? null);
|
|
1288
|
+
}
|
|
938
1289
|
/** Unread text whose field has since been written another way — a value typed that reads, the app's — is let go. */
|
|
939
1290
|
function unreadKept(values) {
|
|
940
1291
|
let kept = null;
|
|
@@ -975,7 +1326,7 @@ function innerForm(options, within, place, outer) {
|
|
|
975
1326
|
const value = valueAt(state.values, path);
|
|
976
1327
|
if (!sub || value === gone || isEmpty(sub, value))
|
|
977
1328
|
return undefined;
|
|
978
|
-
return checkValue(sub, value, false, messages, today());
|
|
1329
|
+
return checkValue(checkedLine(path, sub), value, false, messages, today());
|
|
979
1330
|
}
|
|
980
1331
|
/**
|
|
981
1332
|
* Answers changed from outside a saved form — loaded, put back, set — handed
|
|
@@ -1004,25 +1355,75 @@ function innerForm(options, within, place, outer) {
|
|
|
1004
1355
|
* few rounds at most, so rules that set each other cannot run forever.
|
|
1005
1356
|
*/
|
|
1006
1357
|
function settle(written) {
|
|
1007
|
-
let
|
|
1358
|
+
let asked = new Map();
|
|
1359
|
+
let values = computed.apply(written, asked);
|
|
1008
1360
|
for (let round = 0; round < 8; round++) {
|
|
1009
1361
|
const result = setWhen.apply(values, holding);
|
|
1010
1362
|
holding = result.holding;
|
|
1011
1363
|
if (result.values === values)
|
|
1012
1364
|
break;
|
|
1013
|
-
|
|
1365
|
+
asked = new Map();
|
|
1366
|
+
values = computed.apply(result.values, asked);
|
|
1014
1367
|
}
|
|
1368
|
+
askedNow = asked;
|
|
1015
1369
|
return values;
|
|
1016
1370
|
}
|
|
1017
1371
|
/** Values to start again from, as loaded or restored: conditions holding in them hold already, and set nothing. */
|
|
1018
1372
|
function startFrom(values) {
|
|
1019
|
-
const
|
|
1373
|
+
const asked = new Map();
|
|
1374
|
+
const worked = computed.apply(values, asked);
|
|
1375
|
+
askedNow = asked;
|
|
1020
1376
|
holding = setWhen.holding(worked);
|
|
1021
1377
|
return worked;
|
|
1022
1378
|
}
|
|
1023
|
-
/**
|
|
1024
|
-
|
|
1025
|
-
|
|
1379
|
+
/**
|
|
1380
|
+
* An app's function answered: the values worked out again with its answer, and the record as loaded with them, so
|
|
1381
|
+
* an answer is never a change of the person's. Values no one has changed start again from it — no rule starts to
|
|
1382
|
+
* hold on what only came late — and changed ones settle as any change does.
|
|
1383
|
+
*/
|
|
1384
|
+
function answered() {
|
|
1385
|
+
baseline = computed.apply(baseline);
|
|
1386
|
+
if (state.dirty.length)
|
|
1387
|
+
return writeValues(state.values);
|
|
1388
|
+
const values = startFrom(state.values);
|
|
1389
|
+
set({ values, dirty: dirtyFields(values) });
|
|
1390
|
+
}
|
|
1391
|
+
/** Every answer on its way, and those that come of them, waited for: what a save does before it checks. */
|
|
1392
|
+
async function asksSettled() {
|
|
1393
|
+
while (asking.size)
|
|
1394
|
+
await Promise.all([...asking]);
|
|
1395
|
+
}
|
|
1396
|
+
/** Answers that failed asked again — a save's try — and waited for. */
|
|
1397
|
+
async function askAgain() {
|
|
1398
|
+
let failed = false;
|
|
1399
|
+
for (const [key, found] of answers) {
|
|
1400
|
+
if (found === 'asking' || found.failed === undefined)
|
|
1401
|
+
continue;
|
|
1402
|
+
answers.delete(key);
|
|
1403
|
+
failed = true;
|
|
1404
|
+
}
|
|
1405
|
+
if (failed)
|
|
1406
|
+
writeValues(state.values);
|
|
1407
|
+
await asksSettled();
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Write values someone or something changed. `fresh` values are a starting point instead (a restored draft). `said`:
|
|
1411
|
+
* options taken out of an answer by the write, by path, with their words (`FormState.takenOut`); `from`, the values
|
|
1412
|
+
* those the write no longer offers were offered in (the form's now, unless said).
|
|
1413
|
+
*/
|
|
1414
|
+
function writeValues(written, extra = {}, fresh = false, said = {}, from = state.values) {
|
|
1415
|
+
let settled = fresh ? startFrom(written) : settle(written);
|
|
1416
|
+
// A choice written again says no more what was taken out of it — a line's, nor once its line is gone; options a
|
|
1417
|
+
// change no longer offers are taken out now.
|
|
1418
|
+
let takenOut = fresh ? {} : { ...Object.fromEntries(Object.entries(state.takenOut).filter(([path]) => sameAt(written, state.values, path))), ...said };
|
|
1419
|
+
const out = fresh || !optionKit ? null : optionKit.takeOut(from, settled);
|
|
1420
|
+
if (out) {
|
|
1421
|
+
settled = out.values;
|
|
1422
|
+
takenOut = { ...takenOut, ...out.said };
|
|
1423
|
+
}
|
|
1424
|
+
if (Object.keys(takenOut).length || Object.keys(state.takenOut).length)
|
|
1425
|
+
extra = { takenOut, ...extra };
|
|
1426
|
+
const values = syncParts(settled, fresh ? 'fresh' : 'edit');
|
|
1026
1427
|
// Text left unread in a box goes once its field is written another way, and its words with it.
|
|
1027
1428
|
const unread = unreadKept(values);
|
|
1028
1429
|
const before = state;
|
|
@@ -1048,6 +1449,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1048
1449
|
}
|
|
1049
1450
|
if (Object.keys(words).length)
|
|
1050
1451
|
errors = { ...errors, ...words };
|
|
1452
|
+
// A change someone made: a line's choice its columns no longer offer is cleared (followLines); a draft restored is none.
|
|
1453
|
+
edit = !fresh;
|
|
1051
1454
|
set({ values, dirty: dirtyFields(values), errors, warnings: withValues(values, collectWarnings), unread, ...extra });
|
|
1052
1455
|
}
|
|
1053
1456
|
/** Once errors are on screen, keep them current as the person fixes things. */
|
|
@@ -1070,24 +1473,84 @@ function innerForm(options, within, place, outer) {
|
|
|
1070
1473
|
kept[key] ??= unstoredNow[key];
|
|
1071
1474
|
return kept;
|
|
1072
1475
|
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Whether drafts are kept on the app's server: its source keeps them, and the page offers "Continue later" (or the
|
|
1478
|
+
* form continues one by its key). A page without either keeps its drafts where it always did.
|
|
1479
|
+
*/
|
|
1480
|
+
const onServer = () => !!options.dataSource?.saveDraft && !!options.dataSource.loadDraft && (page.continueLater === true || !!options.resume);
|
|
1481
|
+
/** Whether drafts are kept: the app gave a store, or they are kept on the server. */
|
|
1482
|
+
const draftsOn = () => !!options.drafts || onServer();
|
|
1483
|
+
/** The key of the draft kept on the server for the record shown; null until the source gave one. */
|
|
1484
|
+
let serverKey = null;
|
|
1485
|
+
/** Counted each time the draft is let go (sent, discarded, another record shown): a keeping answered after is dropped. */
|
|
1486
|
+
let draftEpoch = 0;
|
|
1487
|
+
/** The draft kept on the server last asked to be kept: the next waits for it, so it carries the key it gave. */
|
|
1488
|
+
let draftGoing = Promise.resolve();
|
|
1489
|
+
/** How many drafts are on their way to the server. */
|
|
1490
|
+
let keeping = 0;
|
|
1491
|
+
/** The draft found as the form opened, offered to restore (`state.draft`). */
|
|
1492
|
+
let found = null;
|
|
1493
|
+
/** The key the form was given to continue from (`resume`), until it is opened. */
|
|
1494
|
+
let resumeKey = outer ? undefined : options.resume;
|
|
1073
1495
|
function scheduleDraft() {
|
|
1074
|
-
|
|
1075
|
-
if (!drafts)
|
|
1496
|
+
if (!draftsOn())
|
|
1076
1497
|
return;
|
|
1077
1498
|
if (draftTimer !== null)
|
|
1078
1499
|
scheduler.clearTimeout(draftTimer);
|
|
1079
1500
|
draftTimer = scheduler.setTimeout(() => {
|
|
1080
1501
|
draftTimer = null;
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1502
|
+
// On the server, a failure is kept in the browser's store instead, where there is one: the form goes on.
|
|
1503
|
+
void track(keepDraft(false).catch(() => keepHere(draftNow())));
|
|
1504
|
+
}, options.drafts?.delayMs ?? (onServer() ? 1500 : 400));
|
|
1505
|
+
}
|
|
1506
|
+
/** The draft of what is typed now: the answers, the wizard's step and the steps passed over, text that does not read. */
|
|
1507
|
+
function draftNow() {
|
|
1508
|
+
// Where files are stored apart, never a file's bytes: they would fill the browser's store (uploads.ts). Without, kept as always.
|
|
1509
|
+
const values = options.dataSource?.upload ? draftOf(state.values) : structuredCopy(state.values);
|
|
1510
|
+
return {
|
|
1511
|
+
values,
|
|
1512
|
+
...(steps.length ? { step: state.step, skipped: [...state.skipped] } : {}),
|
|
1513
|
+
...(Object.keys(state.unread).length ? { unread: structuredCopy(state.unread) } : {}),
|
|
1514
|
+
answered: Object.keys(fields).filter((name) => state.dirty.includes(name)),
|
|
1515
|
+
savedAt: new Date(clock()).toISOString(),
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
/** A draft in the browser's store, where the app gave one: a store that is full keeps none. */
|
|
1519
|
+
function keepHere(kept) {
|
|
1520
|
+
try {
|
|
1521
|
+
options.drafts?.store.setItem(storeKey(), JSON.stringify(kept));
|
|
1522
|
+
}
|
|
1523
|
+
catch {
|
|
1524
|
+
// The draft is a convenience: the form goes on without it.
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Keep what is typed now: on the server where the source keeps drafts — one at a time, each carrying the key the last
|
|
1529
|
+
* was given; its key in the browser's store — else in the browser's store. The server's answer, or null.
|
|
1530
|
+
*/
|
|
1531
|
+
function keepDraft(later) {
|
|
1532
|
+
const draft = draftNow();
|
|
1533
|
+
const source = options.dataSource;
|
|
1534
|
+
if (!onServer() || !source?.saveDraft) {
|
|
1535
|
+
keepHere(draft);
|
|
1536
|
+
return Promise.resolve(null);
|
|
1537
|
+
}
|
|
1538
|
+
const shown = shownNow();
|
|
1539
|
+
const epoch = draftEpoch;
|
|
1540
|
+
keeping++;
|
|
1541
|
+
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 } : {}) }));
|
|
1542
|
+
draftGoing = going.then(() => void keeping--, () => void keeping--);
|
|
1543
|
+
return going.then((saved) => {
|
|
1544
|
+
// Kept for a record no longer shown, or for answers sent or let go meanwhile: the source has it; nothing is laid here.
|
|
1545
|
+
if (!shown() || epoch !== draftEpoch)
|
|
1546
|
+
return saved;
|
|
1547
|
+
serverKey = saved.key;
|
|
1548
|
+
if (state.resume?.key !== saved.key || state.resume.missing)
|
|
1549
|
+
set({ resume: { key: saved.key } });
|
|
1550
|
+
keepHere({ savedAt: draft.savedAt, key: saved.key });
|
|
1551
|
+
emit('draft', { key: saved.key, savedAt: draft.savedAt, later, saved });
|
|
1552
|
+
return saved;
|
|
1553
|
+
});
|
|
1091
1554
|
}
|
|
1092
1555
|
/** 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. */
|
|
1093
1556
|
function withDraft(kept) {
|
|
@@ -1099,32 +1562,144 @@ function innerForm(options, within, place, outer) {
|
|
|
1099
1562
|
}
|
|
1100
1563
|
return values;
|
|
1101
1564
|
}
|
|
1565
|
+
/** 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. */
|
|
1566
|
+
function applyDraft(draft) {
|
|
1567
|
+
found = null;
|
|
1568
|
+
writeValues(withDraft(draft.values ?? {}), { draft: null }, true);
|
|
1569
|
+
const known = (draft.skipped ?? []).filter((id) => steps.includes(id));
|
|
1570
|
+
// Text typed that did not read stays as typed, said as it was, where its field is still on the page.
|
|
1571
|
+
const unread = Object.fromEntries(Object.entries(draft.unread ?? {}).filter(([path]) => !!page.fields[path.split('.')[0]] && typeof draft.unread?.[path]?.text === 'string'));
|
|
1572
|
+
const patch = {
|
|
1573
|
+
...(steps.length ? { skipped: known, step: resumeStep(steps, form.steps(), draft.step) } : {}),
|
|
1574
|
+
...(Object.keys(unread).length ? { unread: { ...state.unread, ...unread } } : {}),
|
|
1575
|
+
// Its boxes were left with it: why each does not read is said at once, as it was where it was typed.
|
|
1576
|
+
...(Object.keys(unread).length ? { errors: { ...state.errors, ...Object.fromEntries(Object.entries(unread).map(([path, kept]) => [path, kept.message])) } } : {}),
|
|
1577
|
+
};
|
|
1578
|
+
if (Object.keys(patch).length)
|
|
1579
|
+
set(patch);
|
|
1580
|
+
}
|
|
1102
1581
|
function forgetDraft() {
|
|
1103
1582
|
if (draftTimer !== null)
|
|
1104
1583
|
scheduler.clearTimeout(draftTimer);
|
|
1105
1584
|
draftTimer = null;
|
|
1106
|
-
|
|
1585
|
+
found = null;
|
|
1586
|
+
draftEpoch++;
|
|
1587
|
+
serverKey = null;
|
|
1588
|
+
if (state.resume)
|
|
1589
|
+
set({ resume: null });
|
|
1590
|
+
options.drafts?.store.removeItem(storeKey());
|
|
1591
|
+
}
|
|
1592
|
+
/** A draft found: laid at once (`restore: "auto"`, or a draft continued from its key over nothing typed), else offered. */
|
|
1593
|
+
function offer(draft, auto = options.drafts?.restore === 'auto') {
|
|
1594
|
+
// What the person typed while it was asked for is theirs: the draft is offered, never laid over it.
|
|
1595
|
+
if (auto && !state.dirty.length && !Object.keys(state.unread).length)
|
|
1596
|
+
return applyDraft(draft);
|
|
1597
|
+
found = draft;
|
|
1598
|
+
set({ draft: { savedAt: draft.savedAt } });
|
|
1107
1599
|
}
|
|
1600
|
+
/**
|
|
1601
|
+
* The draft kept for the record shown, offered as it opens: the one the form was given to continue from (`resume`),
|
|
1602
|
+
* 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
|
|
1603
|
+
* on the record it was asked for.
|
|
1604
|
+
*/
|
|
1108
1605
|
function offerDraft() {
|
|
1109
|
-
const
|
|
1110
|
-
|
|
1606
|
+
const source = options.dataSource;
|
|
1607
|
+
const given = resumeKey;
|
|
1608
|
+
resumeKey = undefined;
|
|
1609
|
+
const kept = given ? null : draftRead(options.drafts?.store.getItem(storeKey()) ?? null);
|
|
1610
|
+
if (!given && !kept)
|
|
1111
1611
|
return;
|
|
1112
|
-
|
|
1113
|
-
|
|
1612
|
+
if (kept && !('key' in kept))
|
|
1613
|
+
return offer(kept);
|
|
1614
|
+
const key = given ?? kept.key;
|
|
1615
|
+
if (!source?.loadDraft || !onServer())
|
|
1114
1616
|
return;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1617
|
+
const shown = shownNow();
|
|
1618
|
+
if (given)
|
|
1619
|
+
set({ status: 'loading' });
|
|
1620
|
+
return track(source.loadDraft({ pageId: page.id, key }).then((draft) => {
|
|
1621
|
+
if (!shown())
|
|
1622
|
+
return;
|
|
1623
|
+
if (given)
|
|
1624
|
+
set({ status: 'ready' });
|
|
1625
|
+
if (!draft) {
|
|
1626
|
+
// Sent, or gone: the form starts empty, and says so where it was asked to continue it.
|
|
1627
|
+
options.drafts?.store.removeItem(storeKey());
|
|
1628
|
+
if (given)
|
|
1629
|
+
set({ resume: { key, missing: true } });
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
serverKey = key;
|
|
1633
|
+
set({ resume: { key } });
|
|
1634
|
+
offer(draft, given ? true : undefined);
|
|
1635
|
+
}, (error) => {
|
|
1636
|
+
if (shown() && given)
|
|
1637
|
+
set({ status: 'error', error: error?.message ?? String(error) });
|
|
1638
|
+
}));
|
|
1639
|
+
}
|
|
1640
|
+
// ---- when the page takes answers, and each choice's places (the limits part: form-limits.ts) --------------------
|
|
1641
|
+
/** The limits made for this form from the limits part, once it came: none before, nor on a page with neither limits nor places. */
|
|
1642
|
+
let limitsKit = null;
|
|
1643
|
+
/** Whether the record shown was loaded once: its counts are asked then, its own choices not counted against it. */
|
|
1644
|
+
let loadedOnce = false;
|
|
1645
|
+
/** Whether the form keeps limits: a page with them or with places of its own, or a list's choices that came with places. */
|
|
1646
|
+
const keepsLimits = () => !outer && (hasLimits(page) || Object.values(state.choices).some((choices) => choices.options?.some((option) => option.seats !== undefined)));
|
|
1647
|
+
/**
|
|
1648
|
+
* The limits, made from their part as it comes — at once where it has come — shown, and asked of the source as the
|
|
1649
|
+
* form opens (or once its record is loaded); null where the form keeps none, or the part could not come: the source's
|
|
1650
|
+
* submit decides then.
|
|
1651
|
+
*/
|
|
1652
|
+
function limitsCode() {
|
|
1653
|
+
if (limitsKit || !keepsLimits())
|
|
1654
|
+
return Promise.resolve(limitsKit);
|
|
1655
|
+
if (formLimitsPart.loaded)
|
|
1656
|
+
return Promise.resolve(limitsMade(formLimitsPart.loaded));
|
|
1657
|
+
return formLimitsPart.load().then(limitsMade, () => null);
|
|
1658
|
+
}
|
|
1659
|
+
function limitsMade(part) {
|
|
1660
|
+
if (limitsKit || disposed)
|
|
1661
|
+
return limitsKit;
|
|
1662
|
+
const kit = part.formLimits({
|
|
1663
|
+
page,
|
|
1664
|
+
dataSource: options.dataSource,
|
|
1665
|
+
user: options.user,
|
|
1666
|
+
messages,
|
|
1667
|
+
scheduler,
|
|
1668
|
+
state: () => state,
|
|
1669
|
+
set,
|
|
1670
|
+
clock,
|
|
1671
|
+
started: () => started,
|
|
1672
|
+
shownNow,
|
|
1673
|
+
generation: () => generation,
|
|
1674
|
+
replaced: () => named(messages.recordReplacedNoun, messages.recordReplaced),
|
|
1675
|
+
listened: () => listeners.size > 0,
|
|
1676
|
+
disposed: () => disposed,
|
|
1677
|
+
refuse(name, message) {
|
|
1678
|
+
// Said until the field changes from what it holds, as a refused save's words are.
|
|
1679
|
+
serverErrors[name] = { message, value: JSON.stringify(state.values[name] ?? null) };
|
|
1680
|
+
set({ errors: { ...state.errors, [name]: message } });
|
|
1681
|
+
},
|
|
1682
|
+
});
|
|
1683
|
+
limitsKit = kit;
|
|
1684
|
+
kit.show();
|
|
1685
|
+
if (kit.asks() && (state.recordId == null || loadedOnce))
|
|
1686
|
+
void track(kit.ask());
|
|
1687
|
+
return kit;
|
|
1688
|
+
}
|
|
1689
|
+
/** Ask the source's `limits` again and show what it tells; without the limits part, nothing to ask. */
|
|
1690
|
+
const askLimits = () => (limitsKit ? limitsKit.ask() : Promise.resolve());
|
|
1691
|
+
/** Whether the form asks the source for counts. */
|
|
1692
|
+
const asksLimits = () => !!limitsKit?.asks();
|
|
1693
|
+
/**
|
|
1694
|
+
* Whether the page takes these answers now, asked of the source again just before they go — the limits part waited
|
|
1695
|
+
* for where the form keeps limits: why not, as a save's problem, or null.
|
|
1696
|
+
*/
|
|
1697
|
+
async function stillTaking(asked) {
|
|
1698
|
+
const kit = await limitsCode();
|
|
1699
|
+
return kit ? kit.stillTaking(asked) : null;
|
|
1127
1700
|
}
|
|
1701
|
+
/** A choice of a full option, picked by the person, refused under its field: the field keeps what it held. */
|
|
1702
|
+
const refusedFull = (name, value) => !!limitsKit?.refusedFull(name, value);
|
|
1128
1703
|
function scheduleAutosave() {
|
|
1129
1704
|
const autosave = options.autosave;
|
|
1130
1705
|
// A record the person may not change is never saved unasked: it would only be refused.
|
|
@@ -1265,6 +1840,20 @@ function innerForm(options, within, place, outer) {
|
|
|
1265
1840
|
await waiting;
|
|
1266
1841
|
if (asked !== generation)
|
|
1267
1842
|
return elsewhere();
|
|
1843
|
+
// Options by condition waited for where they have not come: what is saved never holds one they would take out.
|
|
1844
|
+
if (!optionKit && hasOptionRules(page)) {
|
|
1845
|
+
await optionsCode();
|
|
1846
|
+
if (asked !== generation)
|
|
1847
|
+
return elsewhere();
|
|
1848
|
+
}
|
|
1849
|
+
// Values the app's functions are asked for are waited for — those that failed asked again — so what is saved is worked out.
|
|
1850
|
+
if (asking.size || Object.values(state.asked).some((found) => found.failed !== undefined)) {
|
|
1851
|
+
set({ status: 'saving', error: null, saveProblem: null });
|
|
1852
|
+
await askAgain();
|
|
1853
|
+
if (asked !== generation)
|
|
1854
|
+
return elsewhere();
|
|
1855
|
+
set({ status: 'ready' });
|
|
1856
|
+
}
|
|
1268
1857
|
// 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.
|
|
1269
1858
|
if (filesGoing()) {
|
|
1270
1859
|
set({ status: 'saving', error: null, saveProblem: null });
|
|
@@ -1286,6 +1875,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1286
1875
|
return { reason: 'save', ...(before.message !== undefined ? { message: before.message } : {}) };
|
|
1287
1876
|
}
|
|
1288
1877
|
}
|
|
1878
|
+
// Whether the page still takes answers — its dates, its counts asked again — and each choice held still has room.
|
|
1879
|
+
const refused = await stillTaking(asked);
|
|
1880
|
+
if (refused)
|
|
1881
|
+
return refused;
|
|
1289
1882
|
// A file that did not reach the store stops it, under its field, with the form's own checks.
|
|
1290
1883
|
const errors = { ...collectErrors(), ...fileProblems() };
|
|
1291
1884
|
if (Object.keys(errors).length) {
|
|
@@ -1295,6 +1888,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1295
1888
|
const source = options.dataSource;
|
|
1296
1889
|
serverErrors = {};
|
|
1297
1890
|
set({ status: 'saving', errors: {}, error: null, saveProblem: null, conflict: null });
|
|
1891
|
+
// A draft being kept on the server goes first, so the send names the draft it ends; none is kept after it.
|
|
1892
|
+
if (draftTimer !== null)
|
|
1893
|
+
scheduler.clearTimeout(draftTimer);
|
|
1894
|
+
draftTimer = null;
|
|
1895
|
+
if (keeping)
|
|
1896
|
+
await draftGoing;
|
|
1897
|
+
const draft = serverKey ? { draft: serverKey } : {};
|
|
1298
1898
|
/** The generation the record was in when this save went to the source. */
|
|
1299
1899
|
let at = generation;
|
|
1300
1900
|
try {
|
|
@@ -1313,8 +1913,10 @@ function innerForm(options, within, place, outer) {
|
|
|
1313
1913
|
const sent = shownValues();
|
|
1314
1914
|
const before = state.values;
|
|
1315
1915
|
at = generation;
|
|
1916
|
+
const resetAt = resets;
|
|
1316
1917
|
const seen = botReport();
|
|
1317
|
-
|
|
1918
|
+
// What the source answered: the id it gave the response, and the values it added — what an ending reads.
|
|
1919
|
+
const answer = (await source.submit({ pageId: page.id, values: sent, ...(seen ? { bot: seen } : {}), ...draft })) ?? {};
|
|
1318
1920
|
// The captcha's token is spent once sent: the next send waits for a new one.
|
|
1319
1921
|
if (seen?.captcha)
|
|
1320
1922
|
bot.captcha = null;
|
|
@@ -1324,8 +1926,13 @@ function innerForm(options, within, place, outer) {
|
|
|
1324
1926
|
forgetDraft();
|
|
1325
1927
|
// What it sent is what an answer changed since is told against: still to send.
|
|
1326
1928
|
sentAs = before;
|
|
1327
|
-
|
|
1929
|
+
const added = answer.values && typeof answer.values === 'object' ? structuredCopy(answer.values) : {};
|
|
1930
|
+
// Started again meanwhile (`reset`): a new response is on screen, and no ending is the old one's.
|
|
1931
|
+
set({ status: 'saved', unsent: unsentNow(), ...(resets === resetAt ? { sent: { id: answer.id ?? null, values: { ...structuredCopy(sent), ...added } } } : {}) });
|
|
1328
1932
|
emit('send', { values: sent, ...(seen ? { bot: seen } : {}) });
|
|
1933
|
+
// Counted again: one response fewer to take, this person's sent.
|
|
1934
|
+
if (asksLimits())
|
|
1935
|
+
void track(askLimits());
|
|
1329
1936
|
void track(moment('on.afterSave', page.on?.afterSave, chain, false));
|
|
1330
1937
|
return null;
|
|
1331
1938
|
}
|
|
@@ -1343,6 +1950,7 @@ function innerForm(options, within, place, outer) {
|
|
|
1343
1950
|
values: sentValues(),
|
|
1344
1951
|
// The version it was opened at, for a source that refuses a save over someone else's.
|
|
1345
1952
|
...(state.recordId != null && state.version ? { version: state.version } : {}),
|
|
1953
|
+
...draft,
|
|
1346
1954
|
});
|
|
1347
1955
|
// Answered for a record no longer shown — another opened, or this one loaded again: the source has it, and the
|
|
1348
1956
|
// form lays nothing of it over the record it shows now.
|
|
@@ -1618,7 +2226,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1618
2226
|
return;
|
|
1619
2227
|
// Lines load in their tables' orders; where a person moves them after, they stay.
|
|
1620
2228
|
baseline = syncParts(startFrom(ordered({ ...initialValues(fields, today()), ...loaded })), 'start');
|
|
1621
|
-
set({ status: 'ready', values: structuredCopy(baseline), dirty: [], errors: {}, warnings: withValues(baseline, collectWarnings), version: version ?? null, can: can ?? null, unread: {} });
|
|
2229
|
+
set({ status: 'ready', values: structuredCopy(baseline), dirty: [], errors: {}, warnings: withValues(baseline, collectWarnings), version: version ?? null, can: can ?? null, unread: {}, takenOut: {} });
|
|
2230
|
+
// Each choice's places, for this record: its own choices are its own.
|
|
2231
|
+
loadedOnce = true;
|
|
2232
|
+
if (asksLimits())
|
|
2233
|
+
void track(askLimits());
|
|
1622
2234
|
if (again)
|
|
1623
2235
|
return;
|
|
1624
2236
|
offerDraft();
|
|
@@ -1647,7 +2259,11 @@ function innerForm(options, within, place, outer) {
|
|
|
1647
2259
|
told = null;
|
|
1648
2260
|
showing = false;
|
|
1649
2261
|
clearFiles();
|
|
1650
|
-
|
|
2262
|
+
// The draft kept for the record shown before is that one's.
|
|
2263
|
+
found = null;
|
|
2264
|
+
serverKey = null;
|
|
2265
|
+
draftEpoch++;
|
|
2266
|
+
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 };
|
|
1651
2267
|
if (id != null) {
|
|
1652
2268
|
set({ ...fresh, recordId: id, status: 'idle' });
|
|
1653
2269
|
await load();
|
|
@@ -1657,6 +2273,8 @@ function innerForm(options, within, place, outer) {
|
|
|
1657
2273
|
baseline = startFrom(startingValues(true, false));
|
|
1658
2274
|
const values = syncParts(structuredCopy(baseline), 'start');
|
|
1659
2275
|
set({ ...fresh, recordId: null, status: 'ready', values, warnings: withValues(values, collectWarnings) });
|
|
2276
|
+
if (asksLimits())
|
|
2277
|
+
void track(askLimits());
|
|
1660
2278
|
offerDraft();
|
|
1661
2279
|
await opening();
|
|
1662
2280
|
return true;
|
|
@@ -2017,10 +2635,18 @@ function innerForm(options, within, place, outer) {
|
|
|
2017
2635
|
}
|
|
2018
2636
|
const form = {
|
|
2019
2637
|
page,
|
|
2638
|
+
...(table ? { functions: table } : {}),
|
|
2020
2639
|
getState: () => state,
|
|
2021
2640
|
subscribe(listener) {
|
|
2022
2641
|
listeners.add(listener);
|
|
2023
|
-
|
|
2642
|
+
// Listened to: the page opening or closing is said as it comes.
|
|
2643
|
+
if (listeners.size === 1)
|
|
2644
|
+
limitsKit?.heard(true);
|
|
2645
|
+
return () => {
|
|
2646
|
+
listeners.delete(listener);
|
|
2647
|
+
if (!listeners.size)
|
|
2648
|
+
limitsKit?.heard(false);
|
|
2649
|
+
};
|
|
2024
2650
|
},
|
|
2025
2651
|
load() {
|
|
2026
2652
|
const go = letGo();
|
|
@@ -2028,6 +2654,9 @@ function innerForm(options, within, place, outer) {
|
|
|
2028
2654
|
},
|
|
2029
2655
|
setValue(name, value, how) {
|
|
2030
2656
|
fieldDef(name);
|
|
2657
|
+
// A choice whose places are all taken is shown, and refused when the person picks it: said under its field.
|
|
2658
|
+
if ((how?.by ?? 'person') === 'person' && refusedFull(name, value))
|
|
2659
|
+
return;
|
|
2031
2660
|
writeValues({ ...state.values, [name]: structuredCopy(value) });
|
|
2032
2661
|
afterEdit(name);
|
|
2033
2662
|
changed(name, how?.by ?? 'person');
|
|
@@ -2053,11 +2682,34 @@ function innerForm(options, within, place, outer) {
|
|
|
2053
2682
|
// Nothing of the record changed; what shows of it may have.
|
|
2054
2683
|
set({});
|
|
2055
2684
|
},
|
|
2056
|
-
fieldReadonly: (name) => lockedField(fieldDef(name)) || state.can?.edit === false || state.can?.readonlyFields?.includes(name) === true,
|
|
2057
|
-
loadChoices,
|
|
2685
|
+
fieldReadonly: (name) => lockedField(fieldDef(name)) || state.can?.edit === false || state.can?.readonlyFields?.includes(name) === true || signedNow().has(name),
|
|
2686
|
+
loadChoices: (field) => loadChoices(field),
|
|
2058
2687
|
loadDefinitions,
|
|
2059
2688
|
addDefinition,
|
|
2689
|
+
lineChoices(field, key, subfield, values = {}) {
|
|
2690
|
+
const one = listedAt(field, subfield);
|
|
2691
|
+
const line = key === null ? one && { values: newLineValues(field) } : lineAt(field, key);
|
|
2692
|
+
if (!one || !line)
|
|
2693
|
+
throw new Error(`"${field}.${key}.${subfield}" is no line's choice from the app's lists`);
|
|
2694
|
+
return track(askLine(one, { ...line.values, ...values }));
|
|
2695
|
+
},
|
|
2696
|
+
offered(name) {
|
|
2697
|
+
const cell = cellOf(name);
|
|
2698
|
+
if (!cell)
|
|
2699
|
+
return page.fields[name]?.type === 'selection' ? (optionKit ? optionKit.offered(name, state.values, reading()) : everyOption(name)) : null;
|
|
2700
|
+
const lines = page.fields[cell.table];
|
|
2701
|
+
const line = lineAt(cell.table, cell.key);
|
|
2702
|
+
return lines?.type === 'one2many' && lines.fields[cell.column]?.type === 'selection' && line ? lineOffered(cell.table, line, cell.column, reading().context) : null;
|
|
2703
|
+
},
|
|
2704
|
+
offersChange(name) {
|
|
2705
|
+
const cell = cellOf(name);
|
|
2706
|
+
const lines = cell ? page.fields[cell.table] : undefined;
|
|
2707
|
+
return changesOptions(cell ? (lines?.type === 'one2many' ? lines.fields[cell.column] : undefined) : page.fields[name]);
|
|
2708
|
+
},
|
|
2060
2709
|
waitsFor(name) {
|
|
2710
|
+
const found = state.asked[name];
|
|
2711
|
+
if (found)
|
|
2712
|
+
return { field: name, asking: true, ...(found.failed !== undefined ? { failed: found.failed } : {}) };
|
|
2061
2713
|
const from = workedOut.get(name);
|
|
2062
2714
|
return from ? waitingOn(page, from, state.values, state.unread) : null;
|
|
2063
2715
|
},
|
|
@@ -2139,6 +2791,9 @@ function innerForm(options, within, place, outer) {
|
|
|
2139
2791
|
save: () => track(save()),
|
|
2140
2792
|
canUpload: () => typeof options.dataSource?.upload === 'function',
|
|
2141
2793
|
today,
|
|
2794
|
+
now: () => options.now?.() ?? new Date(),
|
|
2795
|
+
user: () => options.user ?? null,
|
|
2796
|
+
shownFields: () => [...new Set([...visibleFieldNodes(), ...visibleFieldNodes(undefined, 'form')].map((node) => answerOf(node)).filter((name) => !!name))],
|
|
2142
2797
|
recordReads: (id, values, fields) => recordContext(values, fields ?? page.fields, { recordId: id, user: options.user, editing: false, context: options.context }),
|
|
2143
2798
|
canFindSlots: () => typeof options.dataSource?.slots === 'function',
|
|
2144
2799
|
findSlots(field, ask) {
|
|
@@ -2173,6 +2828,22 @@ function innerForm(options, within, place, outer) {
|
|
|
2173
2828
|
const others = (matches ?? []).filter((match) => match && String(match.id) !== String(recordId));
|
|
2174
2829
|
return others.every((match) => typeof match.score === 'number') ? others.sort((a, b) => b.score - a.score) : others;
|
|
2175
2830
|
},
|
|
2831
|
+
keepsNotDuplicates: () => !!(page.data.kind === 'record' && options.dataSource?.notDuplicates && options.dataSource.markNotDuplicate),
|
|
2832
|
+
async notDuplicates() {
|
|
2833
|
+
const [source, model] = recordSource('notDuplicates');
|
|
2834
|
+
const recordId = state.recordId;
|
|
2835
|
+
if (recordId === null)
|
|
2836
|
+
return [];
|
|
2837
|
+
const ids = await source.notDuplicates({ model, recordId });
|
|
2838
|
+
return Array.isArray(ids) ? ids.filter((id) => (typeof id === 'string' || typeof id === 'number') && String(id) !== String(recordId)) : [];
|
|
2839
|
+
},
|
|
2840
|
+
async markNotDuplicate(matchId) {
|
|
2841
|
+
const [source, model] = recordSource('markNotDuplicate');
|
|
2842
|
+
const recordId = state.recordId;
|
|
2843
|
+
if (recordId === null)
|
|
2844
|
+
throw new Error('A new record has no id to keep it by');
|
|
2845
|
+
await source.markNotDuplicate({ model, recordId, matchId });
|
|
2846
|
+
},
|
|
2176
2847
|
canMerge: () => !!(page.data.kind === 'record' && options.dataSource?.merge && options.dataSource.load),
|
|
2177
2848
|
async readRecord(id) {
|
|
2178
2849
|
const [source, model] = recordSource('load');
|
|
@@ -2213,7 +2884,19 @@ function innerForm(options, within, place, outer) {
|
|
|
2213
2884
|
// Started again: a new response, timed from now.
|
|
2214
2885
|
bot = { ...bot, honeypot: false, since: clock() };
|
|
2215
2886
|
const values = syncParts(structuredCopy(baseline), 'start');
|
|
2216
|
-
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: {} });
|
|
2887
|
+
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 });
|
|
2888
|
+
// Started again: whether the page takes another response, and each choice's places, asked again.
|
|
2889
|
+
if (asksLimits())
|
|
2890
|
+
void track(askLimits());
|
|
2891
|
+
},
|
|
2892
|
+
words: (text) => wordsNow(text),
|
|
2893
|
+
ending: endingNow,
|
|
2894
|
+
endingWords(text) {
|
|
2895
|
+
const sent = state.sent;
|
|
2896
|
+
if (!sent)
|
|
2897
|
+
return wordsNow(text);
|
|
2898
|
+
// The response's id where the page has no field of that name: "Your booking is {id}".
|
|
2899
|
+
return wordsNow(text, sent.values, (name) => (name === 'id' && !page.fields['id'] ? String(sent.id ?? '') : undefined));
|
|
2217
2900
|
},
|
|
2218
2901
|
changes,
|
|
2219
2902
|
addLine: (field, values = {}) => addLine(field, values, 'person'),
|
|
@@ -2474,16 +3157,27 @@ function innerForm(options, within, place, outer) {
|
|
|
2474
3157
|
return () => void heard.delete(listener);
|
|
2475
3158
|
},
|
|
2476
3159
|
restoreDraft() {
|
|
2477
|
-
const
|
|
2478
|
-
if (
|
|
2479
|
-
|
|
2480
|
-
const draft = JSON.parse(raw);
|
|
2481
|
-
writeValues(withDraft(draft.values), { draft: null }, true);
|
|
3160
|
+
const kept = found ?? draftRead(options.drafts?.store.getItem(storeKey()) ?? null);
|
|
3161
|
+
if (kept && 'values' in kept)
|
|
3162
|
+
applyDraft(kept);
|
|
2482
3163
|
},
|
|
2483
3164
|
discardDraft() {
|
|
2484
3165
|
forgetDraft();
|
|
2485
3166
|
set({ draft: null });
|
|
2486
3167
|
},
|
|
3168
|
+
canContinueLater: () => page.continueLater === true && onServer() && page.layout.type !== 'list',
|
|
3169
|
+
async continueLater() {
|
|
3170
|
+
if (!form.canContinueLater())
|
|
3171
|
+
throw new Error('No continueLater, or no saveDraft and loadDraft');
|
|
3172
|
+
// 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.
|
|
3173
|
+
await (readTyped() ?? Promise.resolve());
|
|
3174
|
+
if (draftTimer !== null)
|
|
3175
|
+
scheduler.clearTimeout(draftTimer);
|
|
3176
|
+
draftTimer = null;
|
|
3177
|
+
const saved = await track(keepDraft(true));
|
|
3178
|
+
return saved;
|
|
3179
|
+
},
|
|
3180
|
+
refreshLimits: () => askLimits(),
|
|
2487
3181
|
async reloadTheirs() {
|
|
2488
3182
|
// A save on its way first: its answer is not dropped by the load.
|
|
2489
3183
|
const asked = shownNow();
|
|
@@ -2610,6 +3304,8 @@ function innerForm(options, within, place, outer) {
|
|
|
2610
3304
|
scheduler,
|
|
2611
3305
|
messages,
|
|
2612
3306
|
now: options.now,
|
|
3307
|
+
// Its words that show its answers read them as this form's do.
|
|
3308
|
+
...(options.valueWords ? { valueWords: options.valueWords } : {}),
|
|
2613
3309
|
}, chain,
|
|
2614
3310
|
// Its files are the record's: stored for it, under the part's name.
|
|
2615
3311
|
{ model: where.model, recordId: where.recordId, under: `${where.under}${part.name}.` },
|
|
@@ -2653,6 +3349,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2653
3349
|
scheduler.clearTimeout(draftTimer);
|
|
2654
3350
|
if (autosaveTimer !== null)
|
|
2655
3351
|
scheduler.clearTimeout(autosaveTimer);
|
|
3352
|
+
limitsKit?.dispose();
|
|
2656
3353
|
listeners.clear();
|
|
2657
3354
|
events.clear();
|
|
2658
3355
|
for (const part of attached.values())
|
|
@@ -2661,11 +3358,16 @@ function innerForm(options, within, place, outer) {
|
|
|
2661
3358
|
};
|
|
2662
3359
|
// A record's starting values may already break a warning rule; nothing listens yet.
|
|
2663
3360
|
state = { ...state, warnings: collectWarnings() };
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
3361
|
+
// When the page takes answers — at once where the limits part has come, else as soon as it does — its counts and each
|
|
3362
|
+
// choice's places as the source tells them: a record's own once it is loaded, its own choices not counted against it.
|
|
3363
|
+
if (keepsLimits())
|
|
3364
|
+
void track(limitsCode());
|
|
3365
|
+
// A new form has its values now — a draft continued from its key once it is laid — and opens once whoever made it
|
|
3366
|
+
// has had the chance to listen.
|
|
3367
|
+
if (state.recordId == null) {
|
|
3368
|
+
const continued = offerDraft();
|
|
3369
|
+
void track(Promise.resolve(continued).then(opening));
|
|
3370
|
+
}
|
|
2669
3371
|
return {
|
|
2670
3372
|
form,
|
|
2671
3373
|
take(values, how) {
|
|
@@ -2675,7 +3377,7 @@ function innerForm(options, within, place, outer) {
|
|
|
2675
3377
|
filed.clear();
|
|
2676
3378
|
baseline = startFrom({ ...initialValues(page.fields, today()), ...structuredCopy(values) });
|
|
2677
3379
|
const start = syncParts(structuredCopy(baseline), 'start');
|
|
2678
|
-
set({ values: start, dirty: [], errors: {}, warnings: withValues(start, collectWarnings), warning: null, warningField: null, unread: {} });
|
|
3380
|
+
set({ values: start, dirty: [], errors: {}, warnings: withValues(start, collectWarnings), warning: null, warningField: null, unread: {}, takenOut: {} });
|
|
2679
3381
|
},
|
|
2680
3382
|
errorsNow: () => collectErrors(),
|
|
2681
3383
|
typing: typingNow,
|
|
@@ -2699,6 +3401,8 @@ function innerForm(options, within, place, outer) {
|
|
|
2699
3401
|
const sameHow = (a, b) => !!a.changes === !!b.changes && !!a.discard === !!b.discard;
|
|
2700
3402
|
/** Two lists of names the same, in the same order. */
|
|
2701
3403
|
const sameList = (a, b) => a.length === b.length && a.every((name, i) => name === b[i]);
|
|
3404
|
+
/** Whether what waits for an app's answer is the same as before: the same paths, each waiting or failed for the same reason. */
|
|
3405
|
+
const sameAsked = (shown, now) => Object.keys(shown).length === now.size && [...now].every(([path, found]) => shown[path] !== undefined && shown[path].failed === found.failed);
|
|
2702
3406
|
/** 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. */
|
|
2703
3407
|
function underPart(name, upload) {
|
|
2704
3408
|
return { ...upload, id: `${name}/${upload.id}`, ...(upload.line ? { line: { ...upload.line, field: `${name}.${upload.line.field}` } } : { field: `${name}.${upload.field}` }) };
|
|
@@ -2809,7 +3513,7 @@ function linkOps(before, now) {
|
|
|
2809
3513
|
...before.filter((r) => !has.has(r.id)).map((r) => ({ op: 'unlink', id: r.id })),
|
|
2810
3514
|
];
|
|
2811
3515
|
}
|
|
2812
|
-
function indexLayout(root) {
|
|
3516
|
+
function indexLayout(root, functions) {
|
|
2813
3517
|
const index = new Map();
|
|
2814
3518
|
const add = (source, parent, kind, modifiers) => {
|
|
2815
3519
|
const roles = source.roles;
|
|
@@ -2821,10 +3525,10 @@ function indexLayout(root) {
|
|
|
2821
3525
|
kind,
|
|
2822
3526
|
field: modifiers.field,
|
|
2823
3527
|
...(roles?.length ? { roles } : {}),
|
|
2824
|
-
invisible: compileModifier(modifiers.invisible),
|
|
2825
|
-
readonly: compileModifier(modifiers.readonly),
|
|
2826
|
-
required: compileModifier(modifiers.required),
|
|
2827
|
-
...(pressed !== undefined ? { pressed: compileModifier(pressed) } : {}),
|
|
3528
|
+
invisible: compileModifier(modifiers.invisible, functions),
|
|
3529
|
+
readonly: compileModifier(modifiers.readonly, functions),
|
|
3530
|
+
required: compileModifier(modifiers.required, functions),
|
|
3531
|
+
...(pressed !== undefined ? { pressed: compileModifier(pressed, functions) } : {}),
|
|
2828
3532
|
...(does.length ? { does } : {}),
|
|
2829
3533
|
source,
|
|
2830
3534
|
});
|