@uniweb/kit 0.10.11 → 0.10.13
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/package.json +1 -1
- package/src/hooks/useFormSubmit.js +91 -24
- package/src/index.js +3 -1
- package/src/math-tokens.css +11 -2
- package/src/utils/index.js +2 -1
- package/src/utils/submitForm.js +51 -36
- package/src/utils/submitTarget.js +124 -0
package/package.json
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react'
|
|
1
|
+
import { useCallback, useRef, useState } from 'react'
|
|
2
2
|
import { submitForm } from '../utils/submitForm.js'
|
|
3
|
+
import { resolveSubmitTarget } from '../utils/submitTarget.js'
|
|
4
|
+
import { useWebsite } from './useWebsite.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
+
* Submit a form, with the `idle → submitting → success | error` lifecycle most
|
|
8
|
+
* form UIs need.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - submit: async (formData, perCallOverrides?) => response
|
|
14
|
-
* - reset: () => void
|
|
10
|
+
* The hook resolves *where* to submit from the site's configuration (`submit:`
|
|
11
|
+
* in site.yml) or from its host, so a component never names an endpoint. When
|
|
12
|
+
* neither supplies one, `canSubmit` is false and `unavailableReason` says why —
|
|
13
|
+
* render the control disabled rather than letting someone fill in a form whose
|
|
14
|
+
* contents have nowhere to go. See `resolveSubmitTarget` for the precedence.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* ```jsx
|
|
17
|
+
* const { submit, status, error, canSubmit, unavailableReason } =
|
|
18
|
+
* useFormSubmit({ block, context: { formId: 'contact' } })
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* <button type="submit" disabled={!canSubmit || status === 'submitting'}>
|
|
21
|
+
* {canSubmit ? 'Send' : unavailableReason}
|
|
22
|
+
* </button>
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Pass `block` and the submission carries where it came from — section type,
|
|
26
|
+
* section id, page id and label — without every component assembling that by
|
|
27
|
+
* hand. Anything in `context` wins over what the block supplies.
|
|
28
|
+
*
|
|
29
|
+
* `summary` may be an object or a function of the submitted values, which is
|
|
30
|
+
* the useful form when the digest is built from the fields that were just
|
|
31
|
+
* filled in.
|
|
21
32
|
*
|
|
22
33
|
* @param {object} [defaults] — merged into every submit() call
|
|
34
|
+
* @param {object} [defaults.block] — the section's block, for submission context
|
|
35
|
+
* @param {object} [defaults.context] — formId and any explicit overrides
|
|
36
|
+
* @param {object|Function} [defaults.summary] — { title, subtitle, tag? } or (formData) => that
|
|
23
37
|
* @returns {{
|
|
24
38
|
* status: 'idle' | 'submitting' | 'success' | 'error',
|
|
25
39
|
* error: Error | null,
|
|
26
40
|
* response: object | null,
|
|
41
|
+
* canSubmit: boolean,
|
|
42
|
+
* unavailableReason: string | null,
|
|
27
43
|
* submit: (formData: object, overrides?: object) => Promise<object>,
|
|
28
44
|
* reset: () => void,
|
|
29
45
|
* }}
|
|
@@ -33,16 +49,36 @@ export function useFormSubmit(defaults = {}) {
|
|
|
33
49
|
const [error, setError] = useState(null)
|
|
34
50
|
const [response, setResponse] = useState(null)
|
|
35
51
|
|
|
52
|
+
const { website } = useWebsite()
|
|
53
|
+
const { url: target, reason: unavailableReason } = resolveSubmitTarget(website)
|
|
54
|
+
|
|
55
|
+
// `defaults` is a fresh object every render, so listing it as a dependency
|
|
56
|
+
// would rebuild the callback on each one. A ref is what actually makes "the
|
|
57
|
+
// newest defaults win" true — with an empty dependency list the callback
|
|
58
|
+
// closes over the *first* render's defaults forever, so a summary or context
|
|
59
|
+
// computed from changing content would silently keep sending the original.
|
|
60
|
+
const defaultsRef = useRef(defaults)
|
|
61
|
+
defaultsRef.current = defaults
|
|
62
|
+
|
|
36
63
|
const submit = useCallback(
|
|
37
64
|
async (formData, perCallOverrides = {}) => {
|
|
38
65
|
setStatus('submitting')
|
|
39
66
|
setError(null)
|
|
40
67
|
try {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
merged.preview = merged.preview(formData)
|
|
68
|
+
const { block, context, summary, ...rest } = {
|
|
69
|
+
...defaultsRef.current,
|
|
70
|
+
...perCallOverrides,
|
|
45
71
|
}
|
|
72
|
+
|
|
73
|
+
const merged = {
|
|
74
|
+
...rest,
|
|
75
|
+
formData,
|
|
76
|
+
target,
|
|
77
|
+
// Block-derived context first so an explicit `context` overrides it.
|
|
78
|
+
context: { ...contextFromBlock(block), ...context },
|
|
79
|
+
summary: typeof summary === 'function' ? summary(formData) : summary,
|
|
80
|
+
}
|
|
81
|
+
|
|
46
82
|
const result = await submitForm(merged)
|
|
47
83
|
setStatus('success')
|
|
48
84
|
setResponse(result)
|
|
@@ -53,11 +89,7 @@ export function useFormSubmit(defaults = {}) {
|
|
|
53
89
|
throw err
|
|
54
90
|
}
|
|
55
91
|
},
|
|
56
|
-
|
|
57
|
-
// close over the latest one each render rather than memo the hook.
|
|
58
|
-
// The lint rule fires false positives here.
|
|
59
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
-
[],
|
|
92
|
+
[target],
|
|
61
93
|
)
|
|
62
94
|
|
|
63
95
|
const reset = useCallback(() => {
|
|
@@ -66,5 +98,40 @@ export function useFormSubmit(defaults = {}) {
|
|
|
66
98
|
setResponse(null)
|
|
67
99
|
}, [])
|
|
68
100
|
|
|
69
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
status,
|
|
103
|
+
error,
|
|
104
|
+
response,
|
|
105
|
+
canSubmit: !!target,
|
|
106
|
+
unavailableReason,
|
|
107
|
+
submit,
|
|
108
|
+
reset,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Where a submission came from, read off the block the form is rendered in.
|
|
114
|
+
*
|
|
115
|
+
* `stableId` is preferred over the positional `id` because it survives a
|
|
116
|
+
* section being reordered on its page — a submission's origin should not change
|
|
117
|
+
* because something moved above it. Keys with no value are dropped rather than
|
|
118
|
+
* sent as null, so they never mask a caller-supplied one.
|
|
119
|
+
*
|
|
120
|
+
* @param {object} [block]
|
|
121
|
+
* @returns {object}
|
|
122
|
+
*/
|
|
123
|
+
function contextFromBlock(block) {
|
|
124
|
+
if (!block) return {}
|
|
125
|
+
|
|
126
|
+
const page = block.page
|
|
127
|
+
const fields = {
|
|
128
|
+
sectionType: block.type,
|
|
129
|
+
sectionId: block.stableId || block.id,
|
|
130
|
+
pageId: page?.id,
|
|
131
|
+
pageLabel: page?.title,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return Object.fromEntries(
|
|
135
|
+
Object.entries(fields).filter(([, v]) => v !== undefined && v !== null && v !== ''),
|
|
136
|
+
)
|
|
70
137
|
}
|
package/src/index.js
CHANGED
|
@@ -124,7 +124,9 @@ export {
|
|
|
124
124
|
getLocaleLabel,
|
|
125
125
|
// Form submission utilities (low-level companion of useFormSubmit hook)
|
|
126
126
|
submitForm,
|
|
127
|
-
|
|
127
|
+
deriveSummary,
|
|
128
|
+
resolveSubmitTarget,
|
|
129
|
+
NO_SUBMIT_TARGET_REASON
|
|
128
130
|
} from './utils/index.js'
|
|
129
131
|
|
|
130
132
|
// ============================================================================
|
package/src/math-tokens.css
CHANGED
|
@@ -46,11 +46,20 @@
|
|
|
46
46
|
math mtd { padding-top: 0.5ex; padding-bottom: 0.5ex; }
|
|
47
47
|
math mtable.tml-jot mtd { padding-top: 0.7ex; padding-bottom: 0.7ex; }
|
|
48
48
|
|
|
49
|
-
/* AMS auto-numbering
|
|
49
|
+
/* AMS auto-numbering, for lanes that keep our CSS counter.
|
|
50
|
+
|
|
51
|
+
Scoped to :empty because the document lanes cannot rely on it -- Paged.js
|
|
52
|
+
rewrites counters for its own pagination and strips counter-increment, so
|
|
53
|
+
every equation rendered as "(0)" (measured 2026-07-31; the declaration
|
|
54
|
+
survives intact without the polyfill). press therefore writes the numbers
|
|
55
|
+
into the spans as text, and a span carrying a number is no longer :empty, so
|
|
56
|
+
the two can never both fire.
|
|
57
|
+
|
|
58
|
+
AMS auto-numbering. Which equations number is the AUTHOR's choice, made in
|
|
50
59
|
LaTeX: `align` and `equation` number, `aligned` and the starred forms do not.
|
|
51
60
|
Without these two rules that choice was discarded -- `align` and `align*`
|
|
52
61
|
rendered identically, so an author who asked for numbers silently got none. */
|
|
53
|
-
.tml-eqn::before {
|
|
62
|
+
.tml-eqn:empty::before {
|
|
54
63
|
counter-increment: tmlEqnNo;
|
|
55
64
|
content: "(" counter(tmlEqnNo) ")";
|
|
56
65
|
}
|
package/src/utils/index.js
CHANGED
|
@@ -247,7 +247,8 @@ export function detectMediaType(url) {
|
|
|
247
247
|
// Form Submission
|
|
248
248
|
// ─────────────────────────────────────────────────────────────────
|
|
249
249
|
|
|
250
|
-
export { submitForm,
|
|
250
|
+
export { submitForm, deriveSummary } from './submitForm.js'
|
|
251
|
+
export { resolveSubmitTarget, NO_SUBMIT_TARGET_REASON } from './submitTarget.js'
|
|
251
252
|
|
|
252
253
|
/**
|
|
253
254
|
* The text of a ProseMirror node, flattened.
|
package/src/utils/submitForm.js
CHANGED
|
@@ -1,63 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* POST a form's values to a submission endpoint.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* own URL determines the destination.
|
|
4
|
+
* This is the low-level companion of `useFormSubmit()`. Most components want
|
|
5
|
+
* the hook — it resolves the target from the site's configuration and tracks
|
|
6
|
+
* the request's lifecycle. Reach for this directly when you are submitting
|
|
7
|
+
* outside React, or already hold a resolved target.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
* by the Uniweb runtime infrastructure. Override only if you're testing
|
|
12
|
-
* against a non-standard endpoint.
|
|
9
|
+
* ## `target` is required, and there is no default
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* Where a site's submissions go is the site's declaration (`submit:` in
|
|
12
|
+
* site.yml — see `resolveSubmitTarget`), never this function's guess. Calling
|
|
13
|
+
* without a target throws rather than falling back to a path, because a form
|
|
14
|
+
* POSTing into a 404 loses what a visitor typed and reports success-shaped
|
|
15
|
+
* failure. Resolve first, disable the control if there is nowhere to send.
|
|
18
16
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* ## API names and wire names differ, on purpose
|
|
18
|
+
*
|
|
19
|
+
* The request body's field names are a contract with already-deployed
|
|
20
|
+
* endpoints, so they are kept exactly as those endpoints read them. This
|
|
21
|
+
* function's *arguments* are named for what they mean to a foundation
|
|
22
|
+
* developer. The mapping is applied in one place below and is not a bug to
|
|
23
|
+
* tidy up — changing the body's spelling would require every deployed endpoint
|
|
24
|
+
* to change with it.
|
|
21
25
|
*
|
|
22
26
|
* @param {object} args
|
|
23
|
-
* @param {Record<string, unknown>} args.formData
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {object} [args.
|
|
26
|
-
*
|
|
27
|
+
* @param {Record<string, unknown>} args.formData — field values
|
|
28
|
+
* @param {string} args.target — resolved endpoint URL (required)
|
|
29
|
+
* @param {object} [args.summary] — { title, subtitle, tag? }, a short
|
|
30
|
+
* human-readable digest of this
|
|
31
|
+
* submission; derived from formData
|
|
32
|
+
* when omitted
|
|
33
|
+
* @param {object} [args.context] — where the submission came from:
|
|
34
|
+
* formId, sectionType, sectionId,
|
|
35
|
+
* pageId, pageLabel
|
|
36
|
+
* @param {string} [args.verificationToken] — bot-protection token, when the
|
|
37
|
+
* endpoint verifies one
|
|
27
38
|
* @param {Array<{name:string,size:number,mime?:string}>} [args.fileSlots]
|
|
28
|
-
*
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {typeof fetch} [args.fetchFn=fetch] — fetch override (testing / SSR)
|
|
39
|
+
* — declared file uploads
|
|
40
|
+
* @param {typeof fetch} [args.fetchFn=fetch] — fetch override (testing / SSR)
|
|
31
41
|
*
|
|
32
42
|
* @returns {Promise<{ submissionId: string, uploadUrls?: Array }>}
|
|
33
|
-
* @throws {Error} on non-2xx with the server's
|
|
43
|
+
* @throws {Error} with no target, and on non-2xx with the server's message when present.
|
|
34
44
|
*/
|
|
35
45
|
export async function submitForm({
|
|
36
46
|
formData,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
target,
|
|
48
|
+
summary,
|
|
49
|
+
context = {},
|
|
50
|
+
verificationToken,
|
|
40
51
|
fileSlots,
|
|
41
|
-
submitPath = '/_submit',
|
|
42
52
|
fetchFn = typeof fetch === 'function' ? fetch : null,
|
|
43
53
|
} = {}) {
|
|
44
54
|
if (!formData || typeof formData !== 'object') {
|
|
45
55
|
throw new Error('submitForm: formData object is required')
|
|
46
56
|
}
|
|
57
|
+
if (!target || typeof target !== 'string') {
|
|
58
|
+
throw new Error(
|
|
59
|
+
'submitForm: no submission target. Declare `submit:` in site.yml, or ' +
|
|
60
|
+
'check `canSubmit` from useFormSubmit() before calling.',
|
|
61
|
+
)
|
|
62
|
+
}
|
|
47
63
|
if (!fetchFn) {
|
|
48
64
|
throw new Error('submitForm: fetch is unavailable in this environment')
|
|
49
65
|
}
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
// ── API name → wire name. See the header before "correcting" these. ──
|
|
53
68
|
const body = {
|
|
54
69
|
formData,
|
|
55
|
-
metadata: { ...
|
|
56
|
-
...(
|
|
70
|
+
metadata: { ...context, preview: summary || deriveSummary(formData) },
|
|
71
|
+
...(verificationToken ? { turnstileToken: verificationToken } : {}),
|
|
57
72
|
...(Array.isArray(fileSlots) && fileSlots.length ? { fileSlots } : {}),
|
|
58
73
|
}
|
|
59
74
|
|
|
60
|
-
const res = await fetchFn(
|
|
75
|
+
const res = await fetchFn(target, {
|
|
61
76
|
method: 'POST',
|
|
62
77
|
headers: { 'Content-Type': 'application/json' },
|
|
63
78
|
body: JSON.stringify(body),
|
|
@@ -73,14 +88,14 @@ export async function submitForm({
|
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
/**
|
|
76
|
-
* Build a default
|
|
77
|
-
*
|
|
78
|
-
*
|
|
91
|
+
* Build a default summary from a form's values: the first two non-empty string
|
|
92
|
+
* fields become the title and subtitle, so whoever reads submissions sees
|
|
93
|
+
* something meaningful even when a component passes no summary of its own.
|
|
79
94
|
*
|
|
80
95
|
* @param {Record<string, unknown>} data
|
|
81
96
|
* @returns {{ title: string, subtitle: string }}
|
|
82
97
|
*/
|
|
83
|
-
export function
|
|
98
|
+
export function deriveSummary(data) {
|
|
84
99
|
if (!data || typeof data !== 'object') return { title: 'Submission', subtitle: '' }
|
|
85
100
|
const entries = Object.entries(data).filter(
|
|
86
101
|
([, v]) => typeof v === 'string' && v.trim().length > 0,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve where this site's form submissions go.
|
|
3
|
+
*
|
|
4
|
+
* A site declares its endpoint under `submit:` in site.yml, and the value
|
|
5
|
+
* arrives here as `website.config.submit` — the same passthrough every other
|
|
6
|
+
* site.yml key rides (`collections:`, `search:`, `fetcher:`). Two spellings,
|
|
7
|
+
* because one destination is the common case and the object form leaves room
|
|
8
|
+
* to grow:
|
|
9
|
+
*
|
|
10
|
+
* submit: /forms # shorthand
|
|
11
|
+
* submit: { endpoint: /forms } # object form
|
|
12
|
+
* submit: https://forms.example.com/intake
|
|
13
|
+
*
|
|
14
|
+
* A relative endpoint resolves against `website.basePath`, so one spelling
|
|
15
|
+
* works whether the site is served from the root, from a subdirectory
|
|
16
|
+
* (`base: /docs/`), or from a host subpath. An absolute URL passes through
|
|
17
|
+
* untouched, for a form service on another origin.
|
|
18
|
+
*
|
|
19
|
+
* ## Precedence — and why the framework never picks one itself
|
|
20
|
+
*
|
|
21
|
+
* A destination comes from the first of these that applies:
|
|
22
|
+
*
|
|
23
|
+
* 1. `submit:` here — the site operator's declaration.
|
|
24
|
+
* 2. `forms: { endpoint, reason }` in the served payload config — what the
|
|
25
|
+
* HOST reports about this site. A host that accepts submissions gives an
|
|
26
|
+
* endpoint; one that does not may give a reason, which is relayed verbatim.
|
|
27
|
+
* 3. Neither — `url: null` plus a generic reason, so the form renders disabled.
|
|
28
|
+
*
|
|
29
|
+
* What the framework must never do is invent one. It cannot know where a given
|
|
30
|
+
* site can accept a submission, and guessing is worse than having none: on a
|
|
31
|
+
* static host, or any deployment with nothing listening, the form POSTs a
|
|
32
|
+
* visitor's data into a 404 and nothing here can tell. The asymmetry with
|
|
33
|
+
* reading matters — a fetch that 404s degrades to `[]` and the page still
|
|
34
|
+
* renders, which is a documented guarantee, but a write that 404s loses what
|
|
35
|
+
* someone typed.
|
|
36
|
+
*
|
|
37
|
+
* Note that a host supplying a site-local path is NOT the same as the framework
|
|
38
|
+
* defaulting to one, even when the two produce an identical request: a host only
|
|
39
|
+
* offers the path when something is actually there to catch it. Same bytes, and
|
|
40
|
+
* only one of them is correct.
|
|
41
|
+
*
|
|
42
|
+
* ## Simulating a host locally
|
|
43
|
+
*
|
|
44
|
+
* The bundle lane spreads all of site.yml into the payload config, so writing
|
|
45
|
+
* `forms: { endpoint: … }` in site.yml exercises tier 2 end to end with no
|
|
46
|
+
* framework change. It cannot leak into a real deployment: the sync lane is an
|
|
47
|
+
* explicit allowlist and does not carry `forms`, so a synced site's value can
|
|
48
|
+
* only have come from its host.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Shown when a site declares no endpoint. English, and a foundation is free to
|
|
53
|
+
* ignore it and render its own copy — check `url` for the yes/no and treat this
|
|
54
|
+
* as a default rather than a string to translate.
|
|
55
|
+
*/
|
|
56
|
+
export const NO_SUBMIT_TARGET_REASON =
|
|
57
|
+
'This site has no form submission endpoint configured.'
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the submission target for a site.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} website - The active Website instance
|
|
63
|
+
* @returns {{ url: string|null, reason: string|null }}
|
|
64
|
+
* `url` is the resolved absolute-or-base-relative endpoint, or null when the
|
|
65
|
+
* site declares none. `reason` is set exactly when `url` is null.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveSubmitTarget(website) {
|
|
68
|
+
const config = website?.config
|
|
69
|
+
const basePath = website?.basePath
|
|
70
|
+
|
|
71
|
+
// Tier 1 — the site operator's own declaration (`submit:` in site.yml).
|
|
72
|
+
const declared = config?.submit
|
|
73
|
+
const declaredEndpoint =
|
|
74
|
+
typeof declared === 'string'
|
|
75
|
+
? declared.trim()
|
|
76
|
+
: typeof declared?.endpoint === 'string'
|
|
77
|
+
? declared.endpoint.trim()
|
|
78
|
+
: ''
|
|
79
|
+
|
|
80
|
+
if (declaredEndpoint) {
|
|
81
|
+
return { url: resolveAgainstBase(declaredEndpoint, basePath), reason: null }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Tier 2 — a destination the HOST reports, under `forms` in the served
|
|
85
|
+
// payload config. Named for the capability rather than the action so it
|
|
86
|
+
// cannot be misread as the authored `submit` sitting beside it in the same
|
|
87
|
+
// flat object.
|
|
88
|
+
const host = config?.forms
|
|
89
|
+
const hostEndpoint = typeof host?.endpoint === 'string' ? host.endpoint.trim() : ''
|
|
90
|
+
if (hostEndpoint) {
|
|
91
|
+
return { url: resolveAgainstBase(hostEndpoint, basePath), reason: null }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A host that reports WHY it cannot accept submissions gets that relayed
|
|
95
|
+
// verbatim. The framework does not interpret, reword, or second-guess it: it
|
|
96
|
+
// has no standing to judge why a given site can or cannot accept them, and
|
|
97
|
+
// guessing at the reason in public code would mean encoding someone else's
|
|
98
|
+
// policy here.
|
|
99
|
+
const hostReason = typeof host?.reason === 'string' ? host.reason.trim() : ''
|
|
100
|
+
if (hostReason) return { url: null, reason: hostReason }
|
|
101
|
+
|
|
102
|
+
// Tier 3 — nobody supplied one.
|
|
103
|
+
return { url: null, reason: NO_SUBMIT_TARGET_REASON }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Join a declared endpoint to the site's base path.
|
|
108
|
+
*
|
|
109
|
+
* This is the same shape as `resolveEndpointUrl` in the search provider, and is
|
|
110
|
+
* deliberately NOT shared with it: that one falls back to a default endpoint
|
|
111
|
+
* when given nothing, which is precisely the behaviour a submission target must
|
|
112
|
+
* not have (see the header). Three lines of duplication is the cheaper mistake.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} endpoint - Declared endpoint, relative or absolute
|
|
115
|
+
* @param {string} [basePath] - `website.basePath`, normalized without a trailing slash
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
export function resolveAgainstBase(endpoint, basePath = '') {
|
|
119
|
+
if (/^https?:\/\//i.test(endpoint)) return endpoint
|
|
120
|
+
|
|
121
|
+
const base = (basePath || '').replace(/\/+$/, '')
|
|
122
|
+
const rel = endpoint.replace(/^\/+/, '')
|
|
123
|
+
return `${base}/${rel}`
|
|
124
|
+
}
|