@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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/AGENTS.md +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +14 -5
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +9 -9
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- package/src/changes.json +251 -8
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/wizard-form. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# wizard-form
|
|
4
|
+
|
|
5
|
+
EditLayout.mode: "wizard" — a multi-step entity form with a review step that reads live form values through an extension section, and draft: true resuming the in-progress form via the bundled form-draft feature.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# Wizard form
|
|
10
|
+
|
|
11
|
+
An `entityEdit` screen with `EditLayout.mode: "wizard"` — one section per
|
|
12
|
+
step instead of one long form. The final step is a review section
|
|
13
|
+
(`kind: "extension"`) that reads the wizard's live values instead of
|
|
14
|
+
re-fetching them, and `draft: true` resumes an abandoned wizard from where
|
|
15
|
+
the user left off.
|
|
16
|
+
|
|
17
|
+
## What it shows
|
|
18
|
+
|
|
19
|
+
- **`mode: "wizard"`** — the same three sections a `"single"` layout would
|
|
20
|
+
render all at once instead render one per step, with a progress
|
|
21
|
+
indicator and per-step validation. The boot-validator requires at least
|
|
22
|
+
two sections, each with a non-empty `title`.
|
|
23
|
+
- **A review step as an extension section** — the third step
|
|
24
|
+
(`kind: "extension"`) mounts `ListingReviewSection`, a client component
|
|
25
|
+
resolved by the `__component` name at render time. It receives the host
|
|
26
|
+
form's current values through `ExtensionSectionProps.values` (the same
|
|
27
|
+
live snapshot the other steps edit) and renders them read-only via
|
|
28
|
+
`DetailList` — no second fetch, no duplicated state.
|
|
29
|
+
- **`draft: true`** — persists the in-progress wizard as a resumable draft.
|
|
30
|
+
This flag only does two things in the feature: it requires
|
|
31
|
+
`mode: "wizard"` and it requires the bundled `form-draft` feature to be
|
|
32
|
+
mounted alongside this one (both enforced at boot). The actual
|
|
33
|
+
save/resume/discard wiring happens automatically inside `RenderEdit` —
|
|
34
|
+
this recipe never calls a form-draft handler from its own code.
|
|
35
|
+
|
|
36
|
+
## Feature composition
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
listing — the entity being created through the wizard
|
|
40
|
+
form-draft — bundled feature backing draft: true (mounted, never called
|
|
41
|
+
directly by this feature — RenderEdit wires it client-side)
|
|
42
|
+
config — form-draft requires this for its retention-days setting;
|
|
43
|
+
mount createConfigFeature() alongside form-draft
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Flow
|
|
47
|
+
|
|
48
|
+
1. A user opens the `listing-wizard` screen and fills in the "Basics"
|
|
49
|
+
step, then "Pricing".
|
|
50
|
+
2. If they navigate away mid-wizard, `RenderEdit` has already saved a
|
|
51
|
+
draft under a client-minted `${screen.id}:new:${draftId}` key
|
|
52
|
+
(sessionStorage-backed); reopening the same tab resumes those values
|
|
53
|
+
instead of starting over. A new tab has no `draftId` yet — it falls
|
|
54
|
+
back to `form-draft:query:list` and either auto-adopts a single
|
|
55
|
+
in-progress draft or offers a picker when there's more than one.
|
|
56
|
+
3. On the "Review" step, `ListingReviewSection` reads the values entered
|
|
57
|
+
so far straight from the host form and displays them read-only — no
|
|
58
|
+
extra network round-trip.
|
|
59
|
+
4. Submitting creates the `listing` through the standard CRUD create and
|
|
60
|
+
discards the draft.
|
|
61
|
+
|
|
62
|
+
## When to reach for it
|
|
63
|
+
|
|
64
|
+
A form is long enough that one page of fields overwhelms the user, or
|
|
65
|
+
losing everything on an accidental tab close is a real cost — onboarding,
|
|
66
|
+
a multi-part application, anything a user is likely to fill in over more
|
|
67
|
+
than one sitting. Use a `"single"`-mode layout instead when the form is
|
|
68
|
+
short enough to fit on one screen; wizard mode's per-step validation and
|
|
69
|
+
draft persistence are overhead a short form doesn't need.
|
|
70
|
+
|
|
71
|
+
## Source
|
|
72
|
+
|
|
73
|
+
The feature entry point is `src/feature.ts`; the review-step component is
|
|
74
|
+
`src/web/listing-review-section.tsx`. `src/web/index.ts` exports
|
|
75
|
+
`listingsClient`, a `ClientFeatureDefinition` that registers
|
|
76
|
+
`ListingReviewSection` under the `__component` name `feature.ts`'s review
|
|
77
|
+
section points at — an app mounting this recipe must include
|
|
78
|
+
`listingsClient` in `createKumikoApp`'s `clientFeatures`, or the review
|
|
79
|
+
step falls back to `RenderEdit`'s unregistered-extension placeholder
|
|
80
|
+
instead of rendering the real component. The integration test under
|
|
81
|
+
`src/__tests__/` covers the entity's CRUD create through the wizard's
|
|
82
|
+
fields and the form-draft save/get/discard round-trip that backs
|
|
83
|
+
`draft: true`. `e2e/wizard.spec.ts` is a Playwright spec against the real
|
|
84
|
+
client bundle (mounted via `listingsClient`, the same registration an app
|
|
85
|
+
would use) — step navigation, per-step validation blocking Next on empty required fields,
|
|
86
|
+
draft-resume after a page reload, and that a successful submit discards
|
|
87
|
+
the draft (reopening the wizard starts fresh on step 1).
|
|
88
|
+
|
|
89
|
+
## Source files
|
|
90
|
+
|
|
91
|
+
### src/__tests__/feature.integration.test.ts
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// Wizard Form Sample — Integration Test
|
|
95
|
+
// Proves: the listing entity + wizard-mode screen boot together (the
|
|
96
|
+
// boot-validator's wizard rules — >= 2 titled sections, draft: true only
|
|
97
|
+
// with mode: "wizard" and form-draft mounted — all pass for this feature
|
|
98
|
+
// combination), the entity's CRUD create works end-to-end, and the
|
|
99
|
+
// form-draft save/get/discard round-trip that backs `draft: true` behaves
|
|
100
|
+
// exactly as a resumed-then-submitted wizard would use it.
|
|
101
|
+
|
|
102
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
103
|
+
import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
104
|
+
import {
|
|
105
|
+
FormDraftHandlers,
|
|
106
|
+
FormDraftQueries,
|
|
107
|
+
formDraftEntity,
|
|
108
|
+
formDraftFeature,
|
|
109
|
+
} from "@cosmicdrift/kumiko-bundled-features/form-draft";
|
|
110
|
+
import type { SaveContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
111
|
+
import {
|
|
112
|
+
createTestUser,
|
|
113
|
+
setupTestStack,
|
|
114
|
+
type TestStack,
|
|
115
|
+
TestUsers,
|
|
116
|
+
unsafeCreateEntityTable,
|
|
117
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
118
|
+
import { listingEntity, listingsFeature } from "../feature";
|
|
119
|
+
|
|
120
|
+
let stack: TestStack;
|
|
121
|
+
|
|
122
|
+
const admin = TestUsers.admin;
|
|
123
|
+
const otherUser = createTestUser({ id: 2, roles: ["Admin"] });
|
|
124
|
+
|
|
125
|
+
// Arbitrary draftKey for exercising the handler contract directly — this
|
|
126
|
+
// suite calls FormDraftHandlers/FormDraftQueries with an explicit key, it
|
|
127
|
+
// doesn't go through RenderEdit's client-side derivation (which mints its
|
|
128
|
+
// own `${screenId}:new:${draftId}` for a create-mode wizard, issue #1913).
|
|
129
|
+
const DRAFT_KEY = "listing-wizard";
|
|
130
|
+
|
|
131
|
+
beforeAll(async () => {
|
|
132
|
+
// form-draft requires the "config" feature (its retention-days setting).
|
|
133
|
+
stack = await setupTestStack({
|
|
134
|
+
features: [listingsFeature, formDraftFeature, createConfigFeature()],
|
|
135
|
+
});
|
|
136
|
+
await unsafeCreateEntityTable(stack.db, listingEntity);
|
|
137
|
+
await unsafeCreateEntityTable(stack.db, formDraftEntity);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
afterAll(async () => {
|
|
141
|
+
await stack.cleanup();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("listing:create", () => {
|
|
145
|
+
test("creates a listing with the wizard's fields", async () => {
|
|
146
|
+
const created = await stack.http.writeOk<SaveContext>(
|
|
147
|
+
"listings:write:listing:create",
|
|
148
|
+
{ title: "Vintage desk lamp", category: "furniture", price: 42, condition: "used" },
|
|
149
|
+
admin,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(created.data["title"]).toBe("Vintage desk lamp");
|
|
153
|
+
expect(created.data["category"]).toBe("furniture");
|
|
154
|
+
expect(created.data["price"]).toBe(42);
|
|
155
|
+
expect(created.data["condition"]).toBe("used");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("condition defaults to used when omitted", async () => {
|
|
159
|
+
const created = await stack.http.writeOk<SaveContext>(
|
|
160
|
+
"listings:write:listing:create",
|
|
161
|
+
{ title: "Old bicycle", category: "vehicles", price: 80 },
|
|
162
|
+
admin,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(created.data["condition"]).toBe("used");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("wizard draft resume", () => {
|
|
170
|
+
test("save persists the in-progress steps, get resumes them", async () => {
|
|
171
|
+
await stack.http.writeOk(
|
|
172
|
+
FormDraftHandlers.save,
|
|
173
|
+
{
|
|
174
|
+
draftKey: DRAFT_KEY,
|
|
175
|
+
values: { title: "Draft title", category: "electronics" },
|
|
176
|
+
stepIndex: 1,
|
|
177
|
+
},
|
|
178
|
+
admin,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const resumed = await stack.http.queryOk<{ draft: { values: Record<string, unknown> } | null }>(
|
|
182
|
+
FormDraftQueries.get,
|
|
183
|
+
{ draftKey: DRAFT_KEY },
|
|
184
|
+
admin,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(resumed.draft?.values["title"]).toBe("Draft title");
|
|
188
|
+
expect(resumed.draft?.values["category"]).toBe("electronics");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("a later save overwrites the earlier one, not duplicates it", async () => {
|
|
192
|
+
const key = "listing-wizard:overwrite";
|
|
193
|
+
await stack.http.writeOk(
|
|
194
|
+
FormDraftHandlers.save,
|
|
195
|
+
{ draftKey: key, values: { title: "First pass" }, stepIndex: 0 },
|
|
196
|
+
admin,
|
|
197
|
+
);
|
|
198
|
+
await stack.http.writeOk(
|
|
199
|
+
FormDraftHandlers.save,
|
|
200
|
+
{ draftKey: key, values: { title: "Second pass", category: "other" }, stepIndex: 1 },
|
|
201
|
+
admin,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const resumed = await stack.http.queryOk<{ draft: { values: Record<string, unknown> } | null }>(
|
|
205
|
+
FormDraftQueries.get,
|
|
206
|
+
{ draftKey: key },
|
|
207
|
+
admin,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(resumed.draft?.values["title"]).toBe("Second pass");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("discard clears the draft — the next get resolves null, same as a completed submit", async () => {
|
|
214
|
+
const key = "listing-wizard:discard";
|
|
215
|
+
await stack.http.writeOk(
|
|
216
|
+
FormDraftHandlers.save,
|
|
217
|
+
{ draftKey: key, values: { title: "About to submit" }, stepIndex: 2 },
|
|
218
|
+
admin,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
await stack.http.writeOk(FormDraftHandlers.discard, { draftKey: key }, admin);
|
|
222
|
+
|
|
223
|
+
const afterDiscard = await stack.http.queryOk<{ draft: unknown }>(
|
|
224
|
+
FormDraftQueries.get,
|
|
225
|
+
{ draftKey: key },
|
|
226
|
+
admin,
|
|
227
|
+
);
|
|
228
|
+
expect(afterDiscard.draft).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("a foreign user's get never resolves another user's draft", async () => {
|
|
232
|
+
const key = "listing-wizard:isolated";
|
|
233
|
+
await stack.http.writeOk(
|
|
234
|
+
FormDraftHandlers.save,
|
|
235
|
+
{ draftKey: key, values: { title: "Only mine" }, stepIndex: 0 },
|
|
236
|
+
admin,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const foreign = await stack.http.queryOk<{ draft: unknown }>(
|
|
240
|
+
FormDraftQueries.get,
|
|
241
|
+
{ draftKey: key },
|
|
242
|
+
otherUser,
|
|
243
|
+
);
|
|
244
|
+
expect(foreign.draft).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### src/entities/listing.ts
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import {
|
|
253
|
+
createEntity,
|
|
254
|
+
createNumberField,
|
|
255
|
+
createSelectField,
|
|
256
|
+
createTextField,
|
|
257
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
258
|
+
|
|
259
|
+
export const listingEntity = createEntity({
|
|
260
|
+
table: "read_sample_listings",
|
|
261
|
+
fields: {
|
|
262
|
+
title: createTextField({
|
|
263
|
+
personal: false,
|
|
264
|
+
reason: "is_business_data",
|
|
265
|
+
required: true,
|
|
266
|
+
searchable: true,
|
|
267
|
+
}),
|
|
268
|
+
category: createSelectField({
|
|
269
|
+
options: ["electronics", "furniture", "vehicles", "other"] as const,
|
|
270
|
+
required: true,
|
|
271
|
+
}),
|
|
272
|
+
price: createNumberField({ required: true }),
|
|
273
|
+
condition: createSelectField({
|
|
274
|
+
options: ["new", "used"] as const,
|
|
275
|
+
default: "used",
|
|
276
|
+
}),
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### src/feature.ts
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
// Wizard Form Sample
|
|
285
|
+
// Shows: EditLayout.mode: "wizard" splitting one entityEdit screen into
|
|
286
|
+
// steps, a review step (kind: "extension") that reads the host form's
|
|
287
|
+
// live values through ExtensionSectionProps.values instead of its own
|
|
288
|
+
// fetch, and draft: true resuming an in-progress wizard via the bundled
|
|
289
|
+
// form-draft feature — the actual save/get/discard wiring is automatic
|
|
290
|
+
// client-side, this feature only turns the two flags on. form-draft itself
|
|
291
|
+
// requires the bundled "config" feature (its retention-days setting), so
|
|
292
|
+
// any app mounting this recipe must mount that too.
|
|
293
|
+
|
|
294
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
295
|
+
import { listingEntity } from "./entities/listing";
|
|
296
|
+
|
|
297
|
+
export { listingEntity } from "./entities/listing";
|
|
298
|
+
|
|
299
|
+
const editorWrite = { access: { roles: ["Admin", "User"] } } as const;
|
|
300
|
+
const openRead = {
|
|
301
|
+
access: {
|
|
302
|
+
openToAll: {
|
|
303
|
+
reason:
|
|
304
|
+
"any signed-in user may list and view listings; writes are still gated by editorWrite",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
} as const;
|
|
308
|
+
|
|
309
|
+
export const listingsFeature = defineFeature("listings", (r) => {
|
|
310
|
+
r.crud("listing", listingEntity, { write: editorWrite, read: openRead });
|
|
311
|
+
|
|
312
|
+
r.screen({
|
|
313
|
+
id: "listing-wizard",
|
|
314
|
+
type: "entityEdit",
|
|
315
|
+
entity: "listing",
|
|
316
|
+
layout: {
|
|
317
|
+
mode: "wizard",
|
|
318
|
+
draft: true,
|
|
319
|
+
sections: [
|
|
320
|
+
{ title: "Basics", fields: ["title", "category"] },
|
|
321
|
+
{ title: "Pricing", fields: ["price", "condition"] },
|
|
322
|
+
{
|
|
323
|
+
kind: "extension",
|
|
324
|
+
title: "Review",
|
|
325
|
+
component: { react: { __component: "ListingReviewSection" } },
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
access: editorWrite.access,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### src/web/index.ts
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
// Client-side registration for the review step's extension section (see
|
|
338
|
+
// ../feature.ts's `component: { react: { __component: "ListingReviewSection" } }`).
|
|
339
|
+
// A `kind: "extension"` section only resolves through
|
|
340
|
+
// clientFeatures.extensionSectionComponents (createKumikoApp → ExtensionSectionsProvider)
|
|
341
|
+
// — never imported directly from feature.ts, which keeps the server file
|
|
342
|
+
// free of the renderer-web graph. Without registering this export, an app
|
|
343
|
+
// mounting this recipe gets RenderEdit's fallback placeholder instead of
|
|
344
|
+
// the real review step.
|
|
345
|
+
|
|
346
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
347
|
+
import { ListingReviewSection } from "./listing-review-section";
|
|
348
|
+
|
|
349
|
+
export const listingsClient: ClientFeatureDefinition = {
|
|
350
|
+
name: "listings",
|
|
351
|
+
extensionSectionComponents: { ListingReviewSection },
|
|
352
|
+
};
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### src/web/listing-review-section.tsx
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
import type { ExtensionSectionProps } from "@cosmicdrift/kumiko-renderer";
|
|
359
|
+
import { DetailList } from "@cosmicdrift/kumiko-renderer-web";
|
|
360
|
+
import type { ReactNode } from "react";
|
|
361
|
+
|
|
362
|
+
// Registered under the `__component` name the feature's review section
|
|
363
|
+
// points at (see feature.ts) — never imported by feature.ts itself, the
|
|
364
|
+
// string name is the only link so the server file stays free of the
|
|
365
|
+
// renderer-web graph.
|
|
366
|
+
export function ListingReviewSection({ values }: ExtensionSectionProps): ReactNode {
|
|
367
|
+
return (
|
|
368
|
+
<DetailList
|
|
369
|
+
testId="listing-review"
|
|
370
|
+
rows={[
|
|
371
|
+
{ label: "Title", value: String(values?.["title"] ?? "") },
|
|
372
|
+
{ label: "Category", value: String(values?.["category"] ?? "") },
|
|
373
|
+
{ label: "Price", value: String(values?.["price"] ?? "") },
|
|
374
|
+
{ label: "Condition", value: String(values?.["condition"] ?? "") },
|
|
375
|
+
]}
|
|
376
|
+
/>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
```
|