@object-ui/plugin-form 17.3.0 → 17.6.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +1159 -0
  2. package/README.md +494 -77
  3. package/dist/DrawerForm.d.ts.map +1 -1
  4. package/dist/EmbeddableForm.d.ts.map +1 -1
  5. package/dist/LineItemsPanel.d.ts +33 -0
  6. package/dist/LineItemsPanel.d.ts.map +1 -1
  7. package/dist/MasterDetailForm.d.ts.map +1 -1
  8. package/dist/ModalForm.d.ts.map +1 -1
  9. package/dist/ObjectForm.d.ts +18 -2
  10. package/dist/ObjectForm.d.ts.map +1 -1
  11. package/dist/SplitForm.d.ts.map +1 -1
  12. package/dist/TabbedForm.d.ts.map +1 -1
  13. package/dist/WizardForm.d.ts +33 -0
  14. package/dist/WizardForm.d.ts.map +1 -1
  15. package/dist/autoLayout.d.ts.map +1 -1
  16. package/dist/deriveMasterDetail.d.ts +1 -1
  17. package/dist/deriveMasterDetail.d.ts.map +1 -1
  18. package/dist/flatFields.d.ts +21 -0
  19. package/dist/flatFields.d.ts.map +1 -0
  20. package/dist/index.d.ts +9 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1570 -1112
  23. package/dist/index.umd.cjs +1 -1
  24. package/dist/schemaDefaults.d.ts +142 -0
  25. package/dist/schemaDefaults.d.ts.map +1 -0
  26. package/dist/sectionFields.d.ts +18 -0
  27. package/dist/sectionFields.d.ts.map +1 -1
  28. package/dist/submitRedirect.d.ts +148 -0
  29. package/dist/submitRedirect.d.ts.map +1 -0
  30. package/dist/submitRedirectNavigation.d.ts +82 -0
  31. package/dist/submitRedirectNavigation.d.ts.map +1 -0
  32. package/dist/successBehavior.d.ts +18 -1
  33. package/dist/successBehavior.d.ts.map +1 -1
  34. package/dist/thankYouRedirectCountdown.d.ts +17 -0
  35. package/dist/thankYouRedirectCountdown.d.ts.map +1 -0
  36. package/dist/thankYouRedirectNavigation.d.ts +111 -0
  37. package/dist/thankYouRedirectNavigation.d.ts.map +1 -0
  38. package/package.json +13 -13
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemaDefaults.d.ts","sourceRoot":"","sources":["../src/schemaDefaults.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAEH,OAAO,EAAwB,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAQzE,OAAO,EAAE,gBAAgB,EAAE,CAAC;AAE5B,qFAAqF;AACrF,UAAU,gBAAgB;IACxB,MAAM,CAAC,EAAE,MAAM,CACb,MAAM,EACN;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS,CACpG,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAGrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GACpE,OAAO,CAET;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,EACxE,YAAY,EAAE,OAAO,GACpB,OAAO,CAIT;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EACjD,GAAG,CAAC,EAAE,WAAW,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAyBzB;AAqBD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,EACjD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACxC,GAAG,CAAC,EAAE,WAAW,GAChB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,YAAY,EAAE,gBAAgB,GAAG,IAAI,GAAG,SAAS,GAChD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB"}
@@ -8,6 +8,15 @@ export interface SectionFieldsContext {
8
8
  readOnly?: boolean;
9
9
  /** Form mode — `view` forces every field disabled. */
10
10
  mode?: 'create' | 'edit' | 'view';
11
+ /**
12
+ * The record this form is editing, if any. Together with `mode` it answers
13
+ * "is there a persisted record behind this form", which decides whether a
14
+ * runtime `defaultValue` excuses the field from `required` (#4069) — see
15
+ * `isCreateFormMode`. Passed rather than derived from `mode` alone because
16
+ * the containers' own create branch is `mode === 'create' || !recordId`, and
17
+ * a form that omits `mode` entirely is still a create form to them.
18
+ */
19
+ recordId?: unknown;
11
20
  /**
12
21
  * Translation-aware label resolver (from `useSafeFieldLabel`).
13
22
  *
@@ -18,6 +27,15 @@ export interface SectionFieldsContext {
18
27
  */
19
28
  fieldLabel: (objectName: string, fieldName: string, fallback: string) => string;
20
29
  }
