@marktiderman/genesis-forms 0.2.0 → 0.3.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.
Files changed (42) hide show
  1. package/README.md +113 -13
  2. package/conformance/{after-class-back-after-a-jump.scenario.json → event-feedback-back-after-a-jump.scenario.json} +21 -21
  3. package/conformance/{after-class-contact-yes-completed.scenario.json → event-feedback-contact-yes-completed.scenario.json} +21 -21
  4. package/conformance/{after-class-contact-yes-requires-name-and-email.scenario.json → event-feedback-contact-yes-requires-name-and-email.scenario.json} +19 -19
  5. package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json} +24 -24
  6. package/conformance/{after-class-invalid-email-rejected.scenario.json → event-feedback-invalid-email-rejected.scenario.json} +19 -19
  7. package/conformance/{after-class-low-rating-skips-went-well.scenario.json → event-feedback-low-rating-skips-went-well.scenario.json} +21 -21
  8. package/conformance/{after-class-server-drops-unknown-and-hidden-answers.scenario.json → event-feedback-server-drops-unknown-and-hidden-answers.scenario.json} +22 -22
  9. package/conformance/{after-class-server-ignores-a-client-sent-coach.scenario.json → event-feedback-server-ignores-a-client-sent-owner.scenario.json} +24 -24
  10. package/conformance/{monthly-pulse-default-ending.scenario.json → monthly-check-in-default-ending.scenario.json} +6 -6
  11. package/conformance/{monthly-pulse-promoter-ending-via-jump.scenario.json → monthly-check-in-promoter-ending-via-jump.scenario.json} +6 -6
  12. package/dist/destinations.cjs +1235 -0
  13. package/dist/destinations.cjs.map +1 -0
  14. package/dist/destinations.d.cts +465 -0
  15. package/dist/destinations.d.ts +465 -0
  16. package/dist/destinations.js +1193 -0
  17. package/dist/destinations.js.map +1 -0
  18. package/dist/{engine-Coc-gQIB.d.ts → engine-CPLtKVew.d.cts} +2 -1
  19. package/dist/{engine-Bj8-mMVb.d.cts → engine-DOrPgFDw.d.ts} +2 -1
  20. package/dist/index.cjs +322 -24
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +102 -5
  23. package/dist/index.d.ts +102 -5
  24. package/dist/index.js +311 -25
  25. package/dist/index.js.map +1 -1
  26. package/dist/server.cjs +249 -22
  27. package/dist/server.cjs.map +1 -1
  28. package/dist/server.d.cts +3 -2
  29. package/dist/server.d.ts +3 -2
  30. package/dist/server.js +249 -22
  31. package/dist/server.js.map +1 -1
  32. package/dist/testing.cjs +253 -22
  33. package/dist/testing.cjs.map +1 -1
  34. package/dist/testing.d.cts +4 -3
  35. package/dist/testing.d.ts +4 -3
  36. package/dist/testing.js +253 -22
  37. package/dist/testing.js.map +1 -1
  38. package/dist/{validate-BbackAjB.d.cts → types-BBKBQ8sp.d.cts} +2 -29
  39. package/dist/{validate-BbackAjB.d.ts → types-BBKBQ8sp.d.ts} +2 -29
  40. package/dist/validate-BXXqi0fq.d.cts +30 -0
  41. package/dist/validate-BjNwF1UE.d.ts +30 -0
  42. package/package.json +14 -2
