@betterstart/cli 0.1.36 → 0.1.38

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 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.literal(true, { message: '${label} is required' })` : "z.boolean().optional()";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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!").replace(
2237
- /'/g,
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
- import { useForm } from 'react-hook-form'
2270
+ ${rhfImport}
2245
2271
  import { z } from 'zod/v3'
2246
- import { create${pascal}Submission } from '@cms/actions/${formName}-form'
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,13 @@ ${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>` : "";
2362
+ const hintPlainJSX = hint ? `
2363
+ <p className="text-sm text-muted-foreground">${escapeJsx(hint)}</p>` : "";
2336
2364
  const requiredStar = field.required ? ' <span className="text-destructive">*</span>' : "";
2337
2365
  switch (field.type) {
2338
2366
  case "textarea":
@@ -2353,7 +2381,7 @@ function generateFieldJSX(field) {
2353
2381
  case "radio":
2354
2382
  if (field.options && field.options.length > 0) {
2355
2383
  const optionItems = field.options.map(
2356
- (opt) => ` <SelectItem value="${opt.value}">${opt.label}</SelectItem>`
2384
+ (opt) => ` <SelectItem value="${opt.value}">${escapeJsx(opt.label)}</SelectItem>`
2357
2385
  ).join("\n");
2358
2386
  return ` <FormField
2359
2387
  control={form.control}
@@ -2429,6 +2457,11 @@ ${optionItems}
2429
2457
  requiredStar,
2430
2458
  "tel"
2431
2459
  );
2460
+ case "file":
2461
+ case "upload":
2462
+ return generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, "file");
2463
+ case "list":
2464
+ return generateListFieldJSX(field, name, label, hintPlainJSX, requiredStar);
2432
2465
  default:
2433
2466
  return generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, "text");
2434
2467
  }
@@ -2448,6 +2481,81 @@ function generateTextFieldJSX(name, label, placeholder, hintJSX, requiredStar, i
2448
2481
  )}
2449
2482
  />`;
2450
2483
  }
2484
+ function generateListFieldJSX(field, name, label, hintJSX, requiredStar) {
2485
+ if (!field.fields || field.fields.length === 0) {
2486
+ return generateTextFieldJSX(name, label, field.placeholder || "", hintJSX, requiredStar, "text");
2487
+ }
2488
+ const singularLabel = singularize(label);
2489
+ const nestedFieldsJSX = field.fields.map((nf) => {
2490
+ const nfLabel = escapeJsx(nf.label || nf.name || "");
2491
+ const nfPlaceholder = nf.placeholder || "";
2492
+ if (nf.type === "select" && nf.options && nf.options.length > 0) {
2493
+ const selectItems = nf.options.map(
2494
+ (opt) => ` <SelectItem value="${opt.value}">${escapeJsx(opt.label)}</SelectItem>`
2495
+ ).join("\n");
2496
+ return ` <FormField
2497
+ control={form.control}
2498
+ name={\`${name}.\${index}.${nf.name}\`}
2499
+ render={({ field: formField }) => (
2500
+ <FormItem className="flex-1">
2501
+ <FormLabel>${nfLabel}</FormLabel>
2502
+ <Select onValueChange={formField.onChange} defaultValue={formField.value}>
2503
+ <FormControl>
2504
+ <SelectTrigger>
2505
+ <SelectValue placeholder="${nf.placeholder || "Select..."}" />
2506
+ </SelectTrigger>
2507
+ </FormControl>
2508
+ <SelectContent>
2509
+ ${selectItems}
2510
+ </SelectContent>
2511
+ </Select>
2512
+ <FormMessage />
2513
+ </FormItem>
2514
+ )}
2515
+ />`;
2516
+ }
2517
+ return ` <FormField
2518
+ control={form.control}
2519
+ name={\`${name}.\${index}.${nf.name}\`}
2520
+ render={({ field: formField }) => (
2521
+ <FormItem className="flex-1">
2522
+ <FormLabel>${nfLabel}</FormLabel>
2523
+ <FormControl>
2524
+ <Input type="text" placeholder="${nfPlaceholder}" {...formField} />
2525
+ </FormControl>
2526
+ <FormMessage />
2527
+ </FormItem>
2528
+ )}
2529
+ />`;
2530
+ }).join("\n");
2531
+ const defaultObj = field.fields.map((nf) => {
2532
+ if (nf.type === "select" || nf.type === "radio") return `${nf.name}: undefined`;
2533
+ if (nf.type === "checkbox") return `${nf.name}: false`;
2534
+ if (nf.type === "number") return `${nf.name}: undefined`;
2535
+ return `${nf.name}: ''`;
2536
+ }).join(", ");
2537
+ return ` <div className="space-y-4">
2538
+ <label className="text-sm font-medium leading-none">${label}${requiredStar}</label>${hintJSX}
2539
+ {${name}FieldArray.fields.map((item, index) => (
2540
+ <div key={item.id} className="flex items-end gap-2 rounded-lg border p-4">
2541
+ <div className="flex-1 space-y-4">
2542
+ ${nestedFieldsJSX}
2543
+ </div>
2544
+ <Button type="button" variant="ghost" size="sm" onClick={() => ${name}FieldArray.remove(index)}>
2545
+ Remove
2546
+ </Button>
2547
+ </div>
2548
+ ))}
2549
+ <Button
2550
+ type="button"
2551
+ variant="outline"
2552
+ size="sm"
2553
+ onClick={() => ${name}FieldArray.append({ ${defaultObj} })}
2554
+ >
2555
+ Add ${singularLabel}
2556
+ </Button>
2557
+ </div>`;
2558
+ }
2451
2559
 
2452
2560
  // src/generators/form-pipeline/form-database.ts
2453
2561
  import fs8 from "fs";