@betterstart/cli 0.1.36 → 0.1.37
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/dist/cli.js +124 -18
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -353,7 +353,7 @@ import path3 from "path";
|
|
|
353
353
|
|
|
354
354
|
// src/utils/string.ts
|
|
355
355
|
function toPascalCase(str) {
|
|
356
|
-
return str.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
356
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
|
|
357
357
|
}
|
|
358
358
|
function toCamelCase(str) {
|
|
359
359
|
const p7 = toPascalCase(str);
|
|
@@ -816,7 +816,7 @@ function DeleteAction({ id }: { id: number }) {
|
|
|
816
816
|
<AlertDialogFooter>
|
|
817
817
|
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
818
818
|
<AlertDialogAction
|
|
819
|
-
onClick={(e) => { e.preventDefault(); handleDelete() }}
|
|
819
|
+
onClick={(e: React.MouseEvent) => { e.preventDefault(); handleDelete() }}
|
|
820
820
|
disabled={isPending}
|
|
821
821
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
822
822
|
>
|
|
@@ -834,14 +834,14 @@ export const columns: ColumnDef<${pascal}SubmissionData>[] = [
|
|
|
834
834
|
header: ({ table }) => (
|
|
835
835
|
<Checkbox
|
|
836
836
|
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
|
|
837
|
-
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
837
|
+
onCheckedChange={(value: boolean) => table.toggleAllPageRowsSelected(!!value)}
|
|
838
838
|
aria-label="Select all"
|
|
839
839
|
/>
|
|
840
840
|
),
|
|
841
841
|
cell: ({ row }) => (
|
|
842
842
|
<Checkbox
|
|
843
843
|
checked={row.getIsSelected()}
|
|
844
|
-
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
844
|
+
onCheckedChange={(value: boolean) => row.toggleSelected(!!value)}
|
|
845
845
|
aria-label="Select row"
|
|
846
846
|
/>
|
|
847
847
|
),
|
|
@@ -990,7 +990,7 @@ export function ${pascal}SubmissionsPageContent<TValue>({
|
|
|
990
990
|
<AlertDialogFooter>
|
|
991
991
|
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
992
992
|
<AlertDialogAction
|
|
993
|
-
onClick={(e) => { e.preventDefault(); handleBulkDelete() }}
|
|
993
|
+
onClick={(e: React.MouseEvent) => { e.preventDefault(); handleBulkDelete() }}
|
|
994
994
|
disabled={isPending}
|
|
995
995
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
996
996
|
>
|
|
@@ -1425,6 +1425,20 @@ function generateViewPage(pascal, kebab, fields, label, includeDynamic) {
|
|
|
1425
1425
|
</p>
|
|
1426
1426
|
</div>`;
|
|
1427
1427
|
}
|
|
1428
|
+
if (f.type === "list" || f.type === "multiselect") {
|
|
1429
|
+
return ` <div className="space-y-1">
|
|
1430
|
+
<p className="text-sm font-medium text-muted-foreground">${lbl}</p>
|
|
1431
|
+
<div className="text-sm">
|
|
1432
|
+
{Array.isArray(submission.${name}) && submission.${name}.length > 0 ? (
|
|
1433
|
+
<ul className="list-disc list-inside space-y-1">
|
|
1434
|
+
{submission.${name}.map((item: Record<string, unknown>, idx: number) => (
|
|
1435
|
+
<li key={idx}>{typeof item === 'string' ? item : Object.entries(item).map(([k, v]) => \`\${k}: \${v}\`).join(', ')}</li>
|
|
1436
|
+
))}
|
|
1437
|
+
</ul>
|
|
1438
|
+
) : '-'}
|
|
1439
|
+
</div>
|
|
1440
|
+
</div>`;
|
|
1441
|
+
}
|
|
1428
1442
|
return ` <div className="space-y-1">
|
|
1429
1443
|
<p className="text-sm font-medium text-muted-foreground">${lbl}</p>
|
|
1430
1444
|
<p className="text-sm">{submission.${name} ?? '-'}</p>
|
|
@@ -1823,7 +1837,7 @@ export async function create${pascal}Submission(
|
|
|
1823
1837
|
// Resolve notification emails from form settings or env var
|
|
1824
1838
|
const settings = await getFormSettings('${formName}')
|
|
1825
1839
|
const notificationEmails = settings?.notificationEmails
|
|
1826
|
-
? settings.notificationEmails.split(',').map((e) => e.trim()).filter(Boolean)
|
|
1840
|
+
? settings.notificationEmails.split(',').map((e: string) => e.trim()).filter(Boolean)
|
|
1827
1841
|
: process.env.NOTIFICATION_EMAIL
|
|
1828
1842
|
? [process.env.NOTIFICATION_EMAIL]
|
|
1829
1843
|
: []
|
|
@@ -1891,7 +1905,7 @@ export async function export${pascal}SubmissionsCSV(): Promise<string> {
|
|
|
1891
1905
|
const headers = Object.keys(submissions[0]).join(',')
|
|
1892
1906
|
const rows = submissions.map((sub) =>
|
|
1893
1907
|
Object.values(sub)
|
|
1894
|
-
.map((val) => {
|
|
1908
|
+
.map((val: unknown) => {
|
|
1895
1909
|
if (val === null || val === undefined) return ''
|
|
1896
1910
|
const str = String(val)
|
|
1897
1911
|
if (str.includes(',') || str.includes('"') || str.includes('\\n')) {
|
|
@@ -2130,7 +2144,7 @@ function formFieldToZodType(field, options = {}) {
|
|
|
2130
2144
|
}
|
|
2131
2145
|
return isRequired ? `z.string().min(1, '${label} is required')` : "z.string().optional()";
|
|
2132
2146
|
case "checkbox":
|
|
2133
|
-
return isRequired ? `z.
|
|
2147
|
+
return isRequired ? `z.boolean().refine(val => val === true, { message: '${label} is required' })` : "z.boolean().optional()";
|
|
2134
2148
|
case "multiselect":
|
|
2135
2149
|
return isRequired ? `z.array(z.string()).min(1, '${label} is required')` : "z.array(z.string()).optional()";
|
|
2136
2150
|
case "timezone":
|
|
@@ -2212,6 +2226,9 @@ function toTypeScriptType(field, mode = "output") {
|
|
|
2212
2226
|
}
|
|
2213
2227
|
|
|
2214
2228
|
// src/generators/form-pipeline/form-component.ts
|
|
2229
|
+
function escapeJsx(str) {
|
|
2230
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2231
|
+
}
|
|
2215
2232
|
function generateFormComponent(schema, cwd, cmsDir, options) {
|
|
2216
2233
|
const formName = schema.name;
|
|
2217
2234
|
const pascal = toPascalCase(formName);
|
|
@@ -2228,22 +2245,31 @@ function generateFormComponent(schema, cwd, cmsDir, options) {
|
|
|
2228
2245
|
if (f.type === "checkbox") return ` ${f.name}: false`;
|
|
2229
2246
|
if (f.type === "number") return ` ${f.name}: undefined`;
|
|
2230
2247
|
if (f.type === "multiselect" || f.type === "list") return ` ${f.name}: []`;
|
|
2248
|
+
if (f.type === "select" || f.type === "radio") return ` ${f.name}: undefined`;
|
|
2231
2249
|
if (f.defaultValue !== void 0) return ` ${f.name}: '${f.defaultValue}'`;
|
|
2232
2250
|
return ` ${f.name}: ''`;
|
|
2233
2251
|
}).join(",\n");
|
|
2252
|
+
const listFields = fields.filter(
|
|
2253
|
+
(f) => f.name && f.type === "list" && f.fields && f.fields.length > 0
|
|
2254
|
+
);
|
|
2255
|
+
const hasListFields = listFields.length > 0;
|
|
2256
|
+
const fieldArrayDecls = listFields.map(
|
|
2257
|
+
(f) => ` const ${f.name}FieldArray = useFieldArray({ control: form.control, name: '${f.name}' })`
|
|
2258
|
+
).join("\n");
|
|
2234
2259
|
const fieldJSX = fields.filter((f) => f.name && !f.hidden).map((f) => generateFieldJSX(f)).join("\n\n");
|
|
2235
2260
|
const submitText = schema.submitButtonText || "Submit";
|
|
2236
|
-
const successMessage = (schema.successMessage || "Form submitted successfully!")
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2261
|
+
const successMessage = escapeJsx(schema.successMessage || "Form submitted successfully!");
|
|
2262
|
+
const rhfImport = hasListFields ? `import { useFieldArray, useForm } from 'react-hook-form'` : `import { useForm } from 'react-hook-form'`;
|
|
2263
|
+
const fieldArraySetup = hasListFields ? `
|
|
2264
|
+
${fieldArrayDecls}
|
|
2265
|
+
` : "";
|
|
2240
2266
|
const content = `'use client'
|
|
2241
2267
|
|
|
2242
2268
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2243
2269
|
import { useState } from 'react'
|
|
2244
|
-
|
|
2270
|
+
${rhfImport}
|
|
2245
2271
|
import { z } from 'zod/v3'
|
|
2246
|
-
import { create${pascal}Submission } from '@cms/actions/${
|
|
2272
|
+
import { create${pascal}Submission } from '@cms/actions/${kebab}-form'
|
|
2247
2273
|
import { Button } from '@cms/components/ui/button'
|
|
2248
2274
|
import {
|
|
2249
2275
|
Form,
|
|
@@ -2280,7 +2306,7 @@ export function ${pascal}Form() {
|
|
|
2280
2306
|
${defaults}
|
|
2281
2307
|
},
|
|
2282
2308
|
})
|
|
2283
|
-
|
|
2309
|
+
${fieldArraySetup}
|
|
2284
2310
|
async function onSubmit(values: FormValues) {
|
|
2285
2311
|
setSubmitting(true)
|
|
2286
2312
|
try {
|
|
@@ -2328,11 +2354,11 @@ ${fieldJSX}
|
|
|
2328
2354
|
}
|
|
2329
2355
|
function generateFieldJSX(field) {
|
|
2330
2356
|
const name = field.name || "";
|
|
2331
|
-
const label = field.label;
|
|
2357
|
+
const label = escapeJsx(field.label);
|
|
2332
2358
|
const placeholder = field.placeholder || "";
|
|
2333
2359
|
const hint = field.hint || "";
|
|
2334
2360
|
const hintJSX = hint ? `
|
|
2335
|
-
<FormDescription>${hint}</FormDescription>` : "";
|
|
2361
|
+
<FormDescription>${escapeJsx(hint)}</FormDescription>` : "";
|
|
2336
2362
|
const requiredStar = field.required ? ' <span className="text-destructive">*</span>' : "";
|
|
2337
2363
|
switch (field.type) {
|
|
2338
2364
|
case "textarea":
|
|
@@ -2353,7 +2379,7 @@ function generateFieldJSX(field) {
|
|
|
2353
2379
|
case "radio":
|
|
2354
2380
|
if (field.options && field.options.length > 0) {
|
|
2355
2381
|
const optionItems = field.options.map(
|
|
2356
|
-
(opt) => ` <SelectItem value="${opt.value}">${opt.label}</SelectItem>`
|
|
2382
|
+
(opt) => ` <SelectItem value="${opt.value}">${escapeJsx(opt.label)}</SelectItem>`
|
|
2357
2383
|
).join("\n");
|
|
2358
2384
|
return ` <FormField
|
|
2359
2385
|
control={form.control}
|
|
@@ -2429,6 +2455,11 @@ ${optionItems}
|
|
|
2429
2455
|
requiredStar,
|
|
2430
2456
|
"tel"
|
|
2431
2457
|
);
|
|
2458
|
+
case "file":
|
|
2459
|
+
case "upload":
|
|
2460
|
+
return generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, "file");
|
|
2461
|
+
case "list":
|
|
2462
|
+
return generateListFieldJSX(field, name, label, hintJSX, requiredStar);
|
|
2432
2463
|
default:
|
|
2433
2464
|
return generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, "text");
|
|
2434
2465
|
}
|
|
@@ -2448,6 +2479,81 @@ function generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, i
|
|
|
2448
2479
|
)}
|
|
2449
2480
|
/>`;
|
|
2450
2481
|
}
|
|
2482
|
+
function generateListFieldJSX(field, name, label, hintJSX, requiredStar) {
|
|
2483
|
+
if (!field.fields || field.fields.length === 0) {
|
|
2484
|
+
return generateTextFieldJSX(name, label, field.placeholder || "", hintJSX, requiredStar, "text");
|
|
2485
|
+
}
|
|
2486
|
+
const singularLabel = singularize(label);
|
|
2487
|
+
const nestedFieldsJSX = field.fields.map((nf) => {
|
|
2488
|
+
const nfLabel = escapeJsx(nf.label || nf.name || "");
|
|
2489
|
+
const nfPlaceholder = nf.placeholder || "";
|
|
2490
|
+
if (nf.type === "select" && nf.options && nf.options.length > 0) {
|
|
2491
|
+
const selectItems = nf.options.map(
|
|
2492
|
+
(opt) => ` <SelectItem value="${opt.value}">${escapeJsx(opt.label)}</SelectItem>`
|
|
2493
|
+
).join("\n");
|
|
2494
|
+
return ` <FormField
|
|
2495
|
+
control={form.control}
|
|
2496
|
+
name={\`${name}.\${index}.${nf.name}\`}
|
|
2497
|
+
render={({ field: formField }) => (
|
|
2498
|
+
<FormItem className="flex-1">
|
|
2499
|
+
<FormLabel>${nfLabel}</FormLabel>
|
|
2500
|
+
<Select onValueChange={formField.onChange} defaultValue={formField.value}>
|
|
2501
|
+
<FormControl>
|
|
2502
|
+
<SelectTrigger>
|
|
2503
|
+
<SelectValue placeholder="${nf.placeholder || "Select..."}" />
|
|
2504
|
+
</SelectTrigger>
|
|
2505
|
+
</FormControl>
|
|
2506
|
+
<SelectContent>
|
|
2507
|
+
${selectItems}
|
|
2508
|
+
</SelectContent>
|
|
2509
|
+
</Select>
|
|
2510
|
+
<FormMessage />
|
|
2511
|
+
</FormItem>
|
|
2512
|
+
)}
|
|
2513
|
+
/>`;
|
|
2514
|
+
}
|
|
2515
|
+
return ` <FormField
|
|
2516
|
+
control={form.control}
|
|
2517
|
+
name={\`${name}.\${index}.${nf.name}\`}
|
|
2518
|
+
render={({ field: formField }) => (
|
|
2519
|
+
<FormItem className="flex-1">
|
|
2520
|
+
<FormLabel>${nfLabel}</FormLabel>
|
|
2521
|
+
<FormControl>
|
|
2522
|
+
<Input type="text" placeholder="${nfPlaceholder}" {...formField} />
|
|
2523
|
+
</FormControl>
|
|
2524
|
+
<FormMessage />
|
|
2525
|
+
</FormItem>
|
|
2526
|
+
)}
|
|
2527
|
+
/>`;
|
|
2528
|
+
}).join("\n");
|
|
2529
|
+
const defaultObj = field.fields.map((nf) => {
|
|
2530
|
+
if (nf.type === "select" || nf.type === "radio") return `${nf.name}: undefined`;
|
|
2531
|
+
if (nf.type === "checkbox") return `${nf.name}: false`;
|
|
2532
|
+
if (nf.type === "number") return `${nf.name}: undefined`;
|
|
2533
|
+
return `${nf.name}: ''`;
|
|
2534
|
+
}).join(", ");
|
|
2535
|
+
return ` <div className="space-y-4">
|
|
2536
|
+
<FormLabel>${label}${requiredStar}</FormLabel>${hintJSX}
|
|
2537
|
+
{${name}FieldArray.fields.map((item, index) => (
|
|
2538
|
+
<div key={item.id} className="flex items-end gap-2 rounded-lg border p-4">
|
|
2539
|
+
<div className="flex-1 space-y-4">
|
|
2540
|
+
${nestedFieldsJSX}
|
|
2541
|
+
</div>
|
|
2542
|
+
<Button type="button" variant="ghost" size="sm" onClick={() => ${name}FieldArray.remove(index)}>
|
|
2543
|
+
Remove
|
|
2544
|
+
</Button>
|
|
2545
|
+
</div>
|
|
2546
|
+
))}
|
|
2547
|
+
<Button
|
|
2548
|
+
type="button"
|
|
2549
|
+
variant="outline"
|
|
2550
|
+
size="sm"
|
|
2551
|
+
onClick={() => ${name}FieldArray.append({ ${defaultObj} })}
|
|
2552
|
+
>
|
|
2553
|
+
Add ${singularLabel}
|
|
2554
|
+
</Button>
|
|
2555
|
+
</div>`;
|
|
2556
|
+
}
|
|
2451
2557
|
|
|
2452
2558
|
// src/generators/form-pipeline/form-database.ts
|
|
2453
2559
|
import fs8 from "fs";
|