package/README.md CHANGED
@@ -12,24 +12,24 @@ server. Zero dependencies.
12
12
  ```ts
13
13
  import { defineForm, f, when } from "@marktiderman/genesis-forms";
14
14
 
15
- export const afterClass = defineForm({
16
- id: "after-class",
17
- title: "How was class?",
15
+ export const eventFeedback = defineForm({
16
+ id: "event-feedback",
17
+ title: "How was the event?",
18
18
  layout: { mode: "focus" },
19
- hidden: { coach: f.hidden.string({ source: "server" }) },
19
+ hidden: { owner: f.hidden.string({ source: "server" }) },
20
20
  blocks: [
21
- f.rating("overall", { label: "How was today's class?", required: true }),
21
+ f.rating("overall", { label: "How was today's event?", required: true }),
22
22
  f.longText("improve", { label: "Anything we could do better?" }),
23
- f.yesNo("contact_me", { label: "Want {{hidden:coach|a coach}} to follow up?" }),
23
+ f.yesNo("contact_me", { label: "Want {{hidden:owner|someone}} to follow up?" }),
24
24
  f.email("email", { label: "Best email to reach you", required: true }),
25
25
  ],
26
26
  logic: [when("contact_me").isNot(true).hide("email")],
27
27
  endings: [{ id: "default", title: "Thanks!" }],
28
28
  });
29
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
30
+ type Answers = typeof eventFeedback.$answers; // { overall?: number; improve?: string; … }
31
+ const json = eventFeedback.toJSON(); // the canonical definition
32
+ const version = eventFeedback.hash(); // content fingerprint for publishing
33
33
  ```
34
34
 
35
35
  ## The rules it keeps
@@ -76,6 +76,7 @@ const version = afterClass.hash(); // content fingerprint for publishing
76
76
  | `isFormEvent(value)` | Guard for one well-formed event-log entry. |
77
77
  | `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. |
78
78
  | `/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. |
