@betterstart/cli 0.1.35 → 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 +132 -23
- 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);
|
|
@@ -448,7 +448,8 @@ function generateEmailTemplate(schema, cwd, cmsDir, options = {}) {
|
|
|
448
448
|
const pascal = toPascalCase(formName);
|
|
449
449
|
const fields = getAllFormSchemaFields(schema).filter((f) => f.name);
|
|
450
450
|
const includeDynamic = hasDynamicFields(schema);
|
|
451
|
-
const
|
|
451
|
+
const kebab = toKebabCase(formName);
|
|
452
|
+
const filePath = path3.join(cwd, cmsDir, "lib", "emails", `${kebab}-submission.tsx`);
|
|
452
453
|
const dir = path3.dirname(filePath);
|
|
453
454
|
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
454
455
|
if (fs3.existsSync(filePath) && !options.force) {
|
|
@@ -815,7 +816,7 @@ function DeleteAction({ id }: { id: number }) {
|
|
|
815
816
|
<AlertDialogFooter>
|
|
816
817
|
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
817
818
|
<AlertDialogAction
|
|
818
|
-
onClick={(e) => { e.preventDefault(); handleDelete() }}
|
|
819
|
+
onClick={(e: React.MouseEvent) => { e.preventDefault(); handleDelete() }}
|
|
819
820
|
disabled={isPending}
|
|
820
821
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
821
822
|
>
|
|
@@ -833,14 +834,14 @@ export const columns: ColumnDef<${pascal}SubmissionData>[] = [
|
|
|
833
834
|
header: ({ table }) => (
|
|
834
835
|
<Checkbox
|
|
835
836
|
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
|
|
836
|
-
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
837
|
+
onCheckedChange={(value: boolean) => table.toggleAllPageRowsSelected(!!value)}
|
|
837
838
|
aria-label="Select all"
|
|
838
839
|
/>
|
|
839
840
|
),
|
|
840
841
|
cell: ({ row }) => (
|
|
841
842
|
<Checkbox
|
|
842
843
|
checked={row.getIsSelected()}
|
|
843
|
-
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
844
|
+
onCheckedChange={(value: boolean) => row.toggleSelected(!!value)}
|
|
844
845
|
aria-label="Select row"
|
|
845
846
|
/>
|
|
846
847
|
),
|
|
@@ -989,7 +990,7 @@ export function ${pascal}SubmissionsPageContent<TValue>({
|
|
|
989
990
|
<AlertDialogFooter>
|
|
990
991
|
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
991
992
|
<AlertDialogAction
|
|
992
|
-
onClick={(e) => { e.preventDefault(); handleBulkDelete() }}
|
|
993
|
+
onClick={(e: React.MouseEvent) => { e.preventDefault(); handleBulkDelete() }}
|
|
993
994
|
disabled={isPending}
|
|
994
995
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
995
996
|
>
|
|
@@ -1424,6 +1425,20 @@ function generateViewPage(pascal, kebab, fields, label, includeDynamic) {
|
|
|
1424
1425
|
</p>
|
|
1425
1426
|
</div>`;
|
|
1426
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
|
+
}
|
|
1427
1442
|
return ` <div className="space-y-1">
|
|
1428
1443
|
<p className="text-sm font-medium text-muted-foreground">${lbl}</p>
|
|
1429
1444
|
<p className="text-sm">{submission.${name} ?? '-'}</p>
|
|
@@ -1727,7 +1742,8 @@ function generateFormActions(schema, cwd, actionsDir, options) {
|
|
|
1727
1742
|
return { success: false, error: '${f.label} is required' }
|
|
1728
1743
|
}`
|
|
1729
1744
|
).join("\n ");
|
|
1730
|
-
const
|
|
1745
|
+
const kebab = toKebabCase(formName);
|
|
1746
|
+
const filePath = path6.join(cwd, actionsDir, `${kebab}-form.ts`);
|
|
1731
1747
|
const dir = path6.dirname(filePath);
|
|
1732
1748
|
if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
|
|
1733
1749
|
if (fs6.existsSync(filePath) && !options.force) {
|
|
@@ -1821,7 +1837,7 @@ export async function create${pascal}Submission(
|
|
|
1821
1837
|
// Resolve notification emails from form settings or env var
|
|
1822
1838
|
const settings = await getFormSettings('${formName}')
|
|
1823
1839
|
const notificationEmails = settings?.notificationEmails
|
|
1824
|
-
? settings.notificationEmails.split(',').map((e) => e.trim()).filter(Boolean)
|
|
1840
|
+
? settings.notificationEmails.split(',').map((e: string) => e.trim()).filter(Boolean)
|
|
1825
1841
|
: process.env.NOTIFICATION_EMAIL
|
|
1826
1842
|
? [process.env.NOTIFICATION_EMAIL]
|
|
1827
1843
|
: []
|
|
@@ -1889,7 +1905,7 @@ export async function export${pascal}SubmissionsCSV(): Promise<string> {
|
|
|
1889
1905
|
const headers = Object.keys(submissions[0]).join(',')
|
|
1890
1906
|
const rows = submissions.map((sub) =>
|
|
1891
1907
|
Object.values(sub)
|
|
1892
|
-
.map((val) => {
|
|
1908
|
+
.map((val: unknown) => {
|
|
1893
1909
|
if (val === null || val === undefined) return ''
|
|
1894
1910
|
const str = String(val)
|
|
1895
1911
|
if (str.includes(',') || str.includes('"') || str.includes('\\n')) {
|
|
@@ -2128,7 +2144,7 @@ function formFieldToZodType(field, options = {}) {
|
|
|
2128
2144
|
}
|
|
2129
2145
|
return isRequired ? `z.string().min(1, '${label} is required')` : "z.string().optional()";
|
|
2130
2146
|
case "checkbox":
|
|
2131
|
-
return isRequired ? `z.
|
|
2147
|
+
return isRequired ? `z.boolean().refine(val => val === true, { message: '${label} is required' })` : "z.boolean().optional()";
|
|
2132
2148
|
case "multiselect":
|
|
2133
2149
|
return isRequired ? `z.array(z.string()).min(1, '${label} is required')` : "z.array(z.string()).optional()";
|
|
2134
2150
|
case "timezone":
|
|
@@ -2210,6 +2226,9 @@ function toTypeScriptType(field, mode = "output") {
|
|
|
2210
2226
|
}
|
|
2211
2227
|
|
|
2212
2228
|
// src/generators/form-pipeline/form-component.ts
|
|
2229
|
+
function escapeJsx(str) {
|
|
2230
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2231
|
+
}
|
|
2213
2232
|
function generateFormComponent(schema, cwd, cmsDir, options) {
|
|
2214
2233
|
const formName = schema.name;
|
|
2215
2234
|
const pascal = toPascalCase(formName);
|
|
@@ -2226,22 +2245,31 @@ function generateFormComponent(schema, cwd, cmsDir, options) {
|
|
|
2226
2245
|
if (f.type === "checkbox") return ` ${f.name}: false`;
|
|
2227
2246
|
if (f.type === "number") return ` ${f.name}: undefined`;
|
|
2228
2247
|
if (f.type === "multiselect" || f.type === "list") return ` ${f.name}: []`;
|
|
2248
|
+
if (f.type === "select" || f.type === "radio") return ` ${f.name}: undefined`;
|
|
2229
2249
|
if (f.defaultValue !== void 0) return ` ${f.name}: '${f.defaultValue}'`;
|
|
2230
2250
|
return ` ${f.name}: ''`;
|
|
2231
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");
|
|
2232
2259
|
const fieldJSX = fields.filter((f) => f.name && !f.hidden).map((f) => generateFieldJSX(f)).join("\n\n");
|
|
2233
2260
|
const submitText = schema.submitButtonText || "Submit";
|
|
2234
|
-
const successMessage = (schema.successMessage || "Form submitted successfully!")
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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
|
+
` : "";
|
|
2238
2266
|
const content = `'use client'
|
|
2239
2267
|
|
|
2240
2268
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2241
2269
|
import { useState } from 'react'
|
|
2242
|
-
|
|
2270
|
+
${rhfImport}
|
|
2243
2271
|
import { z } from 'zod/v3'
|
|
2244
|
-
import { create${pascal}Submission } from '@cms/actions/${
|
|
2272
|
+
import { create${pascal}Submission } from '@cms/actions/${kebab}-form'
|
|
2245
2273
|
import { Button } from '@cms/components/ui/button'
|
|
2246
2274
|
import {
|
|
2247
2275
|
Form,
|
|
@@ -2278,7 +2306,7 @@ export function ${pascal}Form() {
|
|
|
2278
2306
|
${defaults}
|
|
2279
2307
|
},
|
|
2280
2308
|
})
|
|
2281
|
-
|
|
2309
|
+
${fieldArraySetup}
|
|
2282
2310
|
async function onSubmit(values: FormValues) {
|
|
2283
2311
|
setSubmitting(true)
|
|
2284
2312
|
try {
|
|
@@ -2326,11 +2354,11 @@ ${fieldJSX}
|
|
|
2326
2354
|
}
|
|
2327
2355
|
function generateFieldJSX(field) {
|
|
2328
2356
|
const name = field.name || "";
|
|
2329
|
-
const label = field.label;
|
|
2357
|
+
const label = escapeJsx(field.label);
|
|
2330
2358
|
const placeholder = field.placeholder || "";
|
|
2331
2359
|
const hint = field.hint || "";
|
|
2332
2360
|
const hintJSX = hint ? `
|
|
2333
|
-
<FormDescription>${hint}</FormDescription>` : "";
|
|
2361
|
+
<FormDescription>${escapeJsx(hint)}</FormDescription>` : "";
|
|
2334
2362
|
const requiredStar = field.required ? ' <span className="text-destructive">*</span>' : "";
|
|
2335
2363
|
switch (field.type) {
|
|
2336
2364
|
case "textarea":
|
|
@@ -2351,7 +2379,7 @@ function generateFieldJSX(field) {
|
|
|
2351
2379
|
case "radio":
|
|
2352
2380
|
if (field.options && field.options.length > 0) {
|
|
2353
2381
|
const optionItems = field.options.map(
|
|
2354
|
-
(opt) => ` <SelectItem value="${opt.value}">${opt.label}</SelectItem>`
|
|
2382
|
+
(opt) => ` <SelectItem value="${opt.value}">${escapeJsx(opt.label)}</SelectItem>`
|
|
2355
2383
|
).join("\n");
|
|
2356
2384
|
return ` <FormField
|
|
2357
2385
|
control={form.control}
|
|
@@ -2427,6 +2455,11 @@ ${optionItems}
|
|
|
2427
2455
|
requiredStar,
|
|
2428
2456
|
"tel"
|
|
2429
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);
|
|
2430
2463
|
default:
|
|
2431
2464
|
return generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, "text");
|
|
2432
2465
|
}
|
|
@@ -2446,6 +2479,81 @@ function generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, i
|
|
|
2446
2479
|
)}
|
|
2447
2480
|
/>`;
|
|
2448
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
|
+
}
|
|
2449
2557
|
|
|
2450
2558
|
// src/generators/form-pipeline/form-database.ts
|
|
2451
2559
|
import fs8 from "fs";
|
|
@@ -2514,7 +2622,8 @@ function generateFormHook(schema, cwd, hooksDir, options) {
|
|
|
2514
2622
|
const formName = schema.name;
|
|
2515
2623
|
const pascal = toPascalCase(formName);
|
|
2516
2624
|
const camel = toCamelCase(formName);
|
|
2517
|
-
const
|
|
2625
|
+
const kebab = toKebabCase(formName);
|
|
2626
|
+
const filePath = path9.join(cwd, hooksDir, `use-${kebab}-form.ts`);
|
|
2518
2627
|
const dir = path9.dirname(filePath);
|
|
2519
2628
|
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
2520
2629
|
if (fs9.existsSync(filePath) && !options.force) {
|
|
@@ -2528,8 +2637,8 @@ function generateFormHook(schema, cwd, hooksDir, options) {
|
|
|
2528
2637
|
export${pascal}SubmissionsJSON,
|
|
2529
2638
|
get${pascal}Submission,
|
|
2530
2639
|
get${pascal}Submissions,
|
|
2531
|
-
} from '@cms/actions/${
|
|
2532
|
-
import type { Create${pascal}SubmissionInput } from '@cms/actions/${
|
|
2640
|
+
} from '@cms/actions/${kebab}-form'
|
|
2641
|
+
import type { Create${pascal}SubmissionInput } from '@cms/actions/${kebab}-form'
|
|
2533
2642
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2534
2643
|
import { toast } from 'sonner'
|
|
2535
2644
|
|