30
+ /**
31
+ * Build a runtime FormField from object-schema metadata for `fieldName`.
32
+ *
33
+ * The ONE object-field-to-FormField mapping in this package. Exported because
34
+ * the flat (no-sections) containers need the identical mapping — see
35
+ * `flatFields.ts`, and objectui#4755 for what a second hand-copied version
36
+ * costs.
37
+ */
38
+ export declare function fromObjectSchema(fieldName: string, ctx: SectionFieldsContext): FormField;
21
39
  /**
22
40
  * Normalize one section field definition (string | spec object | runtime
23
41
  * FormField) into a runtime FormField with a guaranteed `name`.
@@ -1 +1 @@
1
- {"version":3,"file":"sectionFields.d.ts","sourceRoot":"","sources":["../src/sectionFields.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,YAAY,EAAE,GAAG,CAAC;IAClB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC;;;;;;;OAOG;IACH,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CACjF;AAsED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACtC,GAAG,EAAE,oBAAoB,GACxB,SAAS,CAqEX;AAED,8CAA8C;AAC9C,wBAAgB,kBAAkB,CAChC,OAAO,EAAE;IAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;CAAE,EACxD,GAAG,EAAE,oBAAoB,GACxB,SAAS,EAAE,CAEb"}
1
+ {"version":3,"file":"sectionFields.d.ts","sourceRoot":"","sources":["../src/sectionFields.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAIlD,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,YAAY,EAAE,GAAG,CAAC;IAClB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CACjF;AA2CD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,oBAAoB,GAAG,SAAS,CAgCxF;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACtC,GAAG,EAAE,oBAAoB,GACxB,SAAS,CA2FX;AAED,8CAA8C;AAC9C,wBAAgB,kBAAkB,CAChC,OAAO,EAAE;IAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;CAAE,EACxD,GAAG,EAAE,oBAAoB,GACxB,SAAS,EAAE,CAEb"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * Where a form's `submitBehavior: { kind: 'redirect' }` sends the submitter
6
+ * (objectui#4989), under the shape objectstack#7496 ruled on 2026-08-11 and
7
+ * objectstack#7657 landed in `@objectstack/spec` (live on the 17.0.0 GA pin
8
+ * this package depends on).
9
+ *
10
+ * The ruling has three points:
11
+ *
12
+ * 1. **relative paths only** — an absolute or protocol-relative URL is
13
+ * refused. This is the open-redirect face: the destination is authored
14
+ * metadata, which is exactly where an address somebody else chose gets
15
+ * copied in.
16
+ * 2. **interpolation only from declared record fields**, spelled
17
+ * `{{record.field_name}}`, with every interpolated value **URL-escaped
18
+ * when the redirect is built**.
19
+ * 3. a **verbatim redirect on the resolved relative path** is the intended
20
+ * consumption — the destination is that string with its tokens
21
+ * substituted, and nothing else.
22
+ *
23
+ * Point 1 is a property of the string, so the spec enforces it at the authoring
24
+ * door and this module ASKS it rather than restating it. Whether
25
+ * `{{record.foo}}` names a field the form's object actually declares needs both
26
+ * the string and the object declaration, so `@objectstack/lint`'s
27
+ * reference-integrity family owns that. Point 2's "when the redirect is built"
28
+ * and point 3's consumption are runtime, and this module is where they happen.
29
+ *
30
+ * ## The shape verdict is the spec's, not a copy of it
31
+ *
32
+ * `@objectstack/spec` does not export its url check as a function, but it does
33
+ * export the schema the check lives in, so the verdict here is produced by
34
+ * PARSING the smallest form view that carries this behavior. That costs one
35
+ * parse per submit and buys the property that matters: there is no second
36
+ * spelling of a security rule in this repo to drift from the first, and when the
37
+ * ruling widens (it says an allowlist of absolute origins "waits for measured
38
+ * demand") this package follows the pin with no edit here.
39
+ *
40
+ * The alternative was a local re-implementation of the seven refusal families
41
+ * kept honest by a parity table. `scripts/check-spec-symbol-derivation.mjs`
42
+ * exists because this repo has paid for that shape four times
43
+ * (objectstack#4115, objectui#4074/#4588/#4592): a hand copy passes every value
44
+ * comparison right up to the release that moves the original.
45
+ *
46
+ * Parsing a MINIMAL view is load-bearing, not laziness. A whole stored
47
+ * `FormView` refuses on any unrelated key the strict schema does not know, and a
48
+ * redirect must not be refused because some other part of the metadata drifted.
49
+ * The question asked here is narrow — "is this url a value the contract
50
+ * allows?" — so only that value is submitted for judgement, and only issues on
51
+ * that value's path are read back.
52
+ *
53
+ * `@objectstack/spec` is already a declared runtime dependency of this package
54
+ * and already imported by its siblings (`sectionFields.ts`, `sanitize.ts`), so
55
+ * the import adds no dependency and no chunk.
56
+ *
57
+ * ## Relationship to `apps/console`'s `submitRedirect.ts`
58
+ *
59
+ * `apps/console` consumes the same ruled key and got the same treatment in
60
+ * objectui#4190 / PR #4992. The two are deliberately SEMANTICALLY aligned and
61
+ * physically separate: a published renderer package cannot import an app's
62
+ * module, and the shared thing — the shape verdict — is not duplicated at all,
63
+ * because both ask the same `FormViewSchema`. What is written twice is the token
64
+ * substitution and the escape, ~20 lines the spec exports no substituter for.
65
+ *
66
+ * Two deliberate divergences, both named so a future consolidation has a map.
67
+ * The accepted value: the console resolves to a `path` it hands to a ROUTER
68
+ * unconditionally, whereas this package resolves to a path it hands to the
69
+ * host's injected navigate when there is one and to a browser-level navigation
70
+ * when there is not — a renderer cannot assume a router, which is the whole
71
+ * subject of `submitRedirectNavigation.ts`. And the split: the console has since
72
+ * separated its shape verdict
73
+ * into `checkSubmitRedirectUrl` because it also owns an authoring door
74
+ * (`PublicFormsPage` asks it before saving), which this package has no
75
+ * counterpart to — a renderer only ever consumes.
76
+ */
77
+ /** Accepted: `url` is the resolved relative destination to navigate to. */
78
+ interface SubmitRedirectAccepted {
79
+ ok: true;
80
+ url: string;
81
+ }
82
+ /** Refused: `refusal` is author-facing prose explaining what to write instead. */
83
+ interface SubmitRedirectRefused {
84
+ ok: false;
85
+ refusal: string;
86
+ }
87
+ export type SubmitRedirectVerdict = SubmitRedirectAccepted | SubmitRedirectRefused;
88
+ /**
89
+ * The token scope for one submit: "the record this submit just wrote".
90
+ *
91
+ * The values as submitted, with whatever the DataSource answered layered over
92
+ * them — defaults, computed columns and the new id are canonical there, and the
93
+ * submitted values are what a `create` that echoes nothing back still has. Both
94
+ * form components call this so a `{{record.id}}` token cannot resolve one way in
95
+ * `ObjectForm` and another in `WizardForm`.
96
+ */
97
+ export declare function submitRedirectScope(submittedValues: unknown, result: unknown): Record<string, unknown>;
98
+ /**
99
+ * Resolve an authored `submitBehavior.url` into the relative destination to
100
+ * navigate to, or refuse it.
101
+ *
102
+ * @param url the authored value, exactly as the metadata carries it
103
+ * @param record the record this submit just wrote — the token scope
104
+ *
105
+ * Refusal is fail-closed and quotable: the message is the spec's own
106
+ * prescription (it names the key, the rule, and what to write instead, including
107
+ * that a deliberately external destination is an app navigation item rather than
108
+ * this key), so the author reads the same sentence here that the authoring door
109
+ * would have told them. Callers must SHOW it — the write already succeeded, and
110
+ * dropping the refusal leaves the submitter looking at a still-filled form with
111
+ * no idea why nothing happened, which is objectui#4989's defect 2 and the one
112
+ * outcome the ruling's consumer half rules out.
113
+ *
114
+ * ## Why substitution cannot widen the destination
115
+ *
116
+ * Every interpolated value goes through `encodeURIComponent`, which escapes the
117
+ * characters that could add structure — `/` and `:`, along with `?`, `#` and the
118
+ * rest. So a field carrying an address, a traversal, or a space becomes one
119
+ * opaque segment or query value: a token is a VALUE in the path, never a way to
120
+ * add path structure.
121
+ *
122
+ * That escape carries the whole weight of the property, and re-parsing the
123
+ * result would NOT be enough on its own. Interpolated raw,
124
+ * `/t/{{record.slug}}` with an address in `slug` becomes
125
+ * `/t/https://evil.example/steal` — which still starts with `/` and carries no
126
+ * leading scheme, i.e. a spec-VALID relative path pointing somewhere the author
127
+ * never wrote. Relative-only is a rule about where a path STARTS, so it has
128
+ * nothing to say about structure injected further along. `submitRedirect.test.ts`
129
+ * therefore pins both halves.
130
+ *
131
+ * ## What this module does not answer: WHO travels (objectui#4989 defect 4)
132
+ *
133
+ * An accepted value is a rooted relative path, and a rooted path resolves
134
+ * against the ORIGIN root when a browser-level navigation performs it — so
135
+ * under a host mounted at a sub-path a ruled in-app `/thanks` used to leave the
136
+ * app. That is defect 4 of the card, and it is not a property of the string:
137
+ * the same accepted path is correct or wrong depending on who travels to it.
138
+ * It is therefore answered one layer out, in `submitRedirectNavigation.ts`,
139
+ * where the host's optionally injected navigate is used when present
140
+ * (maintainer ruling, 2026-08-17) and `window.location.assign` otherwise.
141
+ *
142
+ * Nothing about the verdict changes with the seam: an injected host navigate is
143
+ * never a laundering route for a value this module refuses, because it is only
144
+ * ever handed a path this module already accepted.
145
+ */
146
+ export declare function resolveSubmitRedirect(url: string, record: Record<string, unknown>): SubmitRedirectVerdict;
147
+ export {};
148
+ //# sourceMappingURL=submitRedirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submitRedirect.d.ts","sourceRoot":"","sources":["../src/submitRedirect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AAsBH,2EAA2E;AAC3E,UAAU,sBAAsB;IAC9B,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;CACb;AAED,kFAAkF;AAClF,UAAU,qBAAqB;IAC7B,EAAE,EAAE,KAAK,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,qBAAqB,GAAG,sBAAsB,GAAG,qBAAqB,CAAC;AAoBnF;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,OAAO,EACxB,MAAM,EAAE,OAAO,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,qBAAqB,CAgDvB"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * How an ACCEPTED `submitBehavior: { kind: 'redirect' }` destination is
6
+ * actually travelled to — objectui#4989 **defect 4, mount-blindness**, the
7
+ * reopened half of that card.
8
+ *
9
+ * `submitRedirect.ts` answers WHETHER a destination may be followed and WHAT
10
+ * the resolved relative path is. This module answers WHO performs the
11
+ * navigation, and it is a separate question because the right answer is not
12
+ * this package's to pick: a rooted path such as `/thanks` handed to
13
+ * `window.location.assign` resolves against the ORIGIN, so under a host mounted
14
+ * at a sub-path (the framework CLI configures one for every embedded
15
+ * deployment) a ruled in-app destination leaves the application. Only the host
16
+ * knows its mount.
17
+ *
18
+ * ## The mechanism, and why it is this one
19
+ *
20
+ * Maintainer ruling, 2026-08-17: **optional injected navigation**. The host
21
+ * offers its own router-aware, basename-aware navigate through
22
+ * `HostNavigationContext` (`@object-ui/react`); this package uses it when it is
23
+ * there and keeps today's `window.location.assign` when it is not. Two other
24
+ * mechanisms were weighed and REJECTED in that ruling, recorded here so the
25
+ * next reader does not re-derive them as improvements:
26
+ *
27
+ * - **reading React Router's context when a router happens to be present** —
28
+ * implicit, and unavailable regardless: a React context is a module-instance
29
+ * object, so reading the host's requires importing `react-router` here.
30
+ * This package declares it in none of its four dependency fields, its Vite
31
+ * build externalises every bare specifier, and two real consumers
32
+ * (`apps/site`, `packages/plugin-view`) have no react-router to resolve —
33
+ * the published `dist` would break their builds. Declaring it as a normal
34
+ * dependency does not help either: that yields this package its OWN
35
+ * react-router instance, whose context object is not the host's, so the
36
+ * read answers null forever.
37
+ * - **a `react-router` peer requirement** — the honest version of the above,
38
+ * and it costs this package the property that it drops into ANY React
39
+ * application.
40
+ *
41
+ * ## Absent seam is not a degraded mode
42
+ *
43
+ * With no provider the behaviour is byte-for-byte what it was: one
44
+ * `window.location.assign` of the resolved path after the declared delay. A
45
+ * host with no router has no basename, so origin-rooted resolution is already
46
+ * correct there — the seam changes what a MOUNTED host gets, and nothing else.
47
+ *
48
+ * ## What the host takes on by supplying it
49
+ *
50
+ * A host navigate is a client-side transition, so the destination must be a
51
+ * path the host's router can match; an in-app path with no route renders the
52
+ * host's not-found instead of a full page load. That is the host's routing
53
+ * table to answer for, which is exactly why the choice is the host's. The
54
+ * verdict is NOT relaxed for injected hosts: a destination the contract refuses
55
+ * is refused identically either way (`submitRedirect.ts`), so a supplied
56
+ * navigate can never become a laundering route for a value the authoring door
57
+ * rejects.
58
+ */
59
+ /**
60
+ * An accepted destination plus the delay declared with it, captured at the
61
+ * moment the write was accepted. Held in component state by each form so the
62
+ * wait belongs to the mounted component (objectui#5033) — see the state's own
63
+ * comment at each call site.
64
+ */
65
+ export interface PendingSubmitRedirect {
66
+ url: string;
67
+ delayMs: number;
68
+ }
69
+ /**
70
+ * Own the delayed leg of an accepted `redirect` submit behaviour: wait the
71
+ * declared `delayMs`, then travel to `pending.url` through the host's navigate
72
+ * if one was injected, else through `window.location.assign`.
73
+ *
74
+ * Unmounting cancels the wait, which is the property objectui#5033 bought and
75
+ * this extraction preserves: the hook's effect is the same effect both forms
76
+ * used to declare inline, now written once so the two cannot disagree about
77
+ * either the wait or the seam.
78
+ *
79
+ * Pass `null` when nothing is pending.
80
+ */
81
+ export declare function useSubmitRedirectNavigation(pending: PendingSubmitRedirect | null): void;
82
+ //# sourceMappingURL=submitRedirectNavigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submitRedirectNavigation.d.ts","sourceRoot":"","sources":["../src/submitRedirectNavigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAKH;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,GAAG,IAAI,CA8BvF"}
@@ -13,7 +13,24 @@
13
13
  * → here), so the same-origin check is inlined.