79
+ | `/destinations` → `emailDestination` + `resendProvider`, `sheetsDestination`, `webhookDestination`, `responseRow` | Send a checked submission to an inbox, a Google Sheet or a signed webhook. fetch and WebCrypto only. See [Destinations](#destinations). |
79
80
  | `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. |
80
81
  | `/testing` → `simulate`, `coverage`, `runScenario`, `recordScenario`, `compactEvents` | Walk a form by key, enumerate its paths, and run JSON scenarios. See [Testing](#testing). |
81
82
 
@@ -84,13 +85,44 @@ space, `-`, `(`, `)` and `.`, 7 to 15 digits, at most 32 characters. Operators:
84
85
  `answered`, `empty`. Its manifest carries `autocomplete: "tel"` and `inputMode: "tel"` (email:
85
86
  `"email"` for both).
86
87
 
88
+ ### Structured field types
89
+
90
+ Four built-in types collect more than one value for a question. `STRUCTURED_FIELD_TYPES` lists
91
+ them (`["list", "table", "media-notes", "text-highlights"]`), for a builder palette or a check.
92
+
93
+ | Type and builder | Value | Use it for |
94
+ | --- | --- | --- |
95
+ | `list`, `f.list(key, { min, max, itemMaxLength, itemPlaceholder })` | `string[]` | Several short answers: goals, action items, three wins this week. Supports `contains`. |
96
+ | `table`, `f.table(key, { columns, minRows, maxRows })` | `Record<column, string>[]` | Rows with fixed columns (up to `MAX_TABLE_COLUMNS`, 6): an action plan (What / Who / By when), pros and cons. |
97
+ | `media-notes`, `f.mediaNotes(key, { maxNotes, textMaxLength })` | `{ at, text }[]`, `at` in seconds | Notes pinned to moments of a video or audio: lecture notes, reviewing a recorded call. |
98
+ | `text-highlights`, `f.textHighlights(key, { text, maxHighlights })` | `{ start, end, quote, note }[]` | Highlight passages of a text and note on each: annotating an article, a policy or a contract. Every highlight needs a note. |
99
+
100
+ ```ts
101
+ const plan = defineForm({
102
+ id: "action-plan",
103
+ title: "Action plan",
104
+ blocks: [
105
+ f.list("goals", { label: "Your goals for this quarter", max: 3 }),
106
+ f.table("actions", {
107
+ label: "Actions",
108
+ columns: [{ key: "what", label: "What", long: true }, { key: "who", label: "Who" }, { key: "when", label: "By when" }],
109
+ maxRows: 5,
110
+ }),
111
+ ],
112
+ });
113
+ ```
114
+
115
+ Each type validates, normalizes (trimmed, blanks dropped, notes in time order), summarizes and has
116
+ samples for `coverage()`. Helpers: `listLimits`, `tableColumns`, `tableLimits`, `formatTimestamp`
117
+ and `parseTimestamp`; types `TableColumn`, `MediaNote` and `Highlight`.
118
+
87
119
  ## On the server
88
120
 
89
121
  ```ts
90
122
  import { validateSubmission } from "@marktiderman/genesis-forms/server";
91
123
 
92
124
  const result = validateSubmission(publishedDefinition, await request.json(), {
93
- context: { coach: coachForNow() }, // server inputs, resolved here
125
+ context: { owner: ownerForNow() }, // server inputs, resolved here
94
126
  });
95
127
  if (!result.ok) return Response.json({ errors: result.errors }, { status: 422 });
96
128
  await store(result.submission); // answers, hidden, context, ending
@@ -102,16 +134,84 @@ Errors are keyed by field id. The `""` key is for the whole submission: `"Malfor
102
134
  client's log still replays. With `settings.clearHiddenValues: false`, a hidden field's value is kept
103
135
  only if its format is valid.
104
136
 
137
+ ## Destinations
138
+
139
+ `@marktiderman/genesis-forms/destinations` sends a checked submission somewhere. No dependencies:
140
+ fetch and WebCrypto only, so it runs on Node 20+, edge runtimes and Workers. Storage stays the
141
+ app's (the outbox, claims, attempts, the retry schedule); a destination delivers once and reports
142
+ what happened. It is a separate subpath, not part of `/server`, because `/server` does no IO and
143
+ this does: it is the one entry that touches the network and credentials.
144
+
145
+ ```ts
146
+ import { validateSubmission } from "@marktiderman/genesis-forms/server";
147
+ import {
148
+ emailDestination,
149
+ resendProvider,
150
+ sheetsDestination,
151
+ webhookDestination,
152
+ type Destination,
153
+ } from "@marktiderman/genesis-forms/destinations";
154
+
155
+ const destinations: Record<string, Destination> = {
156
+ email: emailDestination({
157
+ provider: resendProvider({ apiKey: env.RESEND_API_KEY }),
158
+ from: "Feedback <feedback@send.example.com>",
159
+ replyTo: "email", // the key of an email field; only a single valid address is used
160
+ }),
161
+ sheet: sheetsDestination({
162
+ account: { clientEmail: env.GOOGLE_SERVICE_ACCOUNT_EMAIL, privateKey: env.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY },
163
+ }),
164
+ webhook: webhookDestination({ secret: (config) => secretFor(config) }),
165
+ };
166
+
167
+ const checked = validateSubmission(definition, body);
168
+ if (checked.ok) {
169
+ const submission = { id: savedId, ...checked.submission, submittedAt: "2026-10-22", meta: { form_version: 3 } };
170
+ const result = await destinations.sheet.deliver(submission, definition, { spreadsheetId: "1AbC…" }, {
171
+ deliveryId, // stable across retries: the email idempotency key and the webhook-id
172
+ state: saved.state, // what the sheet cached last time
173
+ redelivery: claims > 1,
174
+ });
175
+ if (result.ok) saveState(result.state);
176
+ else if (result.retryable) retryLater(result.retryAfterSeconds); // else: needs a config fix
177
+ }
178
+ ```
179
+
180
+ **The contract.** `deliver(submission, definition, config, options?)` resolves a `DeliveryResult`
181
+ and never throws. Failures carry `retryable` (`true` for 408, 425, 429, 5xx, timeouts and network
182
+ errors; `false` for other statuses, bad config, a secret or account resolver that throws, and a
183
+ `TypeError` from a bug), `retryAfterSeconds` when the service said when to come back
184
+ (Retry-After as delta-seconds or an IMF-fixdate, else 60 s; at least 1 s, at most a day), the
185
+ `status`, and an `error` that is safe to
186
+ store and log: never a secret, an answer, contact details or the webhook URL. Config is plain JSON
187
+ (recipients, a spreadsheet id, a URL); secrets go to the factory, never in config. Every request
188
+ has a timeout (`timeoutMs`, default 10 s) and honours `options.signal`. `deliverBatch(items,
189
+ config)` (the sheet) sends many in one request; all succeed or fail together. Write your own by
190
+ implementing `Destination` and throwing `DeliveryError` inside; `deliveryFailure(error)` turns any
191
+ error into a safe result.
192
+
193
+ **Rows.** `responseRow(submission, definition)` flattens a submission: `id`, `submitted_at`, the
194
+ app's `meta` columns, then one column per field key in form order. Numbers stay numbers, choices
195
+ show their labels, lists join with `, `, yes/no reads `Yes`/`No`. Labels fill recall from the
196
+ submission. On an anonymous form identifying fields get no column. `sensitive` fields:
197
+ `"include"`, `"mask"` (`[sensitive]`) or `"omit"`; recall never leaks a masked value into a label.
198
+
199
+ | Destination | Config | Notes |
200
+ | --- | --- | --- |
201
+ | `emailDestination({ provider, from, replyTo?, render?, sensitive? })` | `{ to: string \| string[], subject? }` | HTML and plain text from the definition (`renderEmail`), every value HTML-escaped. Subject, From, To and Reply-To are stripped of line breaks or refused, so no answer can add a header. At most 50 recipients. Sensitive fields masked by default. The idempotency key is the delivery id plus a hash of the recipients. `resendProvider({ apiKey })` is built in; any `EmailProvider` works. `render` and `emailBody(lines)` let an app write its own content and keep the escaping. |
202
+ | `sheetsDestination({ account, escapeFormulas?, sensitive? })` | `{ spreadsheetId, sheetName? }` | Service-account JWT signed with WebCrypto (RS256), token cached per process. Creates the tab, writes and freezes the header, adds a column for a new key without moving others, appends with `valueInputOption=RAW`, so Sheets stores text exactly as typed and never runs it as a formula (`@team thanks` stays `@team thanks`). No `'` is added by default, since RAW would store it literally; `escapeFormulas: true` adds one (headers included) for a sheet that is exported and opened elsewhere. `escapeFormula(cell)` is the same opt-in guard for CSV you write yourself: text starting with `=`, `@`, tab or CR, or with `+`/`-` followed by anything but number punctuation, gets a leading `'`. Keeps the tab, header and last row in `state`, so a normal delivery is one request. On `redelivery`, skips ids already in the last 500 rows. A 403/404 says to share the sheet with the service account. |
203
+ | `webhookDestination({ secret, allowInsecureUrls?, allowPrivateHosts?, sensitive? })` | `{ url }` | [Standard Webhooks](https://www.standardwebhooks.com): `webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<HMAC-SHA256>`. The secret is base64 (at least 16 bytes decoded), usually `whsec_`-prefixed; as in every Standard Webhooks library the prefix is optional and the rest is always base64-decoded, so receivers verify with the same string. A secret that is not padded standard base64 (a raw passphrase, say) fails permanently rather than signing deliveries no receiver could verify. Generate one with `"whsec_" + btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))))`. Body `{ type: "form.submitted", timestamp, data: { form, submission, row } }`. https only, no URL credentials, no localhost or private, loopback or link-local address literals, redirects not followed. A hostname that resolves to a private address is not caught: if untrusted users pick URLs, send from a network that cannot reach internal services. Receivers check with `verifyWebhook({ secret, headers, body })` (constant-time, five-minute tolerance). |
204
+
105
205
  ## Testing
106
206
 
107
207
  ```ts
