@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.
- package/CHANGELOG.md +1159 -0
- package/README.md +494 -77
- package/dist/DrawerForm.d.ts.map +1 -1
- package/dist/EmbeddableForm.d.ts.map +1 -1
- package/dist/LineItemsPanel.d.ts +33 -0
- package/dist/LineItemsPanel.d.ts.map +1 -1
- package/dist/MasterDetailForm.d.ts.map +1 -1
- package/dist/ModalForm.d.ts.map +1 -1
- package/dist/ObjectForm.d.ts +18 -2
- package/dist/ObjectForm.d.ts.map +1 -1
- package/dist/SplitForm.d.ts.map +1 -1
- package/dist/TabbedForm.d.ts.map +1 -1
- package/dist/WizardForm.d.ts +33 -0
- package/dist/WizardForm.d.ts.map +1 -1
- package/dist/autoLayout.d.ts.map +1 -1
- package/dist/deriveMasterDetail.d.ts +1 -1
- package/dist/deriveMasterDetail.d.ts.map +1 -1
- package/dist/flatFields.d.ts +21 -0
- package/dist/flatFields.d.ts.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1570 -1112
- package/dist/index.umd.cjs +1 -1
- package/dist/schemaDefaults.d.ts +142 -0
- package/dist/schemaDefaults.d.ts.map +1 -0
- package/dist/sectionFields.d.ts +18 -0
- package/dist/sectionFields.d.ts.map +1 -1
- package/dist/submitRedirect.d.ts +148 -0
- package/dist/submitRedirect.d.ts.map +1 -0
- package/dist/submitRedirectNavigation.d.ts +82 -0
- package/dist/submitRedirectNavigation.d.ts.map +1 -0
- package/dist/successBehavior.d.ts +18 -1
- package/dist/successBehavior.d.ts.map +1 -1
- package/dist/thankYouRedirectCountdown.d.ts +17 -0
- package/dist/thankYouRedirectCountdown.d.ts.map +1 -0
- package/dist/thankYouRedirectNavigation.d.ts +111 -0
- package/dist/thankYouRedirectNavigation.d.ts.map +1 -0
- 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"}
|
package/dist/sectionFields.d.ts
CHANGED
|
@@ -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;
|
|
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
|
-
/**
|
|
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
|
|
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
|
+
"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
|
|
19
|
-
"lucide-react": "^1.
|
|
20
|
-
"@object-ui/components": "17.
|
|
21
|
-
"@object-ui/core": "17.
|
|
22
|
-
"@object-ui/fields": "17.
|
|
23
|
-
"@object-ui/i18n": "17.
|
|
24
|
-
"@object-ui/permissions": "17.
|
|
25
|
-
"@object-ui/react": "17.
|
|
26
|
-
"@object-ui/types": "17.
|
|
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.
|
|
36
|
+
"vite": "^8.2.1",
|
|
37
37
|
"vite-plugin-dts": "^5.0.3",
|
|
38
|
-
"@object-ui/data-objectstack": "17.
|
|
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.
|
|
75
|
+
"type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
76
76
|
"lint": "eslint ."
|
|
77
77
|
}
|
|
78
78
|
}
|