@stacksjs/defaults 0.75.422 → 0.75.431
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/ai/skills/stacks-alias/SKILL.md +8 -0
- package/ai/skills/stacks-arrays/SKILL.md +3 -1
- package/ai/skills/stacks-build/SKILL.md +3 -0
- package/ai/skills/stacks-cache/SKILL.md +13 -9
- package/ai/skills/stacks-calendar/SKILL.md +20 -12
- package/ai/skills/stacks-chat/SKILL.md +22 -9
- package/ai/skills/stacks-database/SKILL.md +10 -0
- package/ai/skills/stacks-desktop/SKILL.md +7 -0
- package/ai/skills/stacks-events/SKILL.md +6 -2
- package/ai/skills/stacks-i18n/SKILL.md +9 -10
- package/ai/skills/stacks-native/CATALOG.md +70 -62
- package/ai/skills/stacks-notifications/SKILL.md +11 -2
- package/ai/skills/stacks-objects/SKILL.md +6 -1
- package/ai/skills/stacks-payments/SKILL.md +5 -3
- package/ai/skills/stacks-push/SKILL.md +14 -9
- package/ai/skills/stacks-queue/SKILL.md +7 -0
- package/ai/skills/stacks-slug/SKILL.md +7 -1
- package/ai/skills/stacks-strings/SKILL.md +19 -10
- package/ai/skills/stacks-testing/SKILL.md +5 -1
- package/ai/skills/stacks-utils/SKILL.md +1 -1
- package/ai/skills/stacks-validation/SKILL.md +20 -4
- package/ai/skills/stacks-whois/SKILL.md +5 -4
- package/app/Actions/Dashboard/Content/post-input.ts +1 -1
- package/app/Actions/InstallAction.ts +3 -3
- package/app/Models/Content/Post.ts +1 -1
- package/app/Models/Error.ts +1 -1
- package/app/Models/Log.ts +1 -1
- package/app/Models/PaymentMethod.ts +3 -3
- package/app/Models/PaymentProduct.ts +1 -1
- package/app/Models/Subscription.ts +1 -1
- package/app/Models/commerce/PrintDevice.ts +1 -1
- package/app/Models/commerce/Receipt.ts +1 -1
- package/app/Models/realtime/Websocket.ts +2 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/views/cms/blocks/form.stx +31 -36
- package/routes/forms.ts +14 -5
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
<div class="mx-auto px-6 py-8 max-w-xl cms-form" data-form-uuid="{{ props.formUuid }}">
|
|
9
9
|
<script client>
|
|
10
10
|
import type { PublicFormDefinition, PublicFormField, SubmissionErrors } from '@stacksjs/forms'
|
|
11
|
+
import { visibleFields } from '@stacksjs/forms/conditions'
|
|
11
12
|
import { moneyInputError, moneyInputStep, parseMoneyInput } from '@stacksjs/commerce/money'
|
|
12
13
|
|
|
13
14
|
// `_form` carries a whole-form failure; the rest are keyed by field name.
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
|
|
16
17
|
const host = useRef<HTMLElement>('host')
|
|
17
18
|
const definition = state<PublicFormDefinition | null>(null)
|
|
18
|
-
const values =
|
|
19
|
+
const values = state<Record<string, string | number | boolean>>({})
|
|
19
20
|
const errors = state<FormErrors>({})
|
|
20
21
|
const sending = state(false)
|
|
21
22
|
const done = state('')
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
// rather than only the storage path the server answered with.
|
|
28
29
|
const attached = reactive<Record<string, string>>({})
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
onMount(() => {
|
|
31
32
|
if (!host() || definition())
|
|
32
33
|
return
|
|
33
34
|
const uuid = host()?.dataset.formUuid
|
|
@@ -39,25 +40,19 @@
|
|
|
39
40
|
.catch(() => {})
|
|
40
41
|
})
|
|
41
42
|
|
|
43
|
+
function setFieldValue(name: string, value: string | number | boolean): void {
|
|
44
|
+
values.set({ ...values(), [name]: value })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clearFieldValue(name: string): void {
|
|
48
|
+
const next = { ...values() }
|
|
49
|
+
delete next[name]
|
|
50
|
+
values.set(next)
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
function fieldVisible(field: PublicFormField): boolean {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return true
|
|
46
|
-
const results = conditions.rules.map((rule) => {
|
|
47
|
-
const actual = values[rule.field]
|
|
48
|
-
switch (rule.op) {
|
|
49
|
-
case 'eq': return String(actual ?? '') === String(rule.value ?? '')
|
|
50
|
-
case 'neq': return String(actual ?? '') !== String(rule.value ?? '')
|
|
51
|
-
case 'contains': return String(actual ?? '').includes(String(rule.value ?? ''))
|
|
52
|
-
case 'gt': return Number(actual) > Number(rule.value)
|
|
53
|
-
case 'lt': return Number(actual) < Number(rule.value)
|
|
54
|
-
case 'empty': return actual === undefined || actual === null || String(actual) === ''
|
|
55
|
-
case 'not_empty': return actual !== undefined && actual !== null && String(actual) !== ''
|
|
56
|
-
default: return false
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
const matched = conditions.logic === 'all' ? results.every(Boolean) : results.some(Boolean)
|
|
60
|
-
return conditions.action === 'show' ? matched : !matched
|
|
54
|
+
try { return visibleFields(definition()?.fields ?? [], values()).includes(field) }
|
|
55
|
+
catch { return false }
|
|
61
56
|
}
|
|
62
57
|
|
|
63
58
|
/**
|
|
@@ -97,13 +92,13 @@
|
|
|
97
92
|
// Clear the input as well as the value: leaving a rejected file
|
|
98
93
|
// showing next to its own error message reads as though it was taken.
|
|
99
94
|
input.value = ''
|
|
100
|
-
|
|
95
|
+
clearFieldValue(field.name)
|
|
101
96
|
delete attached[field.name]
|
|
102
97
|
errors.set({ ...errors(), [field.name]: (failure as { data?: { message?: string } }).data?.message ?? 'That file could not be uploaded.' })
|
|
103
98
|
return
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
|
|
101
|
+
setFieldValue(field.name, body.path ?? '')
|
|
107
102
|
attached[field.name] = file.name
|
|
108
103
|
}
|
|
109
104
|
catch {
|
|
@@ -128,7 +123,7 @@
|
|
|
128
123
|
if (field.type === 'currency')
|
|
129
124
|
setAmount(field, raw)
|
|
130
125
|
else
|
|
131
|
-
|
|
126
|
+
setFieldValue(field.name, raw)
|
|
132
127
|
}
|
|
133
128
|
|
|
134
129
|
/**
|
|
@@ -149,16 +144,16 @@
|
|
|
149
144
|
const currency = formCurrency()
|
|
150
145
|
const error = moneyInputError(raw, currency, { required: false })
|
|
151
146
|
if (error) {
|
|
152
|
-
|
|
147
|
+
setFieldValue(field.name, raw)
|
|
153
148
|
errors.set({ ...errors(), [field.name]: error })
|
|
154
149
|
return
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
const minor = parseMoneyInput(raw, currency, { required: false })
|
|
158
153
|
if (minor === null)
|
|
159
|
-
|
|
154
|
+
clearFieldValue(field.name)
|
|
160
155
|
else
|
|
161
|
-
|
|
156
|
+
setFieldValue(field.name, minor)
|
|
162
157
|
if (errors()[field.name])
|
|
163
158
|
errors.set({ ...errors(), [field.name]: '' })
|
|
164
159
|
}
|
|
@@ -199,7 +194,7 @@
|
|
|
199
194
|
// 403 "CSRF token mismatch" on every submission.
|
|
200
195
|
let body: { message?: string, redirect?: string }
|
|
201
196
|
try {
|
|
202
|
-
body = await useMutation(`/api/forms/${uuid}/submissions`).mutate({ ...values, _renderedMs: Date.now() - openedAt })
|
|
197
|
+
body = await useMutation(`/api/forms/${uuid}/submissions`).mutate({ ...values(), _renderedMs: Date.now() - openedAt })
|
|
203
198
|
}
|
|
204
199
|
catch (failure) {
|
|
205
200
|
const { status, data } = failure as { status?: number, data?: { errors?: FormErrors, message?: string } }
|
|
@@ -232,41 +227,41 @@
|
|
|
232
227
|
|
|
233
228
|
<div class="grid gap-5 grid-cols-1 sm:grid-cols-2 mt-6">
|
|
234
229
|
<template :for="field in definition()?.fields ?? []">
|
|
235
|
-
<div :if="fieldVisible(field)" class="
|
|
230
|
+
<div :if="fieldVisible(field)" :class="field.width === 'half' ? '' : 'sm:col-span-2'">
|
|
236
231
|
<h3 :if="field.type === 'section_break'" class="mt-2 pt-4 font-medium text-lg border-gray-200 border-t">{{ field.label }}</h3>
|
|
237
232
|
<template :else>
|
|
238
233
|
<label class="block font-medium text-[14px] text-gray-800">{{ field.label }}<span :if="field.required" aria-hidden="true" class="text-red-500"> *</span></label>
|
|
239
|
-
<textarea :if="field.type === 'textarea'" rows="4" :required="field.required" placeholder="
|
|
240
|
-
<select :else-if="field.type === 'select'" :required="field.required" @change="
|
|
234
|
+
<textarea :if="field.type === 'textarea'" rows="4" :required="field.required" :placeholder="field.options.placeholder ?? ''" @input="setFieldValue(field.name, $event.target.value)" class="mt-1.5 px-3.5 py-2.5 w-full text-[15px] border border-gray-300 rounded-lg"></textarea>
|
|
235
|
+
<select :else-if="field.type === 'select'" :required="field.required" @change="setFieldValue(field.name, $event.target.value)" class="mt-1.5 px-3.5 py-2.5 w-full text-[15px] bg-white border border-gray-300 rounded-lg">
|
|
241
236
|
<option value="">Choose...</option>
|
|
242
|
-
<option :for="choice in field.options.choices ?? []" value="
|
|
237
|
+
<option :for="choice in field.options.choices ?? []" :value="choice.value">{{ choice.label }}</option>
|
|
243
238
|
</select>
|
|
244
239
|
<div :else-if="field.type === 'radio'" class="mt-2 space-y-2">
|
|
245
240
|
<label :for="choice in field.options.choices ?? []" class="flex gap-2.5 items-center text-[15px]">
|
|
246
|
-
<input type="radio" name="
|
|
241
|
+
<input type="radio" :name="field.name" :value="choice.value" @change="setFieldValue(field.name, $event.target.value)" class="h-4 w-4">
|
|
247
242
|
{{ choice.label }}
|
|
248
243
|
</label>
|
|
249
244
|
</div>
|
|
250
245
|
<label :else-if="field.type === 'checkbox'" class="flex gap-2.5 items-center mt-2 text-[15px]">
|
|
251
|
-
<input type="checkbox" @change="
|
|
246
|
+
<input type="checkbox" @change="setFieldValue(field.name, $event.target.checked)" class="h-4 w-4 rounded">
|
|
252
247
|
{{ field.options.placeholder ?? 'Yes' }}
|
|
253
248
|
</label>
|
|
254
249
|
{{-- A file field used to fall through to the text input below, so
|
|
255
250
|
a person typed prose into what should have been a picker and
|
|
256
251
|
the submission could never satisfy it. --}}
|
|
257
252
|
<div :else-if="field.type === 'file'" class="mt-1.5">
|
|
258
|
-
<input type="file" :required="field.required && !values[field.name]" :disabled="uploading[field.name]" accept="
|
|
253
|
+
<input type="file" :required="field.required && !values()[field.name]" :disabled="uploading[field.name]" :accept="acceptAttr(field)" @change="uploadFile(field, $event)" class="block file:mr-3 file:px-3.5 file:py-2 w-full text-[15px] file:font-medium file:text-[14px] file:bg-gray-100 file:border-0 file:rounded-lg">
|
|
259
254
|
<p :if="uploading[field.name]" class="mt-1.5 text-[13px] text-gray-500">Uploading...</p>
|
|
260
255
|
<p :else-if="attached[field.name]" class="mt-1.5 text-[13px] text-green-700">Attached {{ attached[field.name] }}</p>
|
|
261
256
|
</div>
|
|
262
|
-
<input :else type="
|
|
257
|
+
<input :else :type="field.type === 'email' ? 'email' : (field.type === 'phone' ? 'tel' : (field.type === 'date' ? 'date' : (field.type === 'currency' ? 'number' : 'text')))" :required="field.required" :placeholder="field.options.placeholder ?? ''" :step="field.type === 'currency' ? moneyInputStep(formCurrency()) : null" @input="setInput(field, $event.target.value)" class="mt-1.5 px-3.5 py-2.5 w-full text-[15px] border border-gray-300 rounded-lg">
|
|
263
258
|
<p :if="errors()[field.name]" class="mt-1.5 text-[13px] text-red-600">{{ errors()[field.name] }}</p>
|
|
264
259
|
</template>
|
|
265
260
|
</div>
|
|
266
261
|
</template>
|
|
267
262
|
</div>
|
|
268
263
|
|
|
269
|
-
<input type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true" class="hidden" @input="
|
|
264
|
+
<input type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true" class="hidden" @input="setFieldValue('_hp', $event.target.value)">
|
|
270
265
|
|
|
271
266
|
<button type="submit" :disabled="sending() || anyUploading()" class="mt-7 px-6 py-3 font-medium text-white bg-gray-900 rounded-lg disabled:opacity-60 transition-transform active:scale-[0.98]">
|
|
272
267
|
{{ sending() ? 'Sending...' : (definition()?.submitLabel ?? 'Submit') }}
|
package/routes/forms.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { response, route } from '@stacksjs/router'
|
|
|
13
13
|
* allowlist - deliberately not shipped until something needs it.)
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
async function siteIdForRequest(request: any): Promise<number | null> {
|
|
16
|
+
async function siteIdForRequest(request: any): Promise<number | null | undefined> {
|
|
17
17
|
const options = sitesOptions()
|
|
18
18
|
if (!options.enabled)
|
|
19
19
|
return null
|
|
@@ -22,13 +22,16 @@ async function siteIdForRequest(request: any): Promise<number | null> {
|
|
|
22
22
|
? request.headers
|
|
23
23
|
: new Headers(request.headers ?? {})
|
|
24
24
|
const site = await resolveSiteByHost(requestHost(headers, options), undefined, options)
|
|
25
|
-
return site?.id
|
|
25
|
+
return site?.id
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
route.get('/api/forms/{uuid}', async (request: any) => {
|
|
29
29
|
const { loadFormByUuid, publicDefinition } = await import('@stacksjs/forms')
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const siteId = await siteIdForRequest(request)
|
|
32
|
+
if (siteId === undefined)
|
|
33
|
+
return response.notFound('Form not found')
|
|
34
|
+
const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), siteId)
|
|
32
35
|
if (!form || form.status === 'draft')
|
|
33
36
|
return response.notFound('Form not found')
|
|
34
37
|
|
|
@@ -50,7 +53,10 @@ route.get('/api/forms/{uuid}', async (request: any) => {
|
|
|
50
53
|
route.post('/api/forms/{uuid}/uploads', async (request: any) => {
|
|
51
54
|
const { checkUpload, fileFieldNamed, formUploadPrefix, loadFormByUuid, resolveUploadLimits } = await import('@stacksjs/forms')
|
|
52
55
|
|
|
53
|
-
const
|
|
56
|
+
const siteId = await siteIdForRequest(request)
|
|
57
|
+
if (siteId === undefined)
|
|
58
|
+
return response.notFound('Form not found')
|
|
59
|
+
const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), siteId)
|
|
54
60
|
if (!form || form.status === 'draft')
|
|
55
61
|
return response.notFound('Form not found')
|
|
56
62
|
|
|
@@ -84,7 +90,10 @@ route.post('/api/forms/{uuid}/uploads', async (request: any) => {
|
|
|
84
90
|
route.post('/api/forms/{uuid}/submissions', async (request: any) => {
|
|
85
91
|
const { dispatchSubmissionNotifications, loadFormByUuid, submissionIdentity, submitForm } = await import('@stacksjs/forms')
|
|
86
92
|
|
|
87
|
-
const
|
|
93
|
+
const siteId = await siteIdForRequest(request)
|
|
94
|
+
if (siteId === undefined)
|
|
95
|
+
return response.notFound('Form not found')
|
|
96
|
+
const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), siteId)
|
|
88
97
|
if (!form)
|
|
89
98
|
return response.notFound('Form not found')
|
|
90
99
|
|