@stacksjs/defaults 0.74.63 → 0.74.65
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/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/Marketing/ComingSoon.stx +28 -22
- package/resources/plugins/preloader.ts +2 -6
- package/resources/plugins/user-functions.ts +23 -0
- package/resources/views/cms/blocks/form.stx +24 -24
- package/resources/views/coming-soon.stx +10 -1
- package/routes/forms.ts +8 -39
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.65",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
58
|
-
"@stacksjs/mobile": "^0.74.
|
|
58
|
+
"@stacksjs/mobile": "^0.74.65",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
<script>
|
|
1
|
+
<script server>
|
|
2
|
+
import { defineProps, withDefaults } from 'stx'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The default holding page body, mounted by `views/coming-soon.stx`.
|
|
6
|
+
*
|
|
7
|
+
* Every app that turns on coming-soon mode shows this, so it names the app
|
|
8
|
+
* it is given and nothing else. It used to carry the Stacks logo, a pitch
|
|
9
|
+
* for the framework, a link to the framework's docs and a screenshot of the
|
|
10
|
+
* framework dashboard.
|
|
11
|
+
*/
|
|
2
12
|
interface ComingSoonProps {
|
|
3
|
-
|
|
13
|
+
appName?: string
|
|
14
|
+
description?: string
|
|
4
15
|
}
|
|
5
16
|
|
|
6
|
-
|
|
17
|
+
const { appName, description } = withDefaults(
|
|
18
|
+
defineProps<ComingSoonProps>(),
|
|
19
|
+
{ appName: '', description: '' },
|
|
20
|
+
)
|
|
7
21
|
</script>
|
|
8
22
|
|
|
9
23
|
<div class="overflow-hidden relative bg-white isolate">
|
|
@@ -16,22 +30,23 @@ defineProps<ComingSoonProps>()
|
|
|
16
30
|
<rect width="100%" height="100%" stroke-width="0" fill="url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)" />
|
|
17
31
|
</svg>
|
|
18
32
|
|
|
19
|
-
<div class="
|
|
20
|
-
<div class="
|
|
21
|
-
|
|
33
|
+
<div class="mx-auto pb-24 pt-10 px-6 sm:pb-32 lg:px-8 lg:py-20 max-w-7xl">
|
|
34
|
+
<div class="mx-auto lg:pt-24 max-w-2xl">
|
|
35
|
+
@if(appName)
|
|
36
|
+
<p class="font-semibold text-gray-900 text-lg">{{ appName }}</p>
|
|
37
|
+
@endif
|
|
22
38
|
|
|
23
39
|
<div class="mt-24 sm:mt-32 lg:mt-16">
|
|
24
40
|
<span class="inline-block px-3 py-1 font-semibold leading-6 text-blue-600 text-sm bg-blue-600/10 ring-1 ring-blue-600/10 ring-inset rounded-full">Coming soon</span>
|
|
25
41
|
</div>
|
|
26
42
|
|
|
27
43
|
<h1 class="mt-10 font-bold text-4xl text-gray-900 tracking-tight sm:text-6xl">
|
|
28
|
-
|
|
29
|
-
Faster.
|
|
44
|
+
Something new is on the way.
|
|
30
45
|
</h1>
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
@if(description)
|
|
48
|
+
<p class="mt-6 leading-8 text-gray-600 text-lg">{{ description }}</p>
|
|
49
|
+
@endif
|
|
35
50
|
|
|
36
51
|
<p class="mt-3 italic leading-8 text-gray-600">
|
|
37
52
|
Enter your email below to stay in the loop.
|
|
@@ -61,22 +76,13 @@ defineProps<ComingSoonProps>()
|
|
|
61
76
|
<div id="coming-soon-spinner" class="hidden h-4 w-4 animate-spin i-hugeicons-loading-01"></div>
|
|
62
77
|
</button>
|
|
63
78
|
</form>
|
|
64
|
-
|
|
65
|
-
<a href="https://stacksjs-docs.netlify.app" class="font-semibold leading-6 text-gray-900 text-sm">Learn more <span aria-hidden="true">→</span></a>
|
|
66
79
|
</div>
|
|
67
80
|
|
|
68
81
|
<p id="coming-soon-error" class="hidden pt-2 text-red-500 text-xs"></p>
|
|
69
82
|
<p id="coming-soon-success" class="hidden pt-2 text-green-700 text-xs">
|
|
70
|
-
Thanks
|
|
83
|
+
Thanks, we'll be in touch.
|
|
71
84
|
</p>
|
|
72
85
|
</div>
|
|
73
|
-
<div class="flex lg:flex-none mt-16 mx-auto sm:mt-24 lg:ml-10 lg:mr-0 lg:mt-0 xl:ml-32 max-w-2xl lg:max-w-none">
|
|
74
|
-
<div class="flex-none max-w-3xl sm:max-w-5xl lg:max-w-none">
|
|
75
|
-
<div class="-m-2 p-2 lg:-m-4 lg:p-4 bg-gray-900/5 ring-1 ring-gray-900/10 ring-inset rounded-xl lg:rounded-2xl">
|
|
76
|
-
<img src="/images/screenshots/dashboard.png" alt="App screenshot" width="2432" height="1442" class="w-[76rem] ring-1 ring-gray-900/10 rounded-md shadow-2xl">
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
86
|
</div>
|
|
81
87
|
</div>
|
|
82
88
|
|
|
@@ -145,7 +151,7 @@ if (form) {
|
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
catch {
|
|
148
|
-
showError('Network error
|
|
154
|
+
showError('Network error, please try again.')
|
|
149
155
|
}
|
|
150
156
|
finally {
|
|
151
157
|
setLoading(false)
|
|
@@ -209,6 +209,7 @@ async function belongsToThisProject(specifier: string): Promise<boolean> {
|
|
|
209
209
|
// explicitly when needed — see #1835 root cause 3.
|
|
210
210
|
export async function loadAutoImports() {
|
|
211
211
|
const { Glob } = await import('bun')
|
|
212
|
+
const { userFunctionFiles } = await import('./user-functions')
|
|
212
213
|
const pathPackage = '@stacksjs/' + 'path'
|
|
213
214
|
const path = await import('../../../core/path/src/index.ts')
|
|
214
215
|
.catch(() => import(pathPackage))
|
|
@@ -289,12 +290,7 @@ export async function loadAutoImports() {
|
|
|
289
290
|
const functionsPath = path.resourcesPath('functions')
|
|
290
291
|
const glob = new Glob('**/*.ts')
|
|
291
292
|
|
|
292
|
-
for await (const file of
|
|
293
|
-
cwd: functionsPath,
|
|
294
|
-
absolute: true,
|
|
295
|
-
onlyFiles: true,
|
|
296
|
-
})) {
|
|
297
|
-
if (file.endsWith('.d.ts')) continue
|
|
293
|
+
for await (const file of userFunctionFiles(functionsPath)) {
|
|
298
294
|
|
|
299
295
|
try {
|
|
300
296
|
const module = await import(file)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Glob } from 'bun'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The `.ts` files an app keeps in `resources/functions`, or none.
|
|
6
|
+
*
|
|
7
|
+
* An app with no functions of its own is normal - `buddy new --minimal`
|
|
8
|
+
* leaves the folder empty, and git does not keep an empty folder - but
|
|
9
|
+
* `Glob.scan` throws ENOENT on a missing `cwd`. Unguarded, that aborted the
|
|
10
|
+
* preload for every command in a fresh checkout: CI's `buddy key:generate`
|
|
11
|
+
* died with a bare "no such file or directory, open .../resources/functions"
|
|
12
|
+
* and no stack, while the same app ran fine on the laptop that still had the
|
|
13
|
+
* empty folder. The model, job and controller scans below were already
|
|
14
|
+
* wrapped; this one was not.
|
|
15
|
+
*/
|
|
16
|
+
export async function* userFunctionFiles(dir: string): AsyncGenerator<string> {
|
|
17
|
+
if (!existsSync(dir))
|
|
18
|
+
return
|
|
19
|
+
for await (const file of new Glob('**/*.ts').scan({ cwd: dir, absolute: true, onlyFiles: true })) {
|
|
20
|
+
if (!file.endsWith('.d.ts'))
|
|
21
|
+
yield file
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Form block: renders a @stacksjs/forms form inline on a CMS page.
|
|
3
3
|
`props.formUuid` names the form; the client script fetches the public
|
|
4
4
|
definition (same-origin), renders the fields, evaluates conditions live,
|
|
5
|
-
and submits
|
|
5
|
+
and submits through useMutation, which repeats the page's CSRF cookie in
|
|
6
|
+
the header the submit route checks.
|
|
6
7
|
--}}
|
|
7
8
|
<div class="cms-form mx-auto max-w-xl px-6 py-8" data-form-uuid="{{ props.formUuid }}">
|
|
8
9
|
<script client>
|
|
@@ -84,24 +85,24 @@
|
|
|
84
85
|
payload.append('field', field.name)
|
|
85
86
|
payload.append('file', file)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
// Through the data layer rather than a bare fetch(): it is what echoes
|
|
89
|
+
// the CSRF cookie the submit routes require, and it passes FormData
|
|
90
|
+
// through untouched.
|
|
91
|
+
let body: { path?: string }
|
|
92
|
+
try {
|
|
93
|
+
body = await useMutation(`/api/forms/${uuid}/uploads`).mutate(payload)
|
|
94
|
+
}
|
|
95
|
+
catch (failure) {
|
|
95
96
|
// Clear the input as well as the value: leaving a rejected file
|
|
96
97
|
// showing next to its own error message reads as though it was taken.
|
|
97
98
|
input.value = ''
|
|
98
99
|
delete values[field.name]
|
|
99
100
|
delete attached[field.name]
|
|
100
|
-
errors.set({ ...errors(), [field.name]:
|
|
101
|
+
errors.set({ ...errors(), [field.name]: (failure as { data?: { message?: string } }).data?.message ?? 'That file could not be uploaded.' })
|
|
101
102
|
return
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
values[field.name] = body.path
|
|
105
|
+
values[field.name] = body.path ?? ''
|
|
105
106
|
attached[field.name] = file.name
|
|
106
107
|
}
|
|
107
108
|
catch {
|
|
@@ -144,20 +145,19 @@
|
|
|
144
145
|
|
|
145
146
|
try {
|
|
146
147
|
const uuid = host()?.dataset.formUuid
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const body = await reply.json().catch(() => ({}))
|
|
154
|
-
|
|
155
|
-
if (reply.status === 422) {
|
|
156
|
-
errors.set(body.errors ?? {})
|
|
157
|
-
return
|
|
148
|
+
// useMutation, not fetch(): the submit route is CSRF-protected and the
|
|
149
|
+
// data layer is what repeats the cookie in the header. A bare fetch got
|
|
150
|
+
// 403 "CSRF token mismatch" on every submission.
|
|
151
|
+
let body: { message?: string, redirect?: string }
|
|
152
|
+
try {
|
|
153
|
+
body = await useMutation(`/api/forms/${uuid}/submissions`).mutate({ ...values, _renderedMs: Date.now() - openedAt })
|
|
158
154
|
}
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
catch (failure) {
|
|
156
|
+
const { status, data } = failure as { status?: number, data?: { errors?: FormErrors, message?: string } }
|
|
157
|
+
if (status === 422)
|
|
158
|
+
errors.set(data?.errors ?? {})
|
|
159
|
+
else
|
|
160
|
+
errors.set({ _form: data?.message ?? 'Something went wrong. Please try again.' })
|
|
161
161
|
return
|
|
162
162
|
}
|
|
163
163
|
if (body.redirect) {
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
<script server>
|
|
2
|
+
import { config } from '@stacksjs/config'
|
|
3
|
+
|
|
4
|
+
// The app's own name and description, so the holding page describes the app
|
|
5
|
+
// that turned coming-soon mode on rather than the framework.
|
|
6
|
+
const appName = config.app.name || ''
|
|
7
|
+
const description = config.app.description || ''
|
|
8
|
+
</script>
|
|
9
|
+
|
|
1
10
|
<main>
|
|
2
|
-
<ComingSoon />
|
|
11
|
+
<ComingSoon appName="{{ appName }}" description="{{ description }}" />
|
|
3
12
|
</main>
|
package/routes/forms.ts
CHANGED
|
@@ -82,7 +82,7 @@ route.post('/api/forms/{uuid}/uploads', async (request: any) => {
|
|
|
82
82
|
}).rateLimit(20, 'minute')
|
|
83
83
|
|
|
84
84
|
route.post('/api/forms/{uuid}/submissions', async (request: any) => {
|
|
85
|
-
const { dispatchSubmissionNotifications, loadFormByUuid, submitForm } = await import('@stacksjs/forms')
|
|
85
|
+
const { dispatchSubmissionNotifications, loadFormByUuid, submissionIdentity, submitForm } = await import('@stacksjs/forms')
|
|
86
86
|
|
|
87
87
|
const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), await siteIdForRequest(request))
|
|
88
88
|
if (!form)
|
|
@@ -105,10 +105,14 @@ route.post('/api/forms/{uuid}/submissions', async (request: any) => {
|
|
|
105
105
|
|
|
106
106
|
// Fire-and-forget AFTER the write: a slow mail transport must not hold a
|
|
107
107
|
// parent's phone on a spinner, and a failed one must not undo the answers.
|
|
108
|
+
// The identity lookup belongs inside that chain too. It used to be awaited
|
|
109
|
+
// in the handler, so when it threw - and it always did, selecting a
|
|
110
|
+
// `values` column that has never existed - the visitor got a 500 for an
|
|
111
|
+
// answer that was already saved, and every retry saved another copy.
|
|
108
112
|
if (result.submissionId > 0) {
|
|
109
|
-
void
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
void submissionIdentity(result.submissionId)
|
|
114
|
+
.then(identity => dispatchSubmissionNotifications(form, result, identity))
|
|
115
|
+
.catch(() => {})
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
return response.json({
|
|
@@ -119,38 +123,3 @@ route.post('/api/forms/{uuid}/submissions', async (request: any) => {
|
|
|
119
123
|
redirect: result.redirect,
|
|
120
124
|
}, { status: 201 })
|
|
121
125
|
}).rateLimit(10, 'minute')
|
|
122
|
-
|
|
123
|
-
async function submissionIdentity(submissionId: number): Promise<{ email: string | null, name: string | null, values: Record<string, unknown> }> {
|
|
124
|
-
const { db } = await import('@stacksjs/database')
|
|
125
|
-
const row = await db
|
|
126
|
-
.selectFrom('form_submissions')
|
|
127
|
-
.where('id', '=', submissionId)
|
|
128
|
-
.select(['email', 'name', 'values'])
|
|
129
|
-
.executeTakeFirst() as { email: string | null, name: string | null, values: string | null } | undefined
|
|
130
|
-
|
|
131
|
-
let values: Record<string, unknown> = {}
|
|
132
|
-
try {
|
|
133
|
-
values = row?.values ? JSON.parse(row.values) as Record<string, unknown> : {}
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// unreadable values only degrade the notification summary
|
|
137
|
-
}
|
|
138
|
-
return { email: row?.email ?? null, name: row?.name ?? null, values }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Admin CSV export. Auth'd; site scoping rides the form lookup. */
|
|
142
|
-
route.get('/api/admin/forms/{uuid}/submissions.csv', async (request: any) => {
|
|
143
|
-
const { exportSubmissionsCsv, loadFormByUuid } = await import('@stacksjs/forms')
|
|
144
|
-
|
|
145
|
-
const form = await loadFormByUuid(String(request.params?.uuid ?? request.param?.('uuid') ?? ''), await siteIdForRequest(request))
|
|
146
|
-
if (!form)
|
|
147
|
-
return response.notFound('Form not found')
|
|
148
|
-
|
|
149
|
-
const csv = await exportSubmissionsCsv(form)
|
|
150
|
-
return new Response(csv, {
|
|
151
|
-
headers: {
|
|
152
|
-
'Content-Type': 'text/csv; charset=utf-8',
|
|
153
|
-
'Content-Disposition': `attachment; filename="${form.handle}-submissions.csv"`,
|
|
154
|
-
},
|
|
155
|
-
})
|
|
156
|
-
}).middleware('auth')
|