Skip to content

Commit b935479

Browse files
committed
feat(runtime): support auto-publish payload and standalone renderer validation
1 parent d43796c commit b935479

10 files changed

Lines changed: 601 additions & 56 deletions

File tree

src/runtime/api/client.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
FormForgeCategoryListQuery,
55
FormForgeCategoryListResponse,
66
FormForgeCategoryUpdateInput,
7+
FormForgeBaseURLParamsInput,
78
FormForgeClientConfig,
89
FormForgeDraftResponse,
910
FormForgeDraftSaveInput,
@@ -108,6 +109,18 @@ class FormForgeClientImpl implements FormForgeClient {
108109
return input
109110
}
110111

112+
private resolveBaseURLParams(input: FormForgeBaseURLParamsInput | undefined): Record<string, string | number | undefined> {
113+
if (input === undefined) {
114+
return {}
115+
}
116+
117+
if (typeof input === 'function') {
118+
return input()
119+
}
120+
121+
return input
122+
}
123+
111124
private resolveNamedScope(name: string): FormForgeResolvedScope {
112125
const scopedRoutes = this.config.scopedRoutes ?? {}
113126
const routeDefinition: FormForgeScopedRouteDefinition | undefined = scopedRoutes[name]
@@ -117,10 +130,11 @@ class FormForgeClientImpl implements FormForgeClient {
117130
}
118131

119132
const sourceParams = this.resolveScopeParams(this.config.scopeParams)
133+
const baseURLParams = this.resolveBaseURLParams(this.config.baseURLParams)
120134
const params: Record<string, string | number> = {}
121135

122136
for (const [scopeParam, sourceParam] of Object.entries(routeDefinition.paramsFromRoute)) {
123-
const value = sourceParams[sourceParam]
137+
const value = sourceParams[sourceParam] ?? baseURLParams[sourceParam]
124138

125139
if (value === undefined || value === '') {
126140
throw new Error(`Missing scope param source "${sourceParam}" for named scope "${name}"`)

src/runtime/api/management.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,24 @@ function toJsonObject(input: FormForgeManagementCreateInput | FormForgeManagemen
104104
return JSON.parse(JSON.stringify(input)) as FormForgeJsonObject
105105
}
106106

107+
function shouldAutoPublish(input: FormForgeManagementCreateInput | FormForgeManagementPatchInput): boolean {
108+
return input.auto_publish === true || input.autoPublish === true
109+
}
110+
111+
function toManagementMutationPayload(input: FormForgeManagementCreateInput | FormForgeManagementPatchInput): FormForgeJsonObject {
112+
const payload = toJsonObject(input)
113+
114+
if ('autoPublish' in payload) {
115+
delete payload.autoPublish
116+
}
117+
118+
if (shouldAutoPublish(input)) {
119+
payload.auto_publish = true
120+
}
121+
122+
return payload
123+
}
124+
107125
export async function createFormForgeForm(
108126
http: FormForgeHttpAdapter,
109127
input: FormForgeManagementCreateInput,
@@ -113,7 +131,7 @@ export async function createFormForgeForm(
113131
path: resolveEndpointPath(options.endpoint, '/forms', {}, options.scope),
114132
method: 'POST',
115133
headers: withMutationHeaders(options),
116-
json: toJsonObject(input)
134+
json: toManagementMutationPayload(input)
117135
})
118136

119137
return normalizeManagementForm(pickFormForgeDataEnvelope(response.data)) ?? {}
@@ -150,7 +168,7 @@ export async function patchFormForgeForm(
150168
}, options.scope),
151169
method: 'PATCH',
152170
headers: withMutationHeaders(options),
153-
json: toJsonObject(input)
171+
json: toManagementMutationPayload(input)
154172
})
155173

156174
return normalizeManagementForm(pickFormForgeDataEnvelope(response.data)) ?? {}

src/runtime/composables/useFormForgeBuilder.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export interface UseFormForgeBuilderOptions {
4040
clientConfig?: FormForgeClientConfig
4141
}
4242

43+
export interface FormForgeBuilderSaveOptions {
44+
idempotencyKey?: string
45+
autoPublish?: boolean
46+
}
47+
4348
export const FORM_FORGE_BUILDER_FIELD_TYPES: FormForgeFieldType[] = [
4449
'text',
4550
'textarea',
@@ -331,7 +336,7 @@ export function useFormForgeBuilder(options: UseFormForgeBuilderOptions = {}) {
331336
}
332337
}
333338