108
208
  import { coverage, recordScenario, runScenario, simulate } from "@marktiderman/genesis-forms/testing";
109
209
 
110
210
  // Walk a form by key.
111
- expect(simulate(afterClass).answer({ overall: 2 }).pathKeys()).toEqual(["overall", "improve", "contact_me"]);
211
+ expect(simulate(eventFeedback).answer({ overall: 2 }).pathKeys()).toEqual(["overall", "improve", "contact_me"]);
112
212
 
113
213
  // Every block reachable, every rule able to fire, every ending reached.
114
- const report = coverage(afterClass);
214
+ const report = coverage(eventFeedback);
115
215
  expect(report.unreachableBlocks).toEqual([]);
116
216
  expect(report.rulesNeverFired).toEqual([]);
117
217
  expect(report.endings.default.reached).toBe(true); // witness: answers that reach it
@@ -138,7 +238,7 @@ to the same field, keeping the last value; the result replays to the same state
138
238
  depends on values alone, not on how they were typed. `recordScenario` compacts by default (`compact: false` keeps every event).
139
239
 
140
240
  The **conformance suite** ships in the package: `@marktiderman/genesis-forms/conformance/*.scenario.json`
141
- (the R1 sample forms: after-class and monthly-pulse). Run each file through `runScenario` to hold
241
+ (the R1 sample forms: event-feedback and monthly-check-in). Run each file through `runScenario` to hold
142
242
  another runtime, a renderer or an app's CI to the same behaviour. The fixtures are generated by