14
14
  */
15
15
  export type { SubmitBehavior } from '@object-ui/types';
16
- /** Same-origin guard for declarative navigation (relative or absolute URLs). */
16
+ /**
17
+ * Same-origin guard for declarative navigation (relative or absolute URLs).
18
+ *
19
+ * **Scope note (objectui#4989).** This guard used to gate `submitBehavior:
20
+ * { kind: 'redirect' }` too. It no longer does: objectstack#7496 ruled that key
21
+ * RELATIVE-ONLY, and `submitRedirect.ts` gets that verdict from the spec's own
22
+ * `FormViewSchema`. Relative-only is strictly stronger than same-origin — a
23
+ * rooted relative path is same-origin by construction — so keeping this call in
24
+ * that arm as defence-in-depth would add no defence, while its converse (it says
25
+ * yes to `https://own-host/thanks`) was exactly the defect: this consumer
26
+ * accepted a spelling the authoring door now refuses, which is how a rejected
27
+ * spelling stays alive in a corpus.
28
+ *
29
+ * It survives because {@link resolveSuccessNavigate} still needs it. That serves
30
+ * `navigateOnSuccess` — a DIFFERENT declared key with its own `{id}` /
31
+ * `{recordId}` dialect, no relative-only ruling, and its own contract question
32
+ * still open. Deleting the guard would silently widen that key.
33
+ */
17
34
  export declare function isSameOriginUrl(rawUrl: string): boolean;
18
35
  /**
19
36
  * Resolve a `navigateOnSuccess` template into a safe URL: interpolate
@@ -1 +1 @@
1
- {"version":3,"file":"successBehavior.d.ts","sourceRoot":"","sources":["../src/successBehavior.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQvD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,GAAG,GACV,MAAM,GAAG,IAAI,CAMf"}
1
+ {"version":3,"file":"successBehavior.d.ts","sourceRoot":"","sources":["../src/successBehavior.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQvD;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,GAAG,GACV,MAAM,GAAG,IAAI,CAMf"}
@@ -0,0 +1,17 @@
1
+ import { PendingThankYouRedirect } from './thankYouRedirectNavigation';
2
+ /** One tick per displayed second — matches the `{{seconds}}` unit the copy promises. */
3
+ export declare const REDIRECT_COUNTDOWN_TICK_MS = 1000;
4
+ /**
5
+ * The remaining whole seconds of an accepted thank-you redirect wait, ticking
6
+ * down once per second until it reaches 0. `null` while nothing is pending —
7
+ * the panel keeps deciding on its own whether a countdown renders at all (a
8
+ * refused destination, objectui#5073, never reaches this hook with a
9
+ * non-null `pending`).
10
+ *
11
+ * Pass the same `PendingThankYouRedirect | null` value given to
12
+ * `useThankYouRedirectNavigation`. This hook does not navigate anything and
13
+ * does not judge the destination — it only counts down the delay that was
14
+ * captured alongside it.
15
+ */
16
+ export declare function useRedirectCountdownSeconds(pending: PendingThankYouRedirect | null): number | null;
17
+ //# sourceMappingURL=thankYouRedirectCountdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thankYouRedirectCountdown.d.ts","sourceRoot":"","sources":["../src/thankYouRedirectCountdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAGH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAE5E,wFAAwF;AACxF,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAM/C;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,uBAAuB,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAmClG"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * WHO travels to an ACCEPTED `thankYouPage.redirectUrl` — objectui#5112, the
6
+ * mount-blindness of `EmbeddableForm`'s thank-you redirect.
7
+ *
8
+ * `EmbeddableForm.isRedirectUrlSafe` answers WHETHER a destination may be
9
+ * followed. This module answers who performs the navigation, and it is a
10
+ * separate question for the reason objectui#4989 defect 4 established: a rooted
11
+ * path such as `/thanks` handed to a browser-level navigation resolves against
12
+ * the ORIGIN root, so under a host mounted at a sub-path (the framework CLI
13
+ * configures one for every embedded deployment; the console runs at basename
14
+ * `/_console`) a same-origin in-app destination leaves the application. Only the
15
+ * host knows its mount, and it offers its mount-aware navigate through
16
+ * `HostNavigationContext` (`@object-ui/react`, landed by PR #5111).
17
+ *
18
+ * ## Why this is a sibling of `submitRedirectNavigation.ts` and not a call into it
19
+ *
20
+ * That module serves `submitBehavior.url`, whose accepted set objectstack#7496
21
+ * ruled **relative-only**: every destination it ever holds is already in-app, so
22
+ * it hands the seam whatever it was given. This key is deliberately more
23
+ * permissive — same-origin OR a host in the author's `allowedRedirectHosts`
24
+ * allowlist — because a public form's thank-you page is often a real external
25
+ * marketing page. So this key needs an arm the other one does not have, and
26
+ * putting that arm inside the shared hook would make every `submitBehavior`
27
+ * consumer carry a branch its contract can never reach. The acceptance set
28
+ * itself is untouched here (objectui#5112 is explicitly not the card that
29
+ * revisits it, and objectstack#7496's relative-only rule is NOT imported onto
30
+ * this key).
31
+ *
32
+ * ## The arm split, and the invariant it buys
33
+ *
34
+ * - **an app-relative destination** goes through the host's navigate when a
35
+ * host supplied one, and falls back to today's browser navigation when none
36
+ * was supplied;
37
+ * - **anything else** — an external destination admitted by the allowlist, and
38
+ * equally an ABSOLUTE same-origin URL the author spelled out in full — keeps
39
+ * browser-level navigation unconditionally.
40
+ *
41
+ * The second arm is not a conservatism, it is the seam's own declared input
42
+ * contract: `HostNavigationValue.navigate` documents `to` as "an already-resolved,
43
+ * application-relative path, never an absolute URL: a renderer that holds an
44
+ * external destination must not launder it through the host's router". A host
45
+ * navigate is a client-side router transition; an absolute URL is not a path a
46
+ * router can match, and a cross-origin one is not the router's to attempt at
47
+ * all. So the discriminant is the seam's admission rule restated as a predicate
48
+ * on the string, which yields the invariant worth stating out loud and pinning:
49
+ * **the host seam can never be handed a cross-origin URL**, because a relative
50
+ * reference cannot carry an authority (RFC 3986) and therefore always resolves
51
+ * to the base's origin.
52
+ *
53
+ * An absolute same-origin URL landing in the browser arm is a deliberate call,
54
+ * not an oversight. It is the one destination shape the card's two named arms do
55
+ * not cover, and routing it through the seam would mean this package rewriting
56
+ * `https://forms.example/thanks` into `/thanks` for a router that would then
57
+ * place it at `/_console/thanks` — a DIFFERENT address from the one the author
58
+ * wrote. An author who spelled the whole address asked for that address; the
59
+ * mount-blindness this card fixes is the one where the author wrote no origin at
60
+ * all and had no way to express the mount.
61
+ *
62
+ * ## Absent seam is not a degraded mode
63
+ *
64
+ * With no provider the behaviour is byte-for-byte what it was: one
65
+ * `window.location.href` assignment of the authored destination after the
66
+ * declared delay. A host with no router has no basename, so origin-rooted
67
+ * resolution is already correct there.
68
+ */
69
+ /**
70
+ * An accepted thank-you destination plus the delay declared with it, captured at
71
+ * the moment the write was accepted (objectui#5049) — see the state's own
72
+ * comment in `EmbeddableForm`.
73
+ */
74
+ export interface PendingThankYouRedirect {
75
+ url: string;
76
+ delayMs: number;
77
+ }
78
+ /**
79
+ * Is `rawUrl` an application-relative reference — i.e. a destination the host's
80
+ * router can be asked to place?
81
+ *
82
+ * True for `/thanks`, `thanks`, `?ok=1`, `#done`. False for `https://x/y`,
83
+ * `//x/y` (protocol-relative carries an authority), `javascript:…`, and for a
84
+ * same-origin ABSOLUTE URL — see the module docblock for why that last one is
85
+ * deliberate.
86
+ *
87
+ * The question is answered by the URL parser rather than by a hand-written
88
+ * scheme grammar, so leading whitespace and C0 controls are handled exactly the
89
+ * way the parser handles them and there is no second spelling of RFC 3986 in
90
+ * this repo to drift from the first.
91
+ *
92
+ * This predicate does NOT judge whether a destination may be followed —
93
+ * `isRedirectUrlSafe` already did that, and this runs only on values it
94
+ * accepted. It picks who travels, nothing more.
95
+ */
96
+ export declare function isAppRelativeDestination(rawUrl: string): boolean;
97
+ /**
98
+ * Own the delayed leg of an accepted thank-you redirect: wait the declared
99
+ * `delayMs`, then travel to `pending.url` through the host's navigate if one was
100
+ * injected AND the destination is app-relative, else through
101
+ * `window.location.href`.
102
+ *
103
+ * Unmounting, or dropping the destination, cancels the wait — the property
104
+ * objectui#5049 bought, preserved here unchanged: this hook is the effect
105
+ * `EmbeddableForm` used to declare inline, with the seam added and nothing else
106
+ * moved.
107
+ *
108
+ * Pass `null` when nothing is pending.
109
+ */
110
+ export declare function useThankYouRedirectNavigation(pending: PendingThankYouRedirect | null): void;
111
+ //# sourceMappingURL=thankYouRedirectNavigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thankYouRedirectNavigation.d.ts","sourceRoot":"","sources":["../src/thankYouRedirectNavigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAKH;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAYhE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,uBAAuB,GAAG,IAAI,GAAG,IAAI,CA+B3F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-form",
3
- "version": "17.3.0",
3
+ "version": "17.6.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Form plugin for Object UI",
@@ -15,15 +15,15 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@objectstack/spec": "^17.0.0-rc.2",
19
- "lucide-react": "^1.28.0",
20
- "@object-ui/components": "17.3.0",
21
- "@object-ui/core": "17.3.0",
22
- "@object-ui/fields": "17.3.0",
23
- "@object-ui/i18n": "17.3.0",
24
- "@object-ui/permissions": "17.3.0",
25
- "@object-ui/react": "17.3.0",
26
- "@object-ui/types": "17.3.0"
18
+ "@objectstack/spec": "^17.0.0",
19
+ "lucide-react": "^1.31.0",
20
+ "@object-ui/components": "17.6.0",
21
+ "@object-ui/core": "17.6.0",
22
+ "@object-ui/fields": "17.6.0",
23
+ "@object-ui/i18n": "17.6.0",
24
+ "@object-ui/permissions": "17.6.0",
25
+ "@object-ui/react": "17.6.0",
26
+ "@object-ui/types": "17.6.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^18.0.0 || ^19.0.0",
@@ -33,9 +33,9 @@
33
33
  "@vitejs/plugin-react": "^6.0.5",
34
34
  "msw": "^2.15.0",
35
35
  "typescript": "^6.0.3",
36
- "vite": "^8.2.0",
36
+ "vite": "^8.2.1",
37
37
  "vite-plugin-dts": "^5.0.3",
38
- "@object-ui/data-objectstack": "17.3.0"
38
+ "@object-ui/data-objectstack": "17.6.0"
39
39
  },
40
40
  "keywords": [
41
41
  "objectui",
@@ -72,7 +72,7 @@
72
72
  "scripts": {
73
73
  "build": "vite build",
74
74
  "test": "vitest run",
75
- "type-check": "tsc --noEmit && tsc -p tsconfig.typetests.json",
75
+ "type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
76
76
  "lint": "eslint ."
77
77
  }
78
78
  }