@marktiderman/genesis-forms 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -13
- package/conformance/{after-class-back-after-a-jump.scenario.json → event-feedback-back-after-a-jump.scenario.json} +21 -21
- package/conformance/{after-class-contact-yes-completed.scenario.json → event-feedback-contact-yes-completed.scenario.json} +21 -21
- package/conformance/{after-class-contact-yes-requires-name-and-email.scenario.json → event-feedback-contact-yes-requires-name-and-email.scenario.json} +19 -19
- package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json} +24 -24
- package/conformance/{after-class-invalid-email-rejected.scenario.json → event-feedback-invalid-email-rejected.scenario.json} +19 -19
- package/conformance/{after-class-low-rating-skips-went-well.scenario.json → event-feedback-low-rating-skips-went-well.scenario.json} +21 -21
- package/conformance/{after-class-server-drops-unknown-and-hidden-answers.scenario.json → event-feedback-server-drops-unknown-and-hidden-answers.scenario.json} +22 -22
- package/conformance/{after-class-server-ignores-a-client-sent-coach.scenario.json → event-feedback-server-ignores-a-client-sent-owner.scenario.json} +24 -24
- package/conformance/{monthly-pulse-default-ending.scenario.json → monthly-check-in-default-ending.scenario.json} +6 -6
- package/conformance/{monthly-pulse-promoter-ending-via-jump.scenario.json → monthly-check-in-promoter-ending-via-jump.scenario.json} +6 -6
- package/dist/destinations.cjs +1211 -0
- package/dist/destinations.cjs.map +1 -0
- package/dist/destinations.d.cts +465 -0
- package/dist/destinations.d.ts +465 -0
- package/dist/destinations.js +1169 -0
- package/dist/destinations.js.map +1 -0
- package/dist/{engine-Coc-gQIB.d.ts → engine-CPLtKVew.d.cts} +2 -1
- package/dist/{engine-Bj8-mMVb.d.cts → engine-DOrPgFDw.d.ts} +2 -1
- package/dist/index.cjs +320 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -5
- package/dist/index.d.ts +102 -5
- package/dist/index.js +309 -25
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +247 -22
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +3 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +247 -22
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +251 -22
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -3
- package/dist/testing.d.ts +4 -3
- package/dist/testing.js +251 -22
- package/dist/testing.js.map +1 -1
- package/dist/{validate-BbackAjB.d.cts → types-BBKBQ8sp.d.cts} +2 -29
- package/dist/{validate-BbackAjB.d.ts → types-BBKBQ8sp.d.ts} +2 -29
- package/dist/validate-BXXqi0fq.d.cts +30 -0
- package/dist/validate-BjNwF1UE.d.ts +30 -0
- 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
|
|
16
|
-
id: "
|
|
17
|
-
title: "How was
|
|
15
|
+
export const eventFeedback = defineForm({
|
|
16
|
+
id: "event-feedback",
|
|
17
|
+
title: "How was the event?",
|
|
18
18
|
layout: { mode: "focus" },
|
|
19
|
-
hidden: {
|
|
19
|
+
hidden: { owner: f.hidden.string({ source: "server" }) },
|
|
20
20
|
blocks: [
|
|
21
|
-
f.rating("overall", { label: "How was today's
|
|
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:
|
|
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
|
|
31
|
-
const json =
|
|
32
|
-
const version =
|
|
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: {
|
|
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(
|
|
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(
|
|
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:
|
|
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": "
|
|
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
|
-
"
|
|
6
|
+
"event-feedback",
|
|
7
7
|
"jump",
|
|
8
8
|
"navigation"
|
|
9
9
|
],
|
|
10
10
|
"definition": {
|
|
11
11
|
"schemaVersion": 1,
|
|
12
|
-
"id": "
|
|
13
|
-
"title": "How was
|
|
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
|
|
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": "
|
|
36
|
-
"label": "
|
|
35
|
+
"value": "hosting",
|
|
36
|
+
"label": "Hosting"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
"value": "
|
|
40
|
-
"label": "The
|
|
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": "
|
|
48
|
-
"label": "
|
|
47
|
+
"value": "pacing",
|
|
48
|
+
"label": "Pacing"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
-
"value": "
|
|
52
|
-
"label": "
|
|
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:
|
|
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": "
|
|
97
|
+
"name": "session",
|
|
98
98
|
"type": "string",
|
|
99
99
|
"source": "server"
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
|
-
"name": "
|
|
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
|
|
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
|
-
"
|
|
164
|
-
"
|
|
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
|
-
"
|
|
213
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
6
|
+
"event-feedback",
|
|
7
7
|
"show-hide"
|
|
8
8
|
],
|
|
9
9
|
"definition": {
|
|
10
10
|
"schemaVersion": 1,
|
|
11
|
-
"id": "
|
|
12
|
-
"title": "How was
|
|
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
|
|
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": "
|
|
35
|
-
"label": "
|
|
34
|
+
"value": "hosting",
|
|
35
|
+
"label": "Hosting"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
|
-
"value": "
|
|
39
|
-
"label": "The
|
|
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": "
|
|
47
|
-
"label": "
|
|
46
|
+
"value": "pacing",
|
|
47
|
+
"label": "Pacing"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
|
-
"value": "
|
|
51
|
-
"label": "
|
|
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:
|
|
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": "
|
|
96
|
+
"name": "session",
|
|
97
97
|
"type": "string",
|
|
98
98
|
"source": "server"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
"name": "
|
|
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
|
|
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
|
-
"
|
|
163
|
-
"
|
|
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
|
-
"
|
|
226
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
6
|
+
"event-feedback",
|
|
7
7
|
"show-hide",
|
|
8
8
|
"validation"
|
|
9
9
|
],
|
|
10
10
|
"definition": {
|
|
11
11
|
"schemaVersion": 1,
|
|
12
|
-
"id": "
|
|
13
|
-
"title": "How was
|
|
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
|
|
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": "
|
|
36
|
-
"label": "
|
|
35
|
+
"value": "hosting",
|
|
36
|
+
"label": "Hosting"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
"value": "
|
|
40
|
-
"label": "The
|
|
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": "
|
|
48
|
-
"label": "
|
|
47
|
+
"value": "pacing",
|
|
48
|
+
"label": "Pacing"
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
-
"value": "
|
|
52
|
-
"label": "
|
|
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:
|
|
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": "
|
|
97
|
+
"name": "session",
|
|
98
98
|
"type": "string",
|
|
99
99
|
"source": "server"
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
|
-
"name": "
|
|
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
|
|
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
|
-
"
|
|
164
|
-
"
|
|
163
|
+
"session": "Tuesday 6 PM workshop",
|
|
164
|
+
"owner": "Sam"
|
|
165
165
|
},
|
|
166
166
|
"events": [
|
|
167
167
|
{
|
package/conformance/{after-class-happy-path.scenario.json → event-feedback-happy-path.scenario.json}
RENAMED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"scenarioVersion": 1,
|
|
3
|
-
"name": "
|
|
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
|
-
"
|
|
6
|
+
"event-feedback",
|
|
7
7
|
"happy-path"
|
|
8
8
|
],
|
|
9
9
|
"definition": {
|
|
10
10
|
"schemaVersion": 1,
|
|
11
|
-
"id": "
|
|
12
|
-
"title": "How was
|
|
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
|
|
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": "
|
|
35
|
-
"label": "
|
|
34
|
+
"value": "hosting",
|
|
35
|
+
"label": "Hosting"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
|
-
"value": "
|
|
39
|
-
"label": "The
|
|
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": "
|
|
47
|
-
"label": "
|
|
46
|
+
"value": "pacing",
|
|
47
|
+
"label": "Pacing"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
|
-
"value": "
|
|
51
|
-
"label": "
|
|
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:
|
|
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": "
|
|
96
|
+
"name": "session",
|
|
97
97
|
"type": "string",
|
|
98
98
|
"source": "server"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
"name": "
|
|
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
|
|
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
|
-
"
|
|
163
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
226
|
+
"hosting",
|
|
227
227
|
"community"
|
|
228
228
|
],
|
|
229
229
|
"contact_me": false
|
|
230
230
|
},
|
|
231
231
|
"context": {
|
|
232
|
-
"
|
|
233
|
-
"
|
|
232
|
+
"session": "Tuesday 6 PM workshop",
|
|
233
|
+
"owner": "Sam"
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
}
|