143
243
  `pnpm --filter @marktiderman/genesis-forms conformance:build`; review the diff before committing.
144
244
 
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "scenarioVersion": 1,
3
- "name": "after-class: back after a jump",
3
+ "name": "event-feedback: back after a jump",
4
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
5
  "tags": [
6
- "after-class",
6
+ "event-feedback",
7
7
  "jump",
8
8
  "navigation"
9
9
  ],
10
10
  "definition": {
11
11
  "schemaVersion": 1,
12
- "id": "after-class",
13
- "title": "How was class?",
12
+ "id": "event-feedback",
13
+ "title": "How was the event?",
14
14
  "blocks": [
15
15
  {
16
16
  "kind": "field",
17
17
  "id": "overall",
18
18
  "key": "overall",
19
19
  "type": "rating",
20
- "label": "How was today's class?",
20
+ "label": "How was today's event?",
21
21
  "required": true,
22
22
  "props": {
23
23
  "max": 5
@@ -32,24 +32,24 @@
32
32
  "props": {
33
33
  "options": [
34
34
  {
35
- "value": "coaching",
36
- "label": "Coaching"
35
+ "value": "hosting",
36
+ "label": "Hosting"
37
37
  },
38
38
  {
39
- "value": "workout",
40
- "label": "The workout"
39
+ "value": "content",
40
+ "label": "The content"
41
41
  },
42
42
  {
43
43
  "value": "community",
44
44
  "label": "Community"
45
45
  },
46
46
  {
47
- "value": "scaling",
48
- "label": "Scaling options"
47
+ "value": "pacing",
48
+ "label": "Pacing"
49
49
  },
50
50
  {
51
- "value": "equipment",
52
- "label": "Equipment"
51
+ "value": "venue",
52
+ "label": "The venue"
53
53
  },
54
54
  {
55
55
  "value": "cleanliness",
@@ -70,7 +70,7 @@
70
70
  "id": "contact_me",
71
71
  "key": "contact_me",
72
72
  "type": "yes-no",
73
- "label": "Want {{hidden:coach|a coach}} to follow up with you?"
73
+ "label": "Want {{hidden:owner|someone}} to follow up with you?"
74
74
  },
75
75
  {
76
76
  "kind": "field",
@@ -94,12 +94,12 @@
94
94
  },
95
95
  "hidden": [
96
96
  {
97
- "name": "class",
97
+ "name": "session",
98
98
  "type": "string",
99
99
  "source": "server"
100
100
  },
101
101
  {
102
- "name": "coach",
102
+ "name": "owner",
103
103
  "type": "string",
104
104
  "source": "server"
105
105
  }
@@ -152,7 +152,7 @@
152
152
  "endings": [
153
153
  {
154
154
  "id": "default",
155
- "title": "Thanks! See you at the next class."
155
+ "title": "Thanks! See you next time."
156
156
  }
157
157
  ],
158
158
  "settings": {
@@ -160,8 +160,8 @@
160
160
  }
161
161
  },
162
162
  "context": {
163
- "class": "6:00 AM CrossFit",
164
- "coach": "Sam"
163
+ "session": "Tuesday 6 PM workshop",
164
+ "owner": "Sam"
165
165
  },
166
166
  "events": [
167
167
  {
@@ -209,8 +209,8 @@
209
209
  "overall": 5
210
210
  },
211
211
  "context": {
212
- "class": "6:00 AM CrossFit",
213
- "coach": "Sam"
212
+ "session": "Tuesday 6 PM workshop",
213
+ "owner": "Sam"
214
214
  }
215
215
  }
216
216
  }
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "scenarioVersion": 1,
3
- "name": "after-class: contact yes, completed",
3
+ "name": "event-feedback: contact yes, completed",
4
4
  "description": "With name and email filled in, the follow-up request completes and both are stored.",
5
5
  "tags": [
6
- "after-class",
6
+ "event-feedback",
7
7
  "show-hide"
8
8
  ],
9
9
  "definition": {
10
10
  "schemaVersion": 1,
11
- "id": "after-class",
12
- "title": "How was class?",
11
+ "id": "event-feedback",
12
+ "title": "How was the event?",
13
13
  "blocks": [
14
14
  {
15
15
  "kind": "field",
16
16
  "id": "overall",
17
17
  "key": "overall",
18
18
  "type": "rating",
19
- "label": "How was today's class?",
19
+ "label": "How was today's event?",
20
20
  "required": true,
21
21
  "props": {
22
22
  "max": 5
@@ -31,24 +31,24 @@
31
31
  "props": {
32
32
  "options": [
33
33
  {
34
- "value": "coaching",
35
- "label": "Coaching"
34
+ "value": "hosting",
35
+ "label": "Hosting"
36
36
  },
37
37
  {
38
- "value": "workout",
39
- "label": "The workout"
38
+ "value": "content",
39
+ "label": "The content"
40
40
  },
41
41
  {
42
42
  "value": "community",
43
43
  "label": "Community"
44
44
  },
45
45
  {
46
- "value": "scaling",
47
- "label": "Scaling options"
46
+ "value": "pacing",
47
+ "label": "Pacing"
48
48
  },
49
49
  {
50
- "value": "equipment",
51
- "label": "Equipment"
50
+ "value": "venue",
51
+ "label": "The venue"
52
52
  },
53
53
  {
54
54
  "value": "cleanliness",
@@ -69,7 +69,7 @@
69
69
  "id": "contact_me",
70
70
  "key": "contact_me",
71
71
  "type": "yes-no",
72
- "label": "Want {{hidden:coach|a coach}} to follow up with you?"
72
+ "label": "Want {{hidden:owner|someone}} to follow up with you?"
73
73
  },
74
74
  {
75
75
  "kind": "field",
@@ -93,12 +93,12 @@
93
93
  },
94
94
  "hidden": [
95
95
  {
96
- "name": "class",
96
+ "name": "session",
97
97
  "type": "string",
98
98
  "source": "server"
99
99
  },
100
100
  {
101
- "name": "coach",
101
+ "name": "owner",
102
102
  "type": "string",
103
103
  "source": "server"
104
104
  }
@@ -151,7 +151,7 @@
151
151
  "endings": [
152
152
  {
153
153
  "id": "default",
154
- "title": "Thanks! See you at the next class."
154
+ "title": "Thanks! See you next time."
155
155
  }
156
156
  ],
157
157
  "settings": {
@@ -159,8 +159,8 @@
159
159
  }
160
160
  },
161
161
  "context": {
162
- "class": "6:00 AM CrossFit",
163
- "coach": "Sam"
162
+ "session": "Tuesday 6 PM workshop",
163
+ "owner": "Sam"
164
164
  },
165
165
  "events": [
166
166
  {
@@ -222,8 +222,8 @@
222
222
  "email": "ada@example.com"
223
223
  },
224
224
  "context": {
225
- "class": "6:00 AM CrossFit",
226
- "coach": "Sam"
225
+ "session": "Tuesday 6 PM workshop",
226
+ "owner": "Sam"
227
227
  }
228
228
  }
229
229
  }
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "scenarioVersion": 1,
3
- "name": "after-class: contact yes requires name and email",
3
+ "name": "event-feedback: contact yes requires name and email",
4
4
  "description": "Answering Yes to contact_me reveals name and email, and both are required on submit and on the server.",
5
5
  "tags": [
6
- "after-class",
6
+ "event-feedback",
7
7
  "show-hide",
8
8
  "validation"
9
9
  ],
10
10
  "definition": {
11
11
  "schemaVersion": 1,
12
- "id": "after-class",
13
- "title": "How was class?",
12
+ "id": "event-feedback",
13
+ "title": "How was the event?",
14
14
  "blocks": [
15
15
  {
16
16
  "kind": "field",
17
17
  "id": "overall",
18
18
  "key": "overall",
19
19
  "type": "rating",
20
- "label": "How was today's class?",
20
+ "label": "How was today's event?",
21
21
  "required": true,
22
22
  "props": {
23
23
  "max": 5
@@ -32,24 +32,24 @@
32
32
  "props": {
33
33
  "options": [
34
34
  {
35
- "value": "coaching",
36
- "label": "Coaching"
35
+ "value": "hosting",
36
+ "label": "Hosting"
37
37
  },
38
38
  {
39
- "value": "workout",
40
- "label": "The workout"
39
+ "value": "content",
40
+ "label": "The content"
41
41
  },
42
42
  {
43
43
  "value": "community",
44
44
  "label": "Community"
45
45
  },
46
46
  {
47
- "value": "scaling",
48
- "label": "Scaling options"
47
+ "value": "pacing",
48
+ "label": "Pacing"
49
49
  },
50
50
  {
51
- "value": "equipment",
52
- "label": "Equipment"
51
+ "value": "venue",
52
+ "label": "The venue"
53
53
  },
54
54
  {
55
55
  "value": "cleanliness",
@@ -70,7 +70,7 @@
70
70
  "id": "contact_me",
71
71
  "key": "contact_me",
72
72
  "type": "yes-no",
73
- "label": "Want {{hidden:coach|a coach}} to follow up with you?"
73
+ "label": "Want {{hidden:owner|someone}} to follow up with you?"
74
74
  },
75
75
  {
76
76
  "kind": "field",
@@ -94,12 +94,12 @@
94
94
  },
95
95
  "hidden": [
96
96
  {
97
- "name": "class",
97
+ "name": "session",
98
98
  "type": "string",
99
99
  "source": "server"
100
100
  },
101
101
  {
102
- "name": "coach",
102
+ "name": "owner",
103
103
  "type": "string",
104
104
  "source": "server"
105
105
  }
@@ -152,7 +152,7 @@
152
152
  "endings": [
153
153
  {
154
154
  "id": "default",
155
- "title": "Thanks! See you at the next class."
155
+ "title": "Thanks! See you next time."
156
156
  }
157
157
  ],
158
158
  "settings": {
@@ -160,8 +160,8 @@
160
160
  }
161
161
  },
162
162
  "context": {
163
- "class": "6:00 AM CrossFit",
164
- "coach": "Sam"
163
+ "session": "Tuesday 6 PM workshop",
164
+ "owner": "Sam"
165
165
  },
166
166
  "events": [
167
167
  {
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "scenarioVersion": 1,
3
- "name": "after-class: happy path",
3
+ "name": "event-feedback: happy path",
4
4
  "description": "Five stars, no follow-up wanted. Name and email stay hidden and are not required.",
5
5
  "tags": [
6
- "after-class",
6
+ "event-feedback",
7
7
  "happy-path"
8
8
  ],
9
9
  "definition": {
10
10
  "schemaVersion": 1,
11
- "id": "after-class",
12
- "title": "How was class?",
11
+ "id": "event-feedback",
12
+ "title": "How was the event?",
13
13
  "blocks": [
14
14
  {
15
15
  "kind": "field",
16
16
  "id": "overall",
17
17
  "key": "overall",
18
18
  "type": "rating",
19
- "label": "How was today's class?",
19
+ "label": "How was today's event?",
20
20
  "required": true,
21
21
  "props": {
22
22
  "max": 5
@@ -31,24 +31,24 @@
31
31
  "props": {
32
32
  "options": [
33
33
  {
34
- "value": "coaching",
35
- "label": "Coaching"
34
+ "value": "hosting",
35
+ "label": "Hosting"
36
36
  },
37
37
  {
38
- "value": "workout",
39
- "label": "The workout"
38
+ "value": "content",
39
+ "label": "The content"
40
40
  },
41
41
  {
42
42
  "value": "community",
43
43
  "label": "Community"
44
44
  },
45
45
  {
46
- "value": "scaling",
47
- "label": "Scaling options"
46
+ "value": "pacing",
47
+ "label": "Pacing"
48
48
  },
49
49
  {
50
- "value": "equipment",
51
- "label": "Equipment"
50
+ "value": "venue",
51
+ "label": "The venue"
52
52
  },
53
53
  {
54
54
  "value": "cleanliness",
@@ -69,7 +69,7 @@
69
69
  "id": "contact_me",
70
70
  "key": "contact_me",
71
71
  "type": "yes-no",
72
- "label": "Want {{hidden:coach|a coach}} to follow up with you?"
72
+ "label": "Want {{hidden:owner|someone}} to follow up with you?"
73
73
  },
74
74
  {
75
75
  "kind": "field",
@@ -93,12 +93,12 @@
93
93
  },
94
94
  "hidden": [
95
95
  {
96
- "name": "class",
96
+ "name": "session",
97
97
  "type": "string",
98
98
  "source": "server"
99
99
  },
100
100
  {
101
- "name": "coach",
101
+ "name": "owner",
102
102
  "type": "string",
103
103
  "source": "server"
104
104
  }
@@ -151,7 +151,7 @@
151
151
  "endings": [
152
152
  {
153
153
  "id": "default",
154
- "title": "Thanks! See you at the next class."
154
+ "title": "Thanks! See you next time."
155
155
  }
156
156
  ],
157
157
  "settings": {
@@ -159,8 +159,8 @@
159
159
  }
160
160
  },
161
161
  "context": {
162
- "class": "6:00 AM CrossFit",
163
- "coach": "Sam"
162
+ "session": "Tuesday 6 PM workshop",
163
+ "owner": "Sam"
164
164
  },
165
165
  "events": [
166
166
  {
@@ -175,7 +175,7 @@
175
175
  "type": "answer",
176
176
  "field": "went_well",
177
177
  "value": [
178
- "coaching",
178
+ "hosting",
179
179
  "community"
180
180
  ]
181
181
  },
@@ -213,7 +213,7 @@
213
213
  "answers": {
214
214
  "overall": 5,
215
215
  "went_well": [
216
- "coaching",
216
+ "hosting",
217
217
  "community"
218
218
  ],
219
219
  "contact_me": false
@@ -223,14 +223,14 @@
223
223
  "answers": {
224
224
  "overall": 5,
225
225
  "went_well": [
226
- "coaching",
226
+ "hosting",
227
227
  "community"
228
228
  ],
229
229
  "contact_me": false
230
230
  },
231
231
  "context": {
232
- "class": "6:00 AM CrossFit",
233
- "coach": "Sam"
232
+ "session": "Tuesday 6 PM workshop",
233
+ "owner": "Sam"
234
234
  }
235
235
  }
236
236
  }