pict-section-form 1.0.196 → 1.0.198
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/docs/Comprehensions.md +181 -0
- package/docs/Comprehensions_Advanced.md +295 -0
- package/docs/Configuration.md +17 -0
- package/docs/Layouts.md +170 -0
- package/docs/Pict_Section_Form_Architecture.md +2 -2
- package/docs/README.md +1 -1
- package/docs/Solvers.md +66 -8
- package/docs/_brand.json +18 -0
- package/docs/_cover.md +1 -1
- package/docs/_sidebar.md +2 -0
- package/docs/_version.json +7 -0
- package/docs/examples/README.md +22 -16
- package/docs/examples/gradebook/README.md +298 -150
- package/docs/index.html +6 -7
- package/docs/input_providers/005-precise-number.md +5 -5
- package/docs/input_providers/009-chart.md +1 -1
- package/docs/input_providers/011-autofill-trigger-group.md +4 -4
- package/docs/input_providers/013-tab-section-selector.md +1 -1
- package/docs/input_providers/README.md +2 -2
- package/docs/retold-catalog.json +45 -5
- package/docs/retold-keyword-index.json +6763 -4709
- package/example_applications/authortopia/html/index.html +5 -5
- package/example_applications/change_tracking/html/index.html +4 -4
- package/example_applications/complex_table/.claude/launch.json +11 -0
- package/example_applications/complex_table/Complex-Tabular-Application.js +31 -0
- package/example_applications/complex_table/html/index.html +5 -5
- package/example_applications/complex_tuigrid/html/index.html +4 -4
- package/example_applications/dynamic_analysis/html/index.html +7 -7
- package/example_applications/gradebook/.quackage.json +9 -0
- package/example_applications/gradebook/Gradebook-Application.js +441 -0
- package/example_applications/gradebook/GradebookData.json +44 -0
- package/example_applications/gradebook/html/index.html +95 -0
- package/example_applications/gradebook/package.json +26 -0
- package/example_applications/ndt_field_test/html/index.html +9 -9
- package/example_applications/postcard_example/css/postcard.css +12 -12
- package/example_applications/postcard_example/css/pure.min.css +1 -1
- package/example_applications/scope_mathematics/Scope-Mathematics_Manifest.json +1 -1
- package/example_applications/scope_mathematics/html/index.html +4 -4
- package/example_applications/simple_distill/html/index.html +4 -4
- package/example_applications/simple_form/html/index.html +4 -4
- package/example_applications/simple_table/html/index.html +4 -4
- package/package.json +7 -6
- package/source/providers/Pict-Provider-DynamicFormSolverBehaviors.js +326 -1
- package/source/providers/Pict-Provider-DynamicTemplates.js +4 -0
- package/source/providers/dynamictemplates/Pict-DynamicTemplates-DefaultFormTemplates-ReadOnly.js +4 -4
- package/source/providers/dynamictemplates/Pict-DynamicTemplates-DefaultFormTemplates.js +54 -4
- package/source/providers/layouts/Pict-Layout-Tabular.js +936 -4
- package/source/services/ManifestFactory.js +276 -0
- package/source/templates/Pict-Template-TabularEditingControls.js +58 -0
- package/source/templates/Pict-Template-TabularRowLabels.js +60 -0
- package/source/views/Pict-View-Form-Metacontroller.js +8 -0
- package/source/views/support/Pict-Provider-PSF-Support.js +12 -12
- package/test/PictSectionForm-Basic_tests.js +138 -0
- package/test/PictSectionForm-Tabular-Features_tests.js +960 -0
- package/docs/css/docuserve.css +0 -73
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Comprehensions
|
|
2
|
+
|
|
3
|
+
The `addComprehensionEntity` solver function builds **multi-context, multi-entity
|
|
4
|
+
comprehensions** from form data — a JSON shape that can be inspected, diffed,
|
|
5
|
+
and pushed to a Meadow REST API via
|
|
6
|
+
[`meadow-integration load_comprehension`](https://github.com/stevenvelozo/meadow-integration).
|
|
7
|
+
|
|
8
|
+
Think of it as the "save side" of a form: a single function call lays down one
|
|
9
|
+
property of one record under one workflow context, and many calls compose into a
|
|
10
|
+
single nested tree that a downstream pipeline can read in one go.
|
|
11
|
+
|
|
12
|
+
## Signature
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
addComprehensionEntity(Context, Entity, GUID, Property, Value)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Parameter | Type | Meaning |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `Context` | string (manyfest address) | Workflow bucket — `"OnSave"`, `"OnApprovalAction.Approve"`, etc. Dots create nested branches. |
|
|
21
|
+
| `Entity` | string | The entity name — `"Book"`, `"Recipe"`, `"Fruit"`. Opaque key (not parsed). |
|
|
22
|
+
| `GUID` | string | External GUID for the record. Opaque key (dots are NOT interpreted). |
|
|
23
|
+
| `Property` | string | The field to set on the record. Opaque key. |
|
|
24
|
+
| `Value` | any | The value to write. Strings, numbers, booleans, objects, arrays. |
|
|
25
|
+
|
|
26
|
+
Successive calls to the same `(Context, Entity, GUID)` **accumulate properties**
|
|
27
|
+
on the same record. Successive calls to the same
|
|
28
|
+
`(Context, Entity, GUID, Property)` **overwrite**.
|
|
29
|
+
|
|
30
|
+
## The shape it builds
|
|
31
|
+
|
|
32
|
+
Given these solvers on a Book form:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
"Solvers":
|
|
36
|
+
[
|
|
37
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, "Title", BookTitle)`,
|
|
38
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, "Author", BookAuthor)`,
|
|
39
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, "ISBN", BookISBN)`,
|
|
40
|
+
`addComprehensionEntity("OnApprovalAction.Submit", "Book", BookGUID, "Status", "Submitted")`,
|
|
41
|
+
`addComprehensionEntity("OnApprovalAction.Approve", "Book", BookGUID, "Status", "Approved")`
|
|
42
|
+
]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
…the destination ends up looking like:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"OnSave": {
|
|
50
|
+
"Book": {
|
|
51
|
+
"0x73278432987": {
|
|
52
|
+
"Title": "The Giving Tree",
|
|
53
|
+
"Author": "Shel Silverstein",
|
|
54
|
+
"ISBN": "8675309"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"OnApprovalAction": {
|
|
59
|
+
"Submit": {
|
|
60
|
+
"Book": {
|
|
61
|
+
"0x73278432987": { "Status": "Submitted" }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"Approve": {
|
|
65
|
+
"Book": {
|
|
66
|
+
"0x73278432987": { "Status": "Approved" }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Every leaf in this tree is a Meadow-shaped record keyed by external GUID — the
|
|
74
|
+
exact format [`load_comprehension`](https://github.com/stevenvelozo/meadow-integration)
|
|
75
|
+
expects.
|
|
76
|
+
|
|
77
|
+
## Where the result lands
|
|
78
|
+
|
|
79
|
+
By default the tree is written to `AppData.FormEntityComprehensions`.
|
|
80
|
+
|
|
81
|
+
The destination is **a manyfest address** resolved against the pict instance, so
|
|
82
|
+
addresses like `AppData.X.Y`, `Bundle.X`, etc. all work. Change it on the
|
|
83
|
+
metacontroller:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
// At any point after the metacontroller is registered (i.e. inside the
|
|
87
|
+
// application's constructor, after super() has run):
|
|
88
|
+
this.pict.views.PictFormMetacontroller.comprehensionDestinationAddress = 'AppData.MyWorkflowComprehensions';
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
…or pass it in the metacontroller view options if you're constructing the
|
|
92
|
+
metacontroller manually:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
pict.addView('PictFormMetacontroller', { ComprehensionDestinationAddress: 'AppData.MyWorkflowComprehensions' },
|
|
96
|
+
libPictSectionForm.PictFormMetacontroller);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If the address resolves to nothing, the function materializes an object there on
|
|
100
|
+
the first write. If it resolves to a non-object scalar, the call logs a warning
|
|
101
|
+
and bails (it won't overwrite a number with an object).
|
|
102
|
+
|
|
103
|
+
## Basic example — flat OnSave context
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
"Sections":
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
"Hash": "BookEditor",
|
|
110
|
+
"Solvers":
|
|
111
|
+
[
|
|
112
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, "Title", BookTitle)`,
|
|
113
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, "Author", BookAuthor)`,
|
|
114
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, "Status", "New")`
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
After a solve, `AppData.FormEntityComprehensions.OnSave.Book[<BookGUID>]` holds
|
|
121
|
+
the three properties.
|
|
122
|
+
|
|
123
|
+
## Quick gotchas
|
|
124
|
+
|
|
125
|
+
1. **Empty GUIDs bail.** If any of `Context`, `Entity`, `GUID`, or `Property`
|
|
126
|
+
resolves to `null`, `undefined`, or the empty string, the call logs a warning
|
|
127
|
+
and returns `undefined`. Recipes with an empty `RecipeName` will not silently
|
|
128
|
+
create a `""` bucket — they just no-op until the user fills the name in.
|
|
129
|
+
2. **Solver ordinals.** Solvers run in ascending ordinal order. Put your
|
|
130
|
+
`addComprehensionEntity` calls *after* any solvers they depend on (e.g. after
|
|
131
|
+
the `TotalCalories = SUM(...)` aggregate they read from). The complex_table
|
|
132
|
+
example uses ordinals 200–220 to keep them after the default-ordinal compute
|
|
133
|
+
solvers.
|
|
134
|
+
3. **Re-solves overwrite.** Each solve re-runs every solver, so each
|
|
135
|
+
`addComprehensionEntity` call overwrites the property it wrote last time
|
|
136
|
+
with the current value. This is what you want — the comprehension always
|
|
137
|
+
reflects the current form state.
|
|
138
|
+
4. **The destination is *not* cleared between solves.** If your form removes a
|
|
139
|
+
record (e.g. deletes a row from a grid), the previous comprehension for that
|
|
140
|
+
record stays behind. If that matters for your workflow, reset the destination
|
|
141
|
+
at the start of the solve cycle (`AppData.FormEntityComprehensions = {}`) or
|
|
142
|
+
in `marshalFromView` before the comprehension solvers fire.
|
|
143
|
+
|
|
144
|
+
## Pushing the result
|
|
145
|
+
|
|
146
|
+
Once the comprehension is built, push it via meadow-integration:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// Browser-side -- POST the AppData blob directly to the Comprehension/Push REST endpoint.
|
|
150
|
+
fetch('/1.0/Comprehension/Push',
|
|
151
|
+
{
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify(
|
|
155
|
+
{
|
|
156
|
+
Comprehension: pict.AppData.FormEntityComprehensions.OnSave,
|
|
157
|
+
GUIDPrefix: 'MYAPP'
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Or write to a file and push from a CLI:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npx meadow-integration load_comprehension out.json --prefix MYAPP
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
See [meadow-integration: Comprehensions](https://github.com/stevenvelozo/meadow-integration/blob/main/docs/comprehensions.md)
|
|
169
|
+
for the full push semantics — GUID marshaling, foreign-key resolution, batch
|
|
170
|
+
upserts, idempotency.
|
|
171
|
+
|
|
172
|
+
## See also
|
|
173
|
+
|
|
174
|
+
- [Advanced patterns](Comprehensions_Advanced.md) — mixing hashes and direct
|
|
175
|
+
addresses, computed contexts, per-row `MAP VAR` generation, customized
|
|
176
|
+
destinations.
|
|
177
|
+
- [Solvers](Solvers.md) — full solver function reference.
|
|
178
|
+
- The [Complex Table example](../example_applications/complex_table/Complex-Tabular-Application.js)
|
|
179
|
+
builds a complete `RecipeWorkflowComprehensions` tree with `OnSave` and
|
|
180
|
+
`OnApprovalAction.{Submit,Approve}` contexts off the Recipe section and the
|
|
181
|
+
FruitGrid recordset.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Comprehensions — Advanced patterns
|
|
2
|
+
|
|
3
|
+
This document goes deeper than [Comprehensions](Comprehensions.md):
|
|
4
|
+
|
|
5
|
+
- **Hash vs. address arguments** — when to write `BookGUID` (bare symbol),
|
|
6
|
+
`"AppData.Bundle.BookGUID"` (string address), `getvalue("...")` (explicit
|
|
7
|
+
lookup), and when to mix them.
|
|
8
|
+
- **Computed contexts** — `IF`/ternary results as the `Context` argument so a
|
|
9
|
+
single solver routes between `OnApprovalAction.Submit` and
|
|
10
|
+
`OnApprovalAction.Approve`.
|
|
11
|
+
- **Per-row generation** with `MAP VAR` over a recordset.
|
|
12
|
+
- **Customized destinations** at the metacontroller level.
|
|
13
|
+
- **Resetting between solves**.
|
|
14
|
+
|
|
15
|
+
The complete worked example for everything here lives at
|
|
16
|
+
[`example_applications/complex_table/Complex-Tabular-Application.js`](../example_applications/complex_table/Complex-Tabular-Application.js)
|
|
17
|
+
— if you only read one thing, read that file. This page explains the *why* behind
|
|
18
|
+
the patterns it uses.
|
|
19
|
+
|
|
20
|
+
## Argument resolution — hashes, addresses, and quoted strings
|
|
21
|
+
|
|
22
|
+
The solver expression parser treats each argument to `addComprehensionEntity`
|
|
23
|
+
the same way it would treat any other function argument:
|
|
24
|
+
|
|
25
|
+
| Argument form | What happens |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `BookGUID` | **Bare symbol** — resolved from the form's manifest. Looked up first by descriptor hash, then by address against the marshal destination. |
|
|
28
|
+
| `Record.GUID` / `AppData.Bundle.X` | **Dotted symbol** — resolved as an address (the parser does NOT need quotes around addresses). |
|
|
29
|
+
| `"OnSave"` / `"Book"` | **Quoted string** — taken literally, no resolution. |
|
|
30
|
+
| `getvalue("AppData.X.Y")` | **Explicit lookup** — useful when you want to force address-resolution semantics on a value built up from other solvers. |
|
|
31
|
+
| `IF(...)` / `CONCAT(...)` | **Nested function call** — the inner function's return value becomes the argument. |
|
|
32
|
+
|
|
33
|
+
In practice you mix freely:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
"Solvers":
|
|
37
|
+
[
|
|
38
|
+
// Context and Property are literals; Entity is a literal; GUID and Value
|
|
39
|
+
// are resolved from form data.
|
|
40
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, "Title", BookTitle)`,
|
|
41
|
+
|
|
42
|
+
// Same shape, but the GUID and Value come from absolute addresses rather
|
|
43
|
+
// than descriptor hashes. Useful if the descriptor hashes haven't been
|
|
44
|
+
// wired up or the data lives outside the form.
|
|
45
|
+
`addComprehensionEntity("OnSave", "Book", AppData.SelectedBook.IDBook, "Status", AppData.SelectedBook.Status)`,
|
|
46
|
+
|
|
47
|
+
// Pull a value through a getvalue() call -- equivalent to the line above
|
|
48
|
+
// but with explicit resolution syntax. Use this form when an inner
|
|
49
|
+
// expression already produces an address string and you want to evaluate it.
|
|
50
|
+
`addComprehensionEntity("OnSave", "Book", getvalue("AppData.SelectedBook.IDBook"), "Status", getvalue("AppData.SelectedBook.Status"))`,
|
|
51
|
+
|
|
52
|
+
// The property name itself is computed. CONCAT returns a string, which
|
|
53
|
+
// becomes the Property argument.
|
|
54
|
+
`addComprehensionEntity("OnSave", "Book", BookGUID, CONCAT("Field_", FieldType), FieldValue)`
|
|
55
|
+
]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Rule of thumb:** quote when you want a literal, leave unquoted when you want
|
|
59
|
+
the parser to look the symbol up. The first three arguments are almost always
|
|
60
|
+
literal strings (Context, Entity name) plus one resolved value (GUID); the
|
|
61
|
+
fourth is almost always a literal Property; the fifth is almost always resolved.
|
|
62
|
+
|
|
63
|
+
## Computed contexts — routing with `IF`
|
|
64
|
+
|
|
65
|
+
The `Context` argument is a manyfest address. It's also just a string that the
|
|
66
|
+
function uses to walk a nested object — which means a *computed* string works
|
|
67
|
+
fine. The complex_table example routes between `OnApprovalAction.Submit` and
|
|
68
|
+
`OnApprovalAction.Approve` based on a `Proprietary` boolean:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
{ Ordinal: 220, Expression:
|
|
72
|
+
`addComprehensionEntity(
|
|
73
|
+
IF(Proprietary, "==", 1, "OnApprovalAction.Submit", "OnApprovalAction.Approve"),
|
|
74
|
+
"Recipe",
|
|
75
|
+
RecipeName,
|
|
76
|
+
"Status",
|
|
77
|
+
IF(Proprietary, "==", 1, "Submitted", "Approved")
|
|
78
|
+
)`
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Both `Submit` and `Approve` branches sit under `OnApprovalAction`, which lets
|
|
83
|
+
downstream code key off `Object.keys(comprehension.OnApprovalAction)` to discover
|
|
84
|
+
which actions fired this solve.
|
|
85
|
+
|
|
86
|
+
The same trick scales to richer routing — e.g. context-per-environment:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
`addComprehensionEntity(
|
|
90
|
+
CONCAT("OnSave.", EnvironmentName),
|
|
91
|
+
"Recipe", RecipeName, "Status", "Saved"
|
|
92
|
+
)`
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This produces `OnSave.Production.Recipe.<name>.Status` or
|
|
96
|
+
`OnSave.Staging.Recipe.<name>.Status` depending on the value of
|
|
97
|
+
`EnvironmentName`.
|
|
98
|
+
|
|
99
|
+
## Per-row generation with `MAP VAR`
|
|
100
|
+
|
|
101
|
+
`MAP VAR` iterates a recordset and fires the body expression once per row, with
|
|
102
|
+
the row bound to a name you choose (`row` is the convention). Combined with
|
|
103
|
+
`addComprehensionEntity`, this fans one solver across an entire grid:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
// From complex_table -- the Recipe section's solvers reach into the FruitGrid
|
|
107
|
+
// recordset and emit one OnSave.Fruit.<name>.<property> entry per (fruit, property)
|
|
108
|
+
// pair. Three MAP VARs produce 3 * N comprehension writes in a single solve.
|
|
109
|
+
{ Ordinal: 210, Expression: `MAP VAR row FROM FruitData.FruityVice : addComprehensionEntity("OnSave", "Fruit", row.name, "Family", row.family)` },
|
|
110
|
+
{ Ordinal: 210, Expression: `MAP VAR row FROM FruitData.FruityVice : addComprehensionEntity("OnSave", "Fruit", row.name, "Order", row.order)` },
|
|
111
|
+
{ Ordinal: 210, Expression: `MAP VAR row FROM FruitData.FruityVice : addComprehensionEntity("OnSave", "Fruit", row.name, "Calories", row.nutritions.calories)` }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Inside the body, `row.X.Y` resolves against each row in turn — so you get
|
|
115
|
+
deep-property access without writing per-row solvers.
|
|
116
|
+
|
|
117
|
+
After the solve runs against the bundled FruityVice data, the comprehension at
|
|
118
|
+
`AppData.RecipeWorkflowComprehensions.OnSave.Fruit` looks like:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"Apple": { "Family": "Rosaceae", "Order": "Rosales", "Calories": "52" },
|
|
123
|
+
"Banana": { "Family": "Musaceae", "Order": "Zingiberales", "Calories": "96" },
|
|
124
|
+
"Mango": { "Family": "Anacardiaceae", "Order": "Sapindales", "Calories": "60" },
|
|
125
|
+
...
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
(49 fruits total in the complex_table dataset.)
|
|
130
|
+
|
|
131
|
+
If you need one comprehension write per row that touches *every* property of the
|
|
132
|
+
row, you have two reasonable options:
|
|
133
|
+
|
|
134
|
+
1. Multiple `MAP VAR` solvers, one per property (as above). Clear and easy to
|
|
135
|
+
audit.
|
|
136
|
+
2. A single helper solver function registered on
|
|
137
|
+
`DynamicFormSolverBehaviors` that takes a row + property list and calls
|
|
138
|
+
`addComprehensionEntity` internally. Worth the indirection only if you have
|
|
139
|
+
many entities with many properties; otherwise just write the explicit
|
|
140
|
+
`MAP VAR`s.
|
|
141
|
+
|
|
142
|
+
## Customizing the destination
|
|
143
|
+
|
|
144
|
+
Each metacontroller has a `comprehensionDestinationAddress` property —
|
|
145
|
+
mirroring the existing `viewMarshalDestination` knob — that controls where
|
|
146
|
+
`addComprehensionEntity` writes. The default is `AppData.FormEntityComprehensions`.
|
|
147
|
+
|
|
148
|
+
### Option 1: in the application constructor
|
|
149
|
+
|
|
150
|
+
This is what the [complex_table example](../example_applications/complex_table/Complex-Tabular-Application.js)
|
|
151
|
+
does. After `super()` (which registers the metacontroller view via
|
|
152
|
+
`PictFormApplication`), set the destination directly:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
class MyWorkflowApplication extends libPictSectionForm.PictFormApplication
|
|
156
|
+
{
|
|
157
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
158
|
+
{
|
|
159
|
+
super(pFable, pOptions, pServiceHash);
|
|
160
|
+
this.pict.views.PictFormMetacontroller.comprehensionDestinationAddress = 'AppData.WorkflowComprehensions';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Option 2: via metacontroller options
|
|
166
|
+
|
|
167
|
+
If you're registering the metacontroller view yourself (no `PictFormApplication`
|
|
168
|
+
parent), pass `ComprehensionDestinationAddress` in the options:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
pict.addView(
|
|
172
|
+
'PictFormMetacontroller',
|
|
173
|
+
{ ComprehensionDestinationAddress: 'Bundle.PendingWrites' },
|
|
174
|
+
libPictSectionForm.PictFormMetacontroller
|
|
175
|
+
);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Option 3: change it mid-flight
|
|
179
|
+
|
|
180
|
+
The property is a plain string -- reassign whenever you need different
|
|
181
|
+
destinations for different phases:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
// Before fanning the form's solvers, redirect to a transient staging slot.
|
|
185
|
+
this.pict.views.PictFormMetacontroller.comprehensionDestinationAddress = 'TempData.PendingComprehension';
|
|
186
|
+
this.pict.PictApplication.solve();
|
|
187
|
+
const tmpPending = this.pict.TempData.PendingComprehension;
|
|
188
|
+
|
|
189
|
+
// Promote / discard / inspect tmpPending however you like.
|
|
190
|
+
|
|
191
|
+
// Switch back to the canonical destination for the next solve.
|
|
192
|
+
this.pict.views.PictFormMetacontroller.comprehensionDestinationAddress = 'AppData.WorkflowComprehensions';
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The destination address is resolved against the pict instance, so any subtree
|
|
196
|
+
works (`AppData.*`, `Bundle.*`, `TempData.*`, ...).
|
|
197
|
+
|
|
198
|
+
## Resetting the tree between solves
|
|
199
|
+
|
|
200
|
+
`addComprehensionEntity` never deletes keys -- it only writes / overwrites. If
|
|
201
|
+
your form workflow expects "only emit comprehensions for *currently visible*
|
|
202
|
+
records," you need to clear the destination at the start of the solve cycle.
|
|
203
|
+
Two patterns:
|
|
204
|
+
|
|
205
|
+
### Pattern A: low-ordinal reset solver
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
"Solvers":
|
|
209
|
+
[
|
|
210
|
+
// Runs before any addComprehensionEntity calls because of the explicit
|
|
211
|
+
// low ordinal. `getvalue` returns a reference to the live AppData branch,
|
|
212
|
+
// but assigning to `AppData.X` rebinds the address. In manyfest assignments
|
|
213
|
+
// we want to nuke the previous tree, so explicitly empty it.
|
|
214
|
+
{ Ordinal: 1, Expression: `AppData.RecipeWorkflowComprehensions = "{}"` },
|
|
215
|
+
// ...then the addComprehensionEntity calls at higher ordinals
|
|
216
|
+
]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The string `"{}"` becomes `{}` after JSON round-trip on the assignment. If your
|
|
220
|
+
solver dialect doesn't coerce strings to JSON for you, do the reset in
|
|
221
|
+
JavaScript instead:
|
|
222
|
+
|
|
223
|
+
### Pattern B: JS-side reset
|
|
224
|
+
|
|
225
|
+
Override `marshalFromView` or `onBeforeSolve` to zero the destination:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
class MyWorkflowApplication extends libPictSectionForm.PictFormApplication
|
|
229
|
+
{
|
|
230
|
+
onBeforeSolve()
|
|
231
|
+
{
|
|
232
|
+
this.pict.AppData.RecipeWorkflowComprehensions = {};
|
|
233
|
+
return super.onBeforeSolve();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This is the simplest version. The `addComprehensionEntity` resolver handles a
|
|
239
|
+
missing-or-emptied destination by re-materializing it on the next write.
|
|
240
|
+
|
|
241
|
+
## Full reference: the complex_table sample config
|
|
242
|
+
|
|
243
|
+
The [complex_table example](../example_applications/complex_table/Complex-Tabular-Application.js)
|
|
244
|
+
exercises every pattern on this page in one application. The relevant pieces:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
// Constructor sets a customized destination.
|
|
248
|
+
this.pict.views.PictFormMetacontroller.comprehensionDestinationAddress = 'AppData.RecipeWorkflowComprehensions';
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
// Recipe section solvers -- mix of bare-symbol GUID (RecipeName) and address
|
|
253
|
+
// arguments, multiple OnSave properties, MAP VAR fanning over a recordset,
|
|
254
|
+
// and IF-routed OnApprovalAction.
|
|
255
|
+
Solvers:
|
|
256
|
+
[
|
|
257
|
+
// ...prior compute solvers...
|
|
258
|
+
|
|
259
|
+
// OnSave.Recipe.<RecipeName>.<Property> for the recipe-level facts.
|
|
260
|
+
{ Ordinal: 200, Expression: `addComprehensionEntity("OnSave", "Recipe", RecipeName, "Name", RecipeName)` },
|
|
261
|
+
{ Ordinal: 200, Expression: `addComprehensionEntity("OnSave", "Recipe", RecipeName, "Type", RecipeType)` },
|
|
262
|
+
{ Ordinal: 200, Expression: `addComprehensionEntity("OnSave", "Recipe", RecipeName, "Description", RecipeDescription)` },
|
|
263
|
+
{ Ordinal: 200, Expression: `addComprehensionEntity("OnSave", "Recipe", RecipeName, "Inventor", Inventor)` },
|
|
264
|
+
{ Ordinal: 200, Expression: `addComprehensionEntity("OnSave", "Recipe", RecipeName, "TotalCalories", TotalFruitCalories)` },
|
|
265
|
+
{ Ordinal: 200, Expression: `addComprehensionEntity("OnSave", "Recipe", RecipeName, "AverageFatPercent", AverageFatPercent)` },
|
|
266
|
+
|
|
267
|
+
// OnSave.Fruit.<fruit>.<Property> -- per-row via MAP VAR over the FruitGrid recordset.
|
|
268
|
+
{ Ordinal: 210, Expression: `MAP VAR row FROM FruitData.FruityVice : addComprehensionEntity("OnSave", "Fruit", row.name, "Family", row.family)` },
|
|
269
|
+
{ Ordinal: 210, Expression: `MAP VAR row FROM FruitData.FruityVice : addComprehensionEntity("OnSave", "Fruit", row.name, "Order", row.order)` },
|
|
270
|
+
{ Ordinal: 210, Expression: `MAP VAR row FROM FruitData.FruityVice : addComprehensionEntity("OnSave", "Fruit", row.name, "Calories", row.nutritions.calories)` },
|
|
271
|
+
|
|
272
|
+
// OnApprovalAction.{Submit,Approve}.Recipe.<RecipeName> -- computed context.
|
|
273
|
+
{ Ordinal: 220, Expression: `addComprehensionEntity(IF(Proprietary, "==", 1, "OnApprovalAction.Submit", "OnApprovalAction.Approve"), "Recipe", RecipeName, "Status", IF(Proprietary, "==", 1, "Submitted", "Approved"))` },
|
|
274
|
+
{ Ordinal: 220, Expression: `addComprehensionEntity(IF(Proprietary, "==", 1, "OnApprovalAction.Submit", "OnApprovalAction.Approve"), "Recipe", RecipeName, "Reviewer", Inventor)` }
|
|
275
|
+
]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
After loading the example, fill in the Recipe section and toggle the
|
|
279
|
+
Proprietary checkbox; inspect `_Pict.AppData.RecipeWorkflowComprehensions` in
|
|
280
|
+
the browser console to see the OnSave / OnApprovalAction subtrees update.
|
|
281
|
+
|
|
282
|
+
## Where this stops being the right tool
|
|
283
|
+
|
|
284
|
+
`addComprehensionEntity` is for shaping comprehension trees inside the
|
|
285
|
+
solver. If you're operating outside the solver loop -- e.g. building a
|
|
286
|
+
comprehension from an HTTP response, transforming CSV, or merging two pre-built
|
|
287
|
+
comprehensions -- reach for the meadow-integration toolchain directly:
|
|
288
|
+
|
|
289
|
+
- `meadow-integration csvtransform` to map columns into entity records.
|
|
290
|
+
- `meadow-integration comprehensionintersect` to merge two comprehensions.
|
|
291
|
+
- The `Comprehension` object's `Object.assign` semantics for in-code merges.
|
|
292
|
+
|
|
293
|
+
See [meadow-integration: Comprehensions](https://github.com/stevenvelozo/meadow-integration/blob/main/docs/comprehensions.md)
|
|
294
|
+
for those tools. The solver helper is purpose-built for "as I edit this form,
|
|
295
|
+
build the comprehension that will save it."
|
package/docs/Configuration.md
CHANGED
|
@@ -85,6 +85,23 @@ Groups organize inputs within a section.
|
|
|
85
85
|
| `CSSClasses` | array | No | CSS classes to apply |
|
|
86
86
|
| `Visible` | boolean | No | Initial visibility (default: true) |
|
|
87
87
|
|
|
88
|
+
#### Tabular Group Properties
|
|
89
|
+
|
|
90
|
+
These additional properties apply only to groups with `Layout: "Tabular"`.
|
|
91
|
+
See [Layouts](Layouts.md) for full details and examples.
|
|
92
|
+
|
|
93
|
+
| Property | Type | Description |
|
|
94
|
+
|----------|------|-------------|
|
|
95
|
+
| `RecordManifest` | string | Reference manifest naming the columns |
|
|
96
|
+
| `Headers` | array | Extra stacked / clustered header rows above the prime header |
|
|
97
|
+
| `RowLabels` | array | Left-side label columns (template / row-number / pre-slotted; clusterable) |
|
|
98
|
+
| `DynamicColumns` | array | Generators that build columns at runtime from another array |
|
|
99
|
+
| `EditingControlsPosition` | string | `"right"` (default), `"left"`, or `"hidden"` |
|
|
100
|
+
| `SuppressDefaultColumnHeaderRow` | boolean | Omit the prime column-name header row |
|
|
101
|
+
| `RowSelection` | boolean/object | Add row checkboxes; selection persists in form data |
|
|
102
|
+
| `ColumnSelection` | boolean/object | Add column checkboxes; selection persists in form data |
|
|
103
|
+
| `ColumnSorting` | boolean | Add clickable sort controls to the prime header cells (default off) |
|
|
104
|
+
|
|
88
105
|
### Layout Types
|
|
89
106
|
|
|
90
107
|
- `Record` - Standard form layout with rows and columns
|
package/docs/Layouts.md
CHANGED
|
@@ -160,6 +160,176 @@ TabularTemplate-RowPostfix
|
|
|
160
160
|
TabularTemplate-TablePostfix
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
### Stacked & Clustered Headers
|
|
164
|
+
|
|
165
|
+
By default a tabular group has a single header row, one cell per column. The
|
|
166
|
+
optional `Headers` property adds **extra header rows stacked above** that
|
|
167
|
+
default ("prime") row. Each entry in `Headers` is one header row; each row is
|
|
168
|
+
an array of cells.
|
|
169
|
+
|
|
170
|
+
| Cell property | Type | Description |
|
|
171
|
+
|---------------|------|-------------|
|
|
172
|
+
| `Label` | string | Header text |
|
|
173
|
+
| `ColumnSpan` | number | Number of data columns this cell spans (default 1) — this is how you "cluster" |
|
|
174
|
+
| `CSSClass` | string | Optional class applied to the `<th>` |
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"Hash": "GradebookGrid",
|
|
179
|
+
"Layout": "Tabular",
|
|
180
|
+
"RecordSetAddress": "Grades",
|
|
181
|
+
"RecordManifest": "GradeRowEditor",
|
|
182
|
+
"Headers": [
|
|
183
|
+
[
|
|
184
|
+
{ "Label": "First Semester", "ColumnSpan": 3, "CSSClass": "term-banner" },
|
|
185
|
+
{ "Label": "Second Semester", "ColumnSpan": 4, "CSSClass": "term-banner" }
|
|
186
|
+
]
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Each header row's `ColumnSpan` total should equal the number of data columns;
|
|
192
|
+
a mismatch is logged as a warning and the header will visually misalign.
|
|
193
|
+
Header rows render top-to-bottom in array order, directly above the prime
|
|
194
|
+
column-name row.
|
|
195
|
+
|
|
196
|
+
### Row Label Columns
|
|
197
|
+
|
|
198
|
+
The `RowLabels` property adds one or more **label columns down the left side**
|
|
199
|
+
of the table (before the data columns). Each entry describes one label column.
|
|
200
|
+
|
|
201
|
+
| Property | Type | Description |
|
|
202
|
+
|----------|------|-------------|
|
|
203
|
+
| `Name` | string | Header text for the label column |
|
|
204
|
+
| `Template` | string | A Pict template resolved per row — the row record is at `Record.Value`, the row index at `Record.Key` |
|
|
205
|
+
| `RowNumber` | boolean | When `true`, the label is the 1-based row number |
|
|
206
|
+
| `SourceAddress` | string | An app-data address of a pre-slotted array; element `[rowIndex]` is the label |
|
|
207
|
+
| `Cluster` | boolean | When `true`, consecutive equal labels collapse into one cell with `rowspan` |
|
|
208
|
+
| `CSSClass` | string | Optional class applied to the label `<td>` |
|
|
209
|
+
|
|
210
|
+
Provide exactly one of `Template`, `RowNumber`, or `SourceAddress` per entry.
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
"RowLabels": [
|
|
214
|
+
{ "Name": "Section", "Template": "{~D:Record.Value.Section~}", "Cluster": true },
|
|
215
|
+
{ "Name": "Student", "Template": "{~D:Record.Value.StudentName~}" },
|
|
216
|
+
{ "Name": "#", "RowNumber": true }
|
|
217
|
+
]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`Cluster: true` is what produces the "merged cell" look — a column of repeated
|
|
221
|
+
values (e.g. a class section) renders as a single tall cell spanning its run
|
|
222
|
+
of rows. Any label column may be clustered; there is no "prime" label column.
|
|
223
|
+
|
|
224
|
+
### Dynamic Columns
|
|
225
|
+
|
|
226
|
+
`DynamicColumns` generates table columns at runtime from **another array** in
|
|
227
|
+
the form data — for example, one grade column per assignment. Each entry is a
|
|
228
|
+
generator:
|
|
229
|
+
|
|
230
|
+
| Property | Type | Description |
|
|
231
|
+
|----------|------|-------------|
|
|
232
|
+
| `SourceAddress` | string | App-data address of the array driving the columns |
|
|
233
|
+
| `HashTemplate` | string | Template producing each column's unique descriptor hash |
|
|
234
|
+
| `NameTemplate` | string | Template producing each column's header text |
|
|
235
|
+
| `InformaryDataAddressTemplate` | string | Template producing the per-row data address the cell binds to |
|
|
236
|
+
| `HeaderGroupTemplate` | string | Optional — template producing a cluster label; auto-adds a clustered super-header row |
|
|
237
|
+
| `DataType` | string | Data type for the generated descriptors |
|
|
238
|
+
| `PictForm` | object | `PictForm` block merged onto each generated descriptor (e.g. `InputType`) |
|
|
239
|
+
| `InsertAt` | string/object | `"End"` (default), `"Start"`, or `{ "After": "<hash>" }` |
|
|
240
|
+
|
|
241
|
+
Inside each template the **source row** is the record (`Record.Field`).
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
"DynamicColumns": [
|
|
245
|
+
{
|
|
246
|
+
"SourceAddress": "Assignments",
|
|
247
|
+
"HashTemplate": "Grade_{~D:Record.IDAssignment~}",
|
|
248
|
+
"NameTemplate": "{~D:Record.Title~}",
|
|
249
|
+
"InformaryDataAddressTemplate": "Grades.{~D:Record.IDAssignment~}",
|
|
250
|
+
"HeaderGroupTemplate": "{~D:Record.Topic~}",
|
|
251
|
+
"DataType": "Number",
|
|
252
|
+
"PictForm": { "InputType": "Number" }
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Dynamic columns are **non-destructive**: when a source row is removed the
|
|
258
|
+
generated column disappears, but the underlying row data at the
|
|
259
|
+
`InformaryDataAddress` is left untouched — re-adding the source row brings the
|
|
260
|
+
column back with its data intact. The columns re-resolve automatically as the
|
|
261
|
+
source array changes; no manual refresh call is needed.
|
|
262
|
+
|
|
263
|
+
When `HeaderGroupTemplate` is set, an extra clustered super-header row is
|
|
264
|
+
synthesized automatically: consecutive generated columns sharing the same
|
|
265
|
+
header-group value merge into one spanning cell (e.g. assignments clustered by
|
|
266
|
+
topic).
|
|
267
|
+
|
|
268
|
+
### Editing Controls Position
|
|
269
|
+
|
|
270
|
+
Tabular rows render del / up / down controls. `EditingControlsPosition`
|
|
271
|
+
controls where:
|
|
272
|
+
|
|
273
|
+
| Value | Behavior |
|
|
274
|
+
|-------|----------|
|
|
275
|
+
| `"right"` | Default — controls in a trailing column |
|
|
276
|
+
| `"left"` | Controls in a leading column, before the data columns |
|
|
277
|
+
| `"hidden"` | No editing controls (read-only style table) |
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{ "Layout": "Tabular", "EditingControlsPosition": "hidden" }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Suppressing the Default Header Row
|
|
284
|
+
|
|
285
|
+
Set `SuppressDefaultColumnHeaderRow: true` to omit the prime column-name row
|
|
286
|
+
entirely — useful when custom `Headers` rows fully describe the columns.
|
|
287
|
+
|
|
288
|
+
### Selectable Rows & Columns
|
|
289
|
+
|
|
290
|
+
`RowSelection` and `ColumnSelection` add checkboxes that let the user pick
|
|
291
|
+
rows / columns. The selected state is **stored in the form data**, so it
|
|
292
|
+
persists with a save and can be read by solvers.
|
|
293
|
+
|
|
294
|
+
Set either to `true` for defaults, or to an object:
|
|
295
|
+
|
|
296
|
+
| Property | Type | Description |
|
|
297
|
+
|----------|------|-------------|
|
|
298
|
+
| `Enabled` | boolean | Set `false` to disable (same as omitting) |
|
|
299
|
+
| `DataAddress` | string | Where the boolean selection array is stored (default `<GroupHash>_RowSelection` / `_ColumnSelection`) |
|
|
300
|
+
| `HighlightClass` | string | Class auto-applied to selected rows/columns; set to `""` for solver-driven highlighting only |
|
|
301
|
+
| `HeaderLabel` | string | Header text for the row-selection column |
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"Layout": "Tabular",
|
|
306
|
+
"RecordSetAddress": "Grades",
|
|
307
|
+
"RowSelection": true,
|
|
308
|
+
"ColumnSelection": true
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Checking a row (or column) highlights every cell across (or down) it and
|
|
313
|
+
writes `true` into the selection array at the configured address. Because the
|
|
314
|
+
array lives in the marshalled form data it round-trips with save / load.
|
|
315
|
+
|
|
316
|
+
### Column Sorting
|
|
317
|
+
|
|
318
|
+
`ColumnSorting: true` (off by default) injects a clickable sort control — a
|
|
319
|
+
`<span>` carrying a sort SVG glyph from Pict's icon registry — into every
|
|
320
|
+
prime header cell.
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{ "Layout": "Tabular", "RecordSetAddress": "Students", "ColumnSorting": true }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Clicking a column's control sorts the record set ascending; clicking the
|
|
327
|
+
active column again toggles to descending. The glyph reflects state: a neutral
|
|
328
|
+
double-arrow on idle columns, an up / down arrow on the active column. Sorting
|
|
329
|
+
works for both static and dynamic columns (dynamic columns sort by their
|
|
330
|
+
`InformaryDataAddress` value). Values that parse as numbers sort numerically;
|
|
331
|
+
others sort lexically.
|
|
332
|
+
|
|
163
333
|
## RecordSet Layout
|
|
164
334
|
|
|
165
335
|
Similar to tabular but renders each record as a full form section rather
|