@marktiderman/genesis-forms 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +146 -2
- package/conformance/after-class-back-after-a-jump.scenario.json +217 -0
- package/conformance/after-class-contact-yes-completed.scenario.json +230 -0
- package/conformance/after-class-contact-yes-requires-name-and-email.scenario.json +218 -0
- package/conformance/after-class-happy-path.scenario.json +237 -0
- package/conformance/after-class-invalid-email-rejected.scenario.json +227 -0
- package/conformance/after-class-low-rating-skips-went-well.scenario.json +223 -0
- package/conformance/after-class-server-drops-unknown-and-hidden-answers.scenario.json +236 -0
- package/conformance/after-class-server-ignores-a-client-sent-coach.scenario.json +219 -0
- package/conformance/contact-details-anonymous-form-skips-identifying-fields.scenario.json +125 -0
- package/conformance/contact-details-named-form-asks-for-identifying-fields.scenario.json +129 -0
- package/conformance/monthly-pulse-default-ending.scenario.json +148 -0
- package/conformance/monthly-pulse-promoter-ending-via-jump.scenario.json +128 -0
- package/dist/engine-Cp5t2_Am.d.ts +75 -0
- package/dist/engine-LsPRTzt3.d.cts +75 -0
- package/dist/index.cjs +1205 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +374 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.js +1174 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +988 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +52 -0
- package/dist/server.d.ts +52 -0
- package/dist/server.js +984 -0
- package/dist/server.js.map +1 -0
- package/dist/testing.cjs +1299 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +129 -0
- package/dist/testing.d.ts +129 -0
- package/dist/testing.js +1293 -0
- package/dist/testing.js.map +1 -0
- package/dist/validate-j8Z188mF.d.cts +175 -0
- package/dist/validate-j8Z188mF.d.ts +175 -0
- package/package.json +79 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Tiderman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @marktiderman/genesis-forms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Genesis Forms engine. A form is plain JSON: blocks, logic, endings and typed hidden inputs. It
|
|
4
|
+
can be stored, diffed, versioned, and evaluated identically in a browser, in React Native and on a
|
|
5
|
+
server. Zero dependencies.
|
|
6
|
+
|
|
7
|
+
**Status: 0.1, release R1.** Field types: `short-text`, `long-text`, `email`, `phone`, `rating`,
|
|
8
|
+
`nps`, `single-choice`, `multi-choice`, `yes-no`. Logic: show, hide, jump to a field or an ending. Layouts
|
|
9
|
+
(rendered by `genesis-forms-web`): focus (one question at a time) and classic. The full plan is in
|
|
10
|
+
[docs/FORMS.md](../../docs/FORMS.md).
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { defineForm, f, when } from "@marktiderman/genesis-forms";
|
|
14
|
+
|
|
15
|
+
export const afterClass = defineForm({
|
|
16
|
+
id: "after-class",
|
|
17
|
+
title: "How was class?",
|
|
18
|
+
layout: { mode: "focus" },
|
|
19
|
+
hidden: { coach: f.hidden.string({ source: "server" }) },
|
|
20
|
+
blocks: [
|
|
21
|
+
f.rating("overall", { label: "How was today's class?", required: true }),
|
|
22
|
+
f.longText("improve", { label: "Anything we could do better?" }),
|
|
23
|
+
f.yesNo("contact_me", { label: "Want {{hidden:coach|a coach}} to follow up?" }),
|
|
24
|
+
f.email("email", { label: "Best email to reach you", required: true }),
|
|
25
|
+
],
|
|
26
|
+
logic: [when("contact_me").isNot(true).hide("email")],
|
|
27
|
+
endings: [{ id: "default", title: "Thanks!" }],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
type Answers = typeof afterClass.$answers; // { overall?: number; improve?: string; … }
|
|
31
|
+
const json = afterClass.toJSON(); // the canonical definition
|
|
32
|
+
const version = afterClass.hash(); // content fingerprint for publishing
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The rules it keeps
|
|
36
|
+
|
|
37
|
+
- **Answers are keyed by field id**, never by label or position. `id` defaults to the key; set it
|
|
38
|
+
explicitly before renaming a key on a live form.
|
|
39
|
+
- **The path is derived from the answers on the path**, so the client, the server and a replay
|
|
40
|
+
always agree. It is built in one forward pass: a block's rules, and the jumps from it, see only the
|
|
41
|
+
fields already on the path before it. A field that is hidden, jumped over or later in the form
|
|
42
|
+
reads as unanswered, whatever value it still holds. A field jumped over counts as hidden: it is
|
|
43
|
+
not validated, and its value is cleared on submit.
|
|
44
|
+
- **Empty is unanswered.** An empty value (unanswered, `null`, blank text, an empty list) fails
|
|
45
|
+
`eq`, `in` and `contains` and passes `neq`: `eq null` is never true for a cleared field.
|
|
46
|
+
- **Hidden inputs are typed and sourced.** `url` and `host` inputs are untrusted; `server` inputs are
|
|
47
|
+
never read from the client. `coerceHidden(definition, raw)` types them, in `createForm` and on the
|
|
48
|
+
server alike: only string, number and boolean primitives, `""` is not a number, strings at most
|
|
49
|
+
1000 characters.
|
|
50
|
+
- **Everything is in the event log.** `initialValues` are logged as leading `answer` events with
|
|
51
|
+
`source: "prefill"`, so `replay` and the server see them.
|
|
52
|
+
- **Anonymous forms withhold identifying fields.** `settings.anonymous` is a per-form admin
|
|
53
|
+
setting, off by default. When it is true, fields marked `identifying: true` are treated as hidden
|
|
54
|
+
(off the path, not validated, cleared on submit), the server drops client-sent values for them and
|
|
55
|
+
lists them in `ignored`, and events carry no `at` timestamps whatever clock is passed. Fields not
|
|
56
|
+
marked `identifying` are unaffected.
|
|
57
|
+
- **Definitions are checked.** `defineForm` runs `parseForm` and throws with every issue;
|
|
58
|
+
`validateSubmission` refuses a definition that does not parse. Ids, keys, hidden names and ending
|
|
59
|
+
ids may not be `Object.prototype` names (`constructor`, `__proto__`…).
|
|
60
|
+
- **Unanswered recall renders its fallback** (`{{field:name|there}}`); the text is never dropped.
|
|
61
|
+
|
|
62
|
+
## Pieces
|
|
63
|
+
|
|
64
|
+
| Export | What it does |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `defineForm`, `f`, `when` | Author a form in code, with inferred answer types. |
|
|
67
|
+
| `parseForm(json)` | Check a stored or built definition: structure, text fields, references, operators, recall. Never throws. |
|
|
68
|
+
| `createForm(def)` | The engine: values, visibility, path, Next/Back with history, validation, submit, an event log. Framework-agnostic `subscribe` / `getState`. |
|
|
69
|
+
| `replay(def, events)` | Rebuild a response from its event log. |
|
|
70
|
+
| `registerFieldType` | Add a field type. Non-JSON values must provide `serialize` and `deserialize`; `normalize` sets the stored form (email trims). A field whose type is not registered accepts no answer. |
|
|
71
|
+
| `coerceHidden(def, raw, { sources })` | Type hidden inputs by their declarations; reports dropped names in `ignored`. |
|
|
72
|
+
| `isFormEvent(value)` | Guard for one well-formed event-log entry. |
|
|
73
|
+
| `isSensitive(block)` | `sensitive: true` on a field, or the `props.sensitive` alias. Hidden inputs take `sensitive: true` too (`f.hidden.string({ source: "server", sensitive: true })`); devtools masks both. |
|
|
74
|
+
| `/server` → `validateSubmission`, `publicDefinition` | Re-check a submission against the published definition: drops unknown and logic-hidden answers, ignores client values for server inputs, checks drafts by format only. Never throws on a malformed body. No IO. |
|
|
75
|
+
| `explain(engine, blockId)` | "Why is this hidden?": the path, the jump that skipped a block, each show/hide rule with its result in plain English, and the field's validation. |
|
|
76
|
+
| `/testing` → `simulate`, `coverage`, `runScenario`, `recordScenario`, `compactEvents` | Walk a form by key, enumerate its paths, and run JSON scenarios. See [Testing](#testing). |
|
|
77
|
+
|
|
78
|
+
`f.phone(key, opts)` stores a string: trimmed, an optional leading `+`, digits and the separators
|
|
79
|
+
space, `-`, `(`, `)` and `.`, 7 to 15 digits, at most 32 characters. Operators: `eq`, `neq`,
|
|
80
|
+
`answered`, `empty`. Its manifest carries `autocomplete: "tel"` and `inputMode: "tel"` (email:
|
|
81
|
+
`"email"` for both).
|
|
82
|
+
|
|
83
|
+
## On the server
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { validateSubmission } from "@marktiderman/genesis-forms/server";
|
|
87
|
+
|
|
88
|
+
const result = validateSubmission(publishedDefinition, await request.json(), {
|
|
89
|
+
context: { coach: coachForNow() }, // server inputs, resolved here
|
|
90
|
+
});
|
|
91
|
+
if (!result.ok) return Response.json({ errors: result.errors }, { status: 422 });
|
|
92
|
+
await store(result.submission); // answers, hidden, context, ending
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Errors are keyed by field id. The `""` key is for the whole submission: `"Malformed submission."`
|
|
96
|
+
(a body of the wrong shape), a definition that fails `parseForm`, or more than `maxEvents` events
|
|
97
|
+
(default 10 000, counted after `compactEvents`). Events of an unknown type are dropped, so a newer
|
|
98
|
+
client's log still replays. With `settings.clearHiddenValues: false`, a hidden field's value is kept
|
|
99
|
+
only if its format is valid.
|
|
100
|
+
|
|
101
|
+
## Testing
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { coverage, recordScenario, runScenario, simulate } from "@marktiderman/genesis-forms/testing";
|
|
105
|
+
|
|
106
|
+
// Walk a form by key.
|
|
107
|
+
expect(simulate(afterClass).answer({ overall: 2 }).pathKeys()).toEqual(["overall", "improve", "contact_me"]);
|
|
108
|
+
|
|
109
|
+
// Every block reachable, every rule able to fire, every ending reached.
|
|
110
|
+
const report = coverage(afterClass);
|
|
111
|
+
expect(report.unreachableBlocks).toEqual([]);
|
|
112
|
+
expect(report.rulesNeverFired).toEqual([]);
|
|
113
|
+
expect(report.endings.default.reached).toBe(true); // witness: answers that reach it
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`coverage` enumerates paths symbolically: each field gets a small domain (rating `1..max`, NPS
|
|
117
|
+
`0..10`, yes/no, each choice option, a sample text, "unanswered" unless required) plus the values
|
|
118
|
+
the logic compares it with, and a depth-first search answers fields in path order. It stops at
|
|
119
|
+
`maxStates` (default 5000) and then reports `truncated: true`. A completed response with no jump to
|
|
120
|
+
an ending reaches the default ending. A plugin type gives coverage its answers through the manifest's
|
|
121
|
+
`samples(field)`. A field with no valid sample answer is listed in `unsampledFields`; coverage
|
|
122
|
+
still steers the logic down each branch on it (unanswered, the compared values, some answer), so its
|
|
123
|
+
blocks and rules count, but an ending found only that way stays `reached: false` with `needsSamples`.
|
|
124
|
+
|
|
125
|
+
**Scenarios** are versioned JSON (`scenarioVersion: 1`): a definition, hidden inputs, server
|
|
126
|
+
`context`, an event log and the expected `path`, `visible`, `current`, `status`, `ending`,
|
|
127
|
+
`answers`, client `errors` and server verdict (all by key). `runScenario(s)` replays the events
|
|
128
|
+
with the clock off, compares, and runs `validateSubmission` (as completed when the replay
|
|
129
|
+
completed, or as `expect.server.complete` says). `recordScenario(engine, name)` freezes a live
|
|
130
|
+
engine as a scenario with its expectations filled in.
|
|
131
|
+
|
|
132
|
+
Text inputs log one answer event per keystroke. `compactEvents(events)` merges consecutive answers
|
|
133
|
+
to the same field, keeping the last value; the result replays to the same state because the path
|
|
134
|
+
depends on values alone, not on how they were typed. `recordScenario` compacts by default (`compact: false` keeps every event).
|
|
135
|
+
|
|
136
|
+
The **conformance suite** ships in the package: `@marktiderman/genesis-forms/conformance/*.scenario.json`
|
|
137
|
+
(the R1 sample forms: after-class and monthly-pulse). Run each file through `runScenario` to hold
|
|
138
|
+
another runtime, a renderer or an app's CI to the same behaviour. The fixtures are generated by
|
|
139
|
+
`pnpm --filter @marktiderman/genesis-forms conformance:build`; review the diff before committing.
|
|
140
|
+
|
|
141
|
+
Two engine rules the suite pins down: failed `next` and `submit` attempts are logged, so a replay
|
|
142
|
+
shows the same errors; and a completed response is final, so edits after submit are ignored by the
|
|
143
|
+
engine and by the server's replay.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scenarioVersion": 1,
|
|
3
|
+
"name": "after-class: back after a jump",
|
|
4
|
+
"description": "Back from a jump target returns to the block the jump came from; raising the rating puts went_well back on the path.",
|
|
5
|
+
"tags": [
|
|
6
|
+
"after-class",
|
|
7
|
+
"jump",
|
|
8
|
+
"navigation"
|
|
9
|
+
],
|
|
10
|
+
"definition": {
|
|
11
|
+
"schemaVersion": 1,
|
|
12
|
+
"id": "after-class",
|
|
13
|
+
"title": "How was class?",
|
|
14
|
+
"blocks": [
|
|
15
|
+
{
|
|
16
|
+
"kind": "field",
|
|
17
|
+
"id": "overall",
|
|
18
|
+
"key": "overall",
|
|
19
|
+
"type": "rating",
|
|
20
|
+
"label": "How was today's class?",
|
|
21
|
+
"required": true,
|
|
22
|
+
"props": {
|
|
23
|
+
"max": 5
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"kind": "field",
|
|
28
|
+
"id": "went_well",
|
|
29
|
+
"key": "went_well",
|
|
30
|
+
"type": "multi-choice",
|
|
31
|
+
"label": "What went well?",
|
|
32
|
+
"props": {
|
|
33
|
+
"options": [
|
|
34
|
+
{
|
|
35
|
+
"value": "coaching",
|
|
36
|
+
"label": "Coaching"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"value": "workout",
|
|
40
|
+
"label": "The workout"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"value": "community",
|
|
44
|
+
"label": "Community"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"value": "scaling",
|
|
48
|
+
"label": "Scaling options"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"value": "equipment",
|
|
52
|
+
"label": "Equipment"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"value": "cleanliness",
|
|
56
|
+
"label": "Cleanliness"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"kind": "field",
|
|
63
|
+
"id": "improve",
|
|
64
|
+
"key": "improve",
|
|
65
|
+
"type": "long-text",
|
|
66
|
+
"label": "Anything we could do better?"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"kind": "field",
|
|
70
|
+
"id": "contact_me",
|
|
71
|
+
"key": "contact_me",
|
|
72
|
+
"type": "yes-no",
|
|
73
|
+
"label": "Want {{hidden:coach|a coach}} to follow up with you?"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"kind": "field",
|
|
77
|
+
"id": "name",
|
|
78
|
+
"key": "name",
|
|
79
|
+
"type": "short-text",
|
|
80
|
+
"label": "Your name",
|
|
81
|
+
"required": true
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"kind": "field",
|
|
85
|
+
"id": "email",
|
|
86
|
+
"key": "email",
|
|
87
|
+
"type": "email",
|
|
88
|
+
"label": "Best email to reach you",
|
|
89
|
+
"required": true
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"layout": {
|
|
93
|
+
"mode": "focus"
|
|
94
|
+
},
|
|
95
|
+
"hidden": [
|
|
96
|
+
{
|
|
97
|
+
"name": "class",
|
|
98
|
+
"type": "string",
|
|
99
|
+
"source": "server"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "coach",
|
|
103
|
+
"type": "string",
|
|
104
|
+
"source": "server"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"logic": [
|
|
108
|
+
{
|
|
109
|
+
"id": "r1",
|
|
110
|
+
"when": {
|
|
111
|
+
"op": "neq",
|
|
112
|
+
"left": {
|
|
113
|
+
"field": "contact_me"
|
|
114
|
+
},
|
|
115
|
+
"right": {
|
|
116
|
+
"value": true
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"effect": "hide",
|
|
120
|
+
"target": "name"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"id": "r2",
|
|
124
|
+
"when": {
|
|
125
|
+
"op": "neq",
|
|
126
|
+
"left": {
|
|
127
|
+
"field": "contact_me"
|
|
128
|
+
},
|
|
129
|
+
"right": {
|
|
130
|
+
"value": true
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"effect": "hide",
|
|
134
|
+
"target": "email"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "r3",
|
|
138
|
+
"when": {
|
|
139
|
+
"op": "lt",
|
|
140
|
+
"left": {
|
|
141
|
+
"field": "overall"
|
|
142
|
+
},
|
|
143
|
+
"right": {
|
|
144
|
+
"value": 4
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"effect": "jump",
|
|
148
|
+
"from": "overall",
|
|
149
|
+
"to": "improve"
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"endings": [
|
|
153
|
+
{
|
|
154
|
+
"id": "default",
|
|
155
|
+
"title": "Thanks! See you at the next class."
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
"settings": {
|
|
159
|
+
"anonymous": true
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"context": {
|
|
163
|
+
"class": "6:00 AM CrossFit",
|
|
164
|
+
"coach": "Sam"
|
|
165
|
+
},
|
|
166
|
+
"events": [
|
|
167
|
+
{
|
|
168
|
+
"type": "answer",
|
|
169
|
+
"field": "overall",
|
|
170
|
+
"value": 2
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"type": "next"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"type": "back"
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"type": "answer",
|
|
180
|
+
"field": "overall",
|
|
181
|
+
"value": 5
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"type": "next"
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
"expect": {
|
|
188
|
+
"path": [
|
|
189
|
+
"overall",
|
|
190
|
+
"went_well",
|
|
191
|
+
"improve",
|
|
192
|
+
"contact_me"
|
|
193
|
+
],
|
|
194
|
+
"visible": [
|
|
195
|
+
"overall",
|
|
196
|
+
"went_well",
|
|
197
|
+
"improve",
|
|
198
|
+
"contact_me"
|
|
199
|
+
],
|
|
200
|
+
"current": "went_well",
|
|
201
|
+
"status": "in_progress",
|
|
202
|
+
"ending": null,
|
|
203
|
+
"answers": {
|
|
204
|
+
"overall": 5
|
|
205
|
+
},
|
|
206
|
+
"server": {
|
|
207
|
+
"ok": true,
|
|
208
|
+
"answers": {
|
|
209
|
+
"overall": 5
|
|
210
|
+
},
|
|
211
|
+
"context": {
|
|
212
|
+
"class": "6:00 AM CrossFit",
|
|
213
|
+
"coach": "Sam"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scenarioVersion": 1,
|
|
3
|
+
"name": "after-class: contact yes, completed",
|
|
4
|
+
"description": "With name and email filled in, the follow-up request completes and both are stored.",
|
|
5
|
+
"tags": [
|
|
6
|
+
"after-class",
|
|
7
|
+
"show-hide"
|
|
8
|
+
],
|
|
9
|
+
"definition": {
|
|
10
|
+
"schemaVersion": 1,
|
|
11
|
+
"id": "after-class",
|
|
12
|
+
"title": "How was class?",
|
|
13
|
+
"blocks": [
|
|
14
|
+
{
|
|
15
|
+
"kind": "field",
|
|
16
|
+
"id": "overall",
|
|
17
|
+
"key": "overall",
|
|
18
|
+
"type": "rating",
|
|
19
|
+
"label": "How was today's class?",
|
|
20
|
+
"required": true,
|
|
21
|
+
"props": {
|
|
22
|
+
"max": 5
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"kind": "field",
|
|
27
|
+
"id": "went_well",
|
|
28
|
+
"key": "went_well",
|
|
29
|
+
"type": "multi-choice",
|
|
30
|
+
"label": "What went well?",
|
|
31
|
+
"props": {
|
|
32
|
+
"options": [
|
|
33
|
+
{
|
|
34
|
+
"value": "coaching",
|
|
35
|
+
"label": "Coaching"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"value": "workout",
|
|
39
|
+
"label": "The workout"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"value": "community",
|
|
43
|
+
"label": "Community"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"value": "scaling",
|
|
47
|
+
"label": "Scaling options"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"value": "equipment",
|
|
51
|
+
"label": "Equipment"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"value": "cleanliness",
|
|
55
|
+
"label": "Cleanliness"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"kind": "field",
|
|
62
|
+
"id": "improve",
|
|
63
|
+
"key": "improve",
|
|
64
|
+
"type": "long-text",
|
|
65
|
+
"label": "Anything we could do better?"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"kind": "field",
|
|
69
|
+
"id": "contact_me",
|
|
70
|
+
"key": "contact_me",
|
|
71
|
+
"type": "yes-no",
|
|
72
|
+
"label": "Want {{hidden:coach|a coach}} to follow up with you?"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"kind": "field",
|
|
76
|
+
"id": "name",
|
|
77
|
+
"key": "name",
|
|
78
|
+
"type": "short-text",
|
|
79
|
+
"label": "Your name",
|
|
80
|
+
"required": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"kind": "field",
|
|
84
|
+
"id": "email",
|
|
85
|
+
"key": "email",
|
|
86
|
+
"type": "email",
|
|
87
|
+
"label": "Best email to reach you",
|
|
88
|
+
"required": true
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"layout": {
|
|
92
|
+
"mode": "focus"
|
|
93
|
+
},
|
|
94
|
+
"hidden": [
|
|
95
|
+
{
|
|
96
|
+
"name": "class",
|
|
97
|
+
"type": "string",
|
|
98
|
+
"source": "server"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "coach",
|
|
102
|
+
"type": "string",
|
|
103
|
+
"source": "server"
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"logic": [
|
|
107
|
+
{
|
|
108
|
+
"id": "r1",
|
|
109
|
+
"when": {
|
|
110
|
+
"op": "neq",
|
|
111
|
+
"left": {
|
|
112
|
+
"field": "contact_me"
|
|
113
|
+
},
|
|
114
|
+
"right": {
|
|
115
|
+
"value": true
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"effect": "hide",
|
|
119
|
+
"target": "name"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "r2",
|
|
123
|
+
"when": {
|
|
124
|
+
"op": "neq",
|
|
125
|
+
"left": {
|
|
126
|
+
"field": "contact_me"
|
|
127
|
+
},
|
|
128
|
+
"right": {
|
|
129
|
+
"value": true
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"effect": "hide",
|
|
133
|
+
"target": "email"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "r3",
|
|
137
|
+
"when": {
|
|
138
|
+
"op": "lt",
|
|
139
|
+
"left": {
|
|
140
|
+
"field": "overall"
|
|
141
|
+
},
|
|
142
|
+
"right": {
|
|
143
|
+
"value": 4
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"effect": "jump",
|
|
147
|
+
"from": "overall",
|
|
148
|
+
"to": "improve"
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"endings": [
|
|
152
|
+
{
|
|
153
|
+
"id": "default",
|
|
154
|
+
"title": "Thanks! See you at the next class."
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"settings": {
|
|
158
|
+
"anonymous": true
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"context": {
|
|
162
|
+
"class": "6:00 AM CrossFit",
|
|
163
|
+
"coach": "Sam"
|
|
164
|
+
},
|
|
165
|
+
"events": [
|
|
166
|
+
{
|
|
167
|
+
"type": "answer",
|
|
168
|
+
"field": "overall",
|
|
169
|
+
"value": 5
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"type": "answer",
|
|
173
|
+
"field": "contact_me",
|
|
174
|
+
"value": true
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"type": "answer",
|
|
178
|
+
"field": "name",
|
|
179
|
+
"value": "Ada"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"type": "answer",
|
|
183
|
+
"field": "email",
|
|
184
|
+
"value": "ada@example.com"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"type": "submit"
|
|
188
|
+
}
|
|
189
|
+
],
|
|
190
|
+
"expect": {
|
|
191
|
+
"path": [
|
|
192
|
+
"overall",
|
|
193
|
+
"went_well",
|
|
194
|
+
"improve",
|
|
195
|
+
"contact_me",
|
|
196
|
+
"name",
|
|
197
|
+
"email"
|
|
198
|
+
],
|
|
199
|
+
"visible": [
|
|
200
|
+
"overall",
|
|
201
|
+
"went_well",
|
|
202
|
+
"improve",
|
|
203
|
+
"contact_me",
|
|
204
|
+
"name",
|
|
205
|
+
"email"
|
|
206
|
+
],
|
|
207
|
+
"current": "overall",
|
|
208
|
+
"status": "completed",
|
|
209
|
+
"ending": "default",
|
|
210
|
+
"answers": {
|
|
211
|
+
"overall": 5,
|
|
212
|
+
"contact_me": true,
|
|
213
|
+
"name": "Ada",
|
|
214
|
+
"email": "ada@example.com"
|
|
215
|
+
},
|
|
216
|
+
"server": {
|
|
217
|
+
"ok": true,
|
|
218
|
+
"answers": {
|
|
219
|
+
"overall": 5,
|
|
220
|
+
"contact_me": true,
|
|
221
|
+
"name": "Ada",
|
|
222
|
+
"email": "ada@example.com"
|
|
223
|
+
},
|
|
224
|
+
"context": {
|
|
225
|
+
"class": "6:00 AM CrossFit",
|
|
226
|
+
"coach": "Sam"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|