334-
function toManagementInput(): FormForgeManagementCreateInput {
339+
function toManagementInput(autoPublish: boolean = false): FormForgeManagementCreateInput {
335340
normalizeFieldLocations()
336341

337342
const pages = draft.value.pages as FormForgePageSchema[]
@@ -352,10 +357,21 @@ export function useFormForgeBuilder(options: UseFormForgeBuilderOptions = {}) {
352357
drafts: draft.value.drafts as FormForgeDraftSettings
353358
}
354359

360+
if (autoPublish) {
361+
input.auto_publish = true
362+
}
363+
355364
return input
356365
}
357366

358-
async function save(idempotencyKey?: string): Promise<void> {
367+
async function save(saveOptions: FormForgeBuilderSaveOptions | string = {}): Promise<void> {
368+
const resolvedSaveOptions = typeof saveOptions === 'string'
369+
? { idempotencyKey: saveOptions, autoPublish: false }
370+
: {
371+
idempotencyKey: saveOptions.idempotencyKey,
372+
autoPublish: saveOptions.autoPublish === true
373+
}
374+
359375
if (draft.value.title.trim() === '') {
360376
throw new Error('Title is required to save')
361377
}
@@ -364,7 +380,7 @@ export function useFormForgeBuilder(options: UseFormForgeBuilderOptions = {}) {
364380
error.value = null
365381

366382
try {
367-
const input: FormForgeManagementCreateInput = toManagementInput()
383+
const input: FormForgeManagementCreateInput = toManagementInput(resolvedSaveOptions.autoPublish)
368384

369385
const mutationIdentifier = resolveMutationIdentifier(draft.value)
370386

@@ -376,7 +392,7 @@ export function useFormForgeBuilder(options: UseFormForgeBuilderOptions = {}) {
376392
}
377393

378394
const created = await client.createForm(input, {
379-
idempotencyKey,
395+
idempotencyKey: resolvedSaveOptions.idempotencyKey,
380396
endpoint: options.endpoint,
381397
scope: options.scope
382398
})
@@ -394,7 +410,7 @@ export function useFormForgeBuilder(options: UseFormForgeBuilderOptions = {}) {
394410
} else {
395411
const patchInput: FormForgeManagementPatchInput = input
396412
const patched = await client.patchForm(mutationIdentifier, patchInput, {
397-
idempotencyKey,
413+
idempotencyKey: resolvedSaveOptions.idempotencyKey,
398414
endpoint: options.endpoint,
399415
scope: options.scope
400416
})

src/runtime/composables/useFormForgeClient.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useNuxtApp, useRoute, useRuntimeConfig } from '#imports'
22
import { createFormForgeClient } from '../api'
3-
import type { FormForgeClient, FormForgeClientConfig, FormForgeJsonObject } from '../types'
3+
import type { FormForgeClient, FormForgeClientConfig, FormForgeJsonObject, FormForgeScopedRouteMap } from '../types'
44

55
interface FormForgeNuxtRouteBridge {
66
_route?: {
@@ -55,7 +55,7 @@ function mergeRouteValues(
5555
}
5656

5757
function templateSegmentKey(value: string): string | null {
58-
const matched = value.match(/^\{([a-zA-Z0-9_]+)\}$/)
58+
const matched = value.match(/^\{([a-zA-Z0-9_]+)(?::[^}]+)?\}$/)
5959
return matched?.[1] ?? null
6060
}
6161

@@ -154,6 +154,25 @@ function withInferredMissingValues(
154154
return resolved
155155
}
156156

157+
function inferScopeSourcesFromPath(
158+
scopedRoutes: FormForgeScopedRouteMap | undefined,
159+
currentPath: string | undefined
160+
): Record<string, string> {
161+
const inferredSources: Record<string, string> = {}
162+
163+
for (const scopedRoute of Object.values(scopedRoutes ?? {})) {
164+
const inferredScopeParams = inferParamsFromPath(scopedRoute.prefix, currentPath)
165+
for (const [scopeParam, sourceParam] of Object.entries(scopedRoute.paramsFromRoute)) {
166+
const inferredValue = inferredScopeParams[scopeParam]
167+
if (inferredValue !== undefined && inferredValue !== '' && inferredSources[sourceParam] === undefined) {
168+
inferredSources[sourceParam] = inferredValue
169+
}
170+
}
171+
}
172+
173+
return inferredSources
174+
}
175+
157176
export function useFormForgeClient(config: FormForgeClientConfig = {}): FormForgeClient {
158177
const nuxtApp = useNuxtApp() as ReturnType<typeof useNuxtApp> & FormForgeNuxtRouteBridge
159178
const route = useRoute()
@@ -183,8 +202,13 @@ export function useFormForgeClient(config: FormForgeClientConfig = {}): FormForg
183202
...appRouteParams
184203
}
185204
)
186-
const inferredFromPath = inferParamsFromPath(config.baseURL ?? (runtimePublicConfig as FormForgeClientConfig | undefined)?.baseURL, composableRoutePath || appRoutePath)
187-
return withInferredMissingValues(mergedRouteValues, inferredFromPath)
205+
const resolvedPath = composableRoutePath || appRoutePath
206+
const resolvedBaseURL = config.baseURL ?? (runtimePublicConfig as FormForgeClientConfig | undefined)?.baseURL
207+
const resolvedScopedRoutes = config.scopedRoutes ?? (runtimePublicConfig as FormForgeClientConfig | undefined)?.scopedRoutes
208+
const inferredFromBaseURL = inferParamsFromPath(resolvedBaseURL, resolvedPath)
209+
const withBaseURLValues = withInferredMissingValues(mergedRouteValues, inferredFromBaseURL)
210+
const inferredFromScopes = inferScopeSourcesFromPath(resolvedScopedRoutes, resolvedPath)
211+
return withInferredMissingValues(withBaseURLValues, inferredFromScopes)
188212
}
189213

190214
const baseConfig = runtimePublicConfig as FormForgeClientConfig | undefined

src/runtime/plugin.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defineNuxtPlugin, useRoute, useRuntimeConfig } from '#imports'
22
import { createFormForgeClient } from './api'
3-
import type { FormForgeBeforeRequestContext, FormForgeClientConfig } from './types'
3+
import type { FormForgeBeforeRequestContext, FormForgeClientConfig, FormForgeScopedRouteMap } from './types'
44

55
interface FormForgeNuxtAppHookBridge {
66
callHook(name: 'formforge:beforeRequest', context: FormForgeBeforeRequestContext): Promise<void>
@@ -55,7 +55,7 @@ function mergeRouteValues(
5555
}
5656

5757
function templateSegmentKey(value: string): string | null {
58-
const matched = value.match(/^\{([a-zA-Z0-9_]+)\}$/)
58+
const matched = value.match(/^\{([a-zA-Z0-9_]+)(?::[^}]+)?\}$/)
5959
return matched?.[1] ?? null
6060
}
6161

@@ -154,6 +154,25 @@ function withInferredMissingValues(
154154
return resolved
155155
}
156156

157+
function inferScopeSourcesFromPath(
158+
scopedRoutes: FormForgeScopedRouteMap | undefined,
159+
currentPath: string | undefined
160+
): Record<string, string> {
161+
const inferredSources: Record<string, string> = {}
162+
163+
for (const scopedRoute of Object.values(scopedRoutes ?? {})) {
164+
const inferredScopeParams = inferParamsFromPath(scopedRoute.prefix, currentPath)
165+
for (const [scopeParam, sourceParam] of Object.entries(scopedRoute.paramsFromRoute)) {
166+
const inferredValue = inferredScopeParams[scopeParam]
167+
if (inferredValue !== undefined && inferredValue !== '' && inferredSources[sourceParam] === undefined) {
168+
inferredSources[sourceParam] = inferredValue
169+
}
170+
}
171+
}
172+
173+
return inferredSources
174+
}
175+
157176
export default defineNuxtPlugin((nuxtApp) => {
158177
const runtimeConfig = useRuntimeConfig()
159178
const route = useRoute()
@@ -177,8 +196,11 @@ export default defineNuxtPlugin((nuxtApp) => {
177196
...appRouteParams
178197
}
179198
)
180-
const inferredFromPath = inferParamsFromPath(publicConfig?.baseURL, composableRoutePath || appRoutePath)
181-
return withInferredMissingValues(mergedRouteValues, inferredFromPath)
199+
const resolvedPath = composableRoutePath || appRoutePath
200+
const inferredFromBaseURL = inferParamsFromPath(publicConfig?.baseURL, resolvedPath)
201+
const withBaseURLValues = withInferredMissingValues(mergedRouteValues, inferredFromBaseURL)
202+
const inferredFromScopes = inferScopeSourcesFromPath(publicConfig?.scopedRoutes, resolvedPath)
203+
return withInferredMissingValues(withBaseURLValues, inferredFromScopes)
182204
}
183205

184206
const runtimeBaseURLParams = publicConfig?.baseURLParams

0 commit comments

Comments
 (0)