@betterstart/cli 0.1.70 → 0.1.72

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
@@ -84,7 +84,7 @@ function walkFields(fields, callback, options = DEFAULT_WALK_OPTIONS) {
84
84
  function walk(fieldsToWalk, depth, parent) {
85
85
  for (const field of fieldsToWalk) {
86
86
  callback(field, depth, parent);
87
- if (options.includeGroups && field.type === "group" && field.fields) {
87
+ if (options.includeGroups && (field.type === "group" || field.type === "section") && field.fields) {
88
88
  walk(field.fields, depth + 1, field);
89
89
  }
90
90
  if (options.includeLists && field.type === "list" && field.fields) {
@@ -140,7 +140,7 @@ function flattenFields(fields) {
140
140
  function flattenFieldsWithoutIdCheck(fields) {
141
141
  const flattened = [];
142
142
  for (const field of fields) {
143
- if (field.type === "group" && field.fields) {
143
+ if ((field.type === "group" || field.type === "section") && field.fields) {
144
144
  flattened.push(...flattenFieldsWithoutIdCheck(field.fields));
145
145
  } else if (field.type === "tabs" && field.tabs) {
146
146
  for (const tab of field.tabs) {
@@ -3353,12 +3353,12 @@ var FIELD_TYPES = {
3353
3353
  TABS: "tabs",
3354
3354
  LIST: "list",
3355
3355
  SEPARATOR: "separator",
3356
- BREAK: "break",
3356
+ SECTION: "section",
3357
3357
  SELECT: "select",
3358
3358
  RELATIONSHIP: "relationship",
3359
3359
  CURRICULUM: "curriculum"
3360
3360
  };
3361
- var LAYOUT_FIELD_TYPES = [FIELD_TYPES.SEPARATOR, FIELD_TYPES.BREAK];
3361
+ var LAYOUT_FIELD_TYPES = [FIELD_TYPES.SEPARATOR];
3362
3362
  var NESTED_FIELD_TYPES = [FIELD_TYPES.GROUP, FIELD_TYPES.TABS, FIELD_TYPES.LIST];
3363
3363
  var RICH_TEXT_FIELD_TYPES = [FIELD_TYPES.MARKDOWN, FIELD_TYPES.TEXT];
3364
3364
  var LONG_TEXT_FIELD_TYPES = [
@@ -6139,7 +6139,7 @@ function generateFieldJSXCore(field, indent = " ") {
6139
6139
  const fieldType = getFormFieldType(field);
6140
6140
  const label = field.label || field.name;
6141
6141
  const hintJSX = field.hint ? `${indent} <FormDescription>${field.hint}</FormDescription>` : "";
6142
- if (field.type === "group" && field.fields) return renderGroupField(field, indent, label);
6142
+ if ((field.type === "group" || field.type === "section") && field.fields) return renderGroupField(field, indent, label);
6143
6143
  if (field.type === "separator") return renderSeparatorField(field, indent);
6144
6144
  if (field.type === "boolean") return renderBooleanField(field, indent, label, hintJSX);
6145
6145
  if (field.type === "image") return renderImageField(field, indent, label, hintJSX);
@@ -6969,6 +6969,184 @@ ${hasDraft ? ` <Button
6969
6969
  // src/generators/form/form-single.ts
6970
6970
  import fs21 from "fs";
6971
6971
  import path21 from "path";
6972
+ function parseCardGroups(allFormFields, schemaLabel) {
6973
+ const sections = allFormFields.filter((f) => f.type === "section" && f.fields);
6974
+ const nonSections = allFormFields.filter((f) => f.type !== "section" && !isLayoutField(f.type));
6975
+ if (sections.length === 0) {
6976
+ const title = nonSections[0]?.label || nonSections[0]?.name || schemaLabel;
6977
+ return [
6978
+ {
6979
+ title,
6980
+ description: "",
6981
+ varPrefix: toCamelCase(nonSections[0]?.name || "form"),
6982
+ componentName: `${toPascalCase(nonSections[0]?.name || "form")}Card`,
6983
+ fields: nonSections,
6984
+ flatFields: flattenFields(nonSections).filter((f) => !isLayoutField(f.type))
6985
+ }
6986
+ ];
6987
+ }
6988
+ const groups = [];
6989
+ for (const section of sections) {
6990
+ const innerFields = section.fields || [];
6991
+ groups.push({
6992
+ title: section.label || section.name,
6993
+ description: section.description || section.hint || "",
6994
+ varPrefix: toCamelCase(section.name),
6995
+ componentName: `${toPascalCase(section.name)}Card`,
6996
+ fields: innerFields,
6997
+ flatFields: flattenFields(innerFields).filter((f) => !isLayoutField(f.type))
6998
+ });
6999
+ }
7000
+ if (nonSections.length > 0) {
7001
+ const title = nonSections[0]?.label || nonSections[0]?.name || "General";
7002
+ groups.unshift({
7003
+ title,
7004
+ description: "",
7005
+ varPrefix: toCamelCase(nonSections[0]?.name || "general"),
7006
+ componentName: `${toPascalCase(nonSections[0]?.name || "General")}Card`,
7007
+ fields: nonSections,
7008
+ flatFields: flattenFields(nonSections).filter((f) => !isLayoutField(f.type))
7009
+ });
7010
+ }
7011
+ return groups;
7012
+ }
7013
+ function analyzeGroup(fields) {
7014
+ const tabFieldNames = /* @__PURE__ */ new Set();
7015
+ for (const f of fields) {
7016
+ if (f.type === "tabs" && f.tabs) {
7017
+ for (const tab of f.tabs) {
7018
+ if (tab.fields) for (const tf of tab.fields) tabFieldNames.add(tf.name);
7019
+ }
7020
+ }
7021
+ }
7022
+ const relFields = collectRelationshipFields(fields, tabFieldNames);
7023
+ const listFieldsWithNested = [];
7024
+ function collectLists(flds) {
7025
+ for (const f of flds) {
7026
+ if (f.type === "list" && f.fields && f.fields.length > 0 && !f.hidden) {
7027
+ listFieldsWithNested.push(f);
7028
+ }
7029
+ if (f.type === "group" && f.fields) collectLists(f.fields);
7030
+ if (f.type === "tabs" && f.tabs) {
7031
+ for (const tab of f.tabs) {
7032
+ if (tab.fields) collectLists(tab.fields);
7033
+ }
7034
+ }
7035
+ }
7036
+ }
7037
+ collectLists(fields);
7038
+ const hasTabsField = fields.some((f) => f.type === "tabs");
7039
+ const tabsField = fields.find((f) => f.type === "tabs");
7040
+ const firstTabName = tabsField?.tabs?.[0]?.name || "";
7041
+ return { relFields, listFieldsWithNested, hasTabsField, firstTabName, tabFieldNames };
7042
+ }
7043
+ function buildGroupFieldsJSX(group3, analysis, skipLabel) {
7044
+ const indent = " ";
7045
+ return group3.fields.map((f) => {
7046
+ if (f.type === "tabs" && f.tabs) {
7047
+ const tabsList = f.tabs.map((t) => ` <TabsTrigger value="${t.name}">${t.label}</TabsTrigger>`).join("\n");
7048
+ const tabsContent = f.tabs.map((t) => {
7049
+ const tabFields = (t.fields || []).map((tf) => generateFieldJSX2(tf, " ")).join("\n");
7050
+ return ` <TabsContent value="${t.name}" className="space-y-6">
7051
+ ${tabFields}
7052
+ </TabsContent>`;
7053
+ }).join("\n");
7054
+ return `${indent}<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
7055
+ <TabsList>
7056
+ ${tabsList}
7057
+ </TabsList>
7058
+ ${tabsContent}
7059
+ ${indent}</Tabs>`;
7060
+ }
7061
+ if (analysis.tabFieldNames.has(f.name)) return "";
7062
+ return generateFieldJSX2(f, indent, { skipLabel });
7063
+ }).filter(Boolean).join("\n");
7064
+ }
7065
+ function buildGroupRelState(analysis) {
7066
+ return analysis.relFields.map((f) => {
7067
+ const relPlural = toPascalCase(pluralize(f.relationship || ""));
7068
+ return ` const [${f.name}Open, set${toPascalCase(f.name)}Open] = React.useState(false)
7069
+ const { data: ${f.relationship}Data } = use${relPlural}()`;
7070
+ }).filter((v, i, a) => a.indexOf(v) === i).join("\n");
7071
+ }
7072
+ function buildGroupFieldArrayHooks(analysis) {
7073
+ return analysis.listFieldsWithNested.map((field) => {
7074
+ const pascalFieldName = toPascalCase(field.name);
7075
+ return ` const [${field.name}Expanded, set${pascalFieldName}Expanded] = React.useState<string | undefined>(undefined)
7076
+ const ${field.name}FieldArray = useFieldArray({
7077
+ control: form.control,
7078
+ name: '${field.name}'
7079
+ })`;
7080
+ }).join("\n");
7081
+ }
7082
+ function generateCardComponent(group3, schema, analysis) {
7083
+ const singular = singularize(schema.name);
7084
+ const Singular = toPascalCase(singular);
7085
+ const zodFields = buildZodFields2(group3.flatFields);
7086
+ const defaultValues = buildDefaultValues2(group3.flatFields);
7087
+ const isSingleField = group3.fields.length === 1 && group3.title === (group3.fields[0].label || group3.fields[0].name);
7088
+ const fieldsJSX = buildGroupFieldsJSX(group3, analysis, isSingleField);
7089
+ const relState = buildGroupRelState(analysis);
7090
+ const fieldArrayHooks = buildGroupFieldArrayHooks(analysis);
7091
+ const descriptionLine = group3.description ? `
7092
+ <CardDescription>${group3.description}</CardDescription>` : "";
7093
+ return `const ${group3.varPrefix}Schema = z.object({
7094
+ ${zodFields}
7095
+ })
7096
+
7097
+ function ${group3.componentName}({ initialData }: { initialData?: ${Singular}Data | null }) {
7098
+ const queryClient = useQueryClient()${analysis.hasTabsField ? `
7099
+ const [activeTab, setActiveTab] = useQueryState('tab', { defaultValue: '${analysis.firstTabName}' })` : ""}${relState ? `
7100
+ ${relState}` : ""}
7101
+
7102
+ const mutation = useMutation({
7103
+ mutationFn: (data: Upsert${Singular}Input) => upsert${Singular}(data),
7104
+ onSuccess: () => {
7105
+ toast.success('${group3.title} saved')
7106
+ queryClient.invalidateQueries({ queryKey: ['${schema.name}'] })
7107
+ },
7108
+ onError: (error: Error) => {
7109
+ toast.error(error.message || 'Failed to save')
7110
+ }
7111
+ })
7112
+
7113
+ const isPending = mutation.isPending
7114
+
7115
+ const form = useForm<z.infer<typeof ${group3.varPrefix}Schema>>({
7116
+ resolver: zodResolver(${group3.varPrefix}Schema),
7117
+ defaultValues: {
7118
+ ${defaultValues}
7119
+ }
7120
+ })
7121
+ ${fieldArrayHooks ? `
7122
+ ${fieldArrayHooks}
7123
+ ` : ""}
7124
+ return (
7125
+ <Form {...form}>
7126
+ <form onSubmit={form.handleSubmit((values) => {
7127
+ const cleaned = Object.fromEntries(
7128
+ Object.entries(values).map(([key, value]) => [key, value === undefined ? '' : value])
7129
+ )
7130
+ mutation.mutate(cleaned as Upsert${Singular}Input)
7131
+ })}>
7132
+ <Card className="material-sm!">
7133
+ <CardHeader>
7134
+ <CardTitle>${group3.title}</CardTitle>${descriptionLine}
7135
+ </CardHeader>
7136
+ <CardContent className="space-y-6">
7137
+ ${fieldsJSX}
7138
+ </CardContent>
7139
+ <CardFooter>
7140
+ <Button type="submit" disabled={isPending} size="sm">
7141
+ {isPending ? 'Saving...' : 'Save'}
7142
+ </Button>
7143
+ </CardFooter>
7144
+ </Card>
7145
+ </form>
7146
+ </Form>
7147
+ )
7148
+ }`;
7149
+ }
6972
7150
  function generateSingleForm(schema, cwd, pagesDir, options = {}) {
6973
7151
  const entityDir = path21.join(cwd, pagesDir, schema.name);
6974
7152
  const formFilePath = path21.join(entityDir, `${schema.name}-form.tsx`);
@@ -6980,15 +7158,7 @@ function generateSingleForm(schema, cwd, pagesDir, options = {}) {
6980
7158
  const allFormFields = schema.fields.filter(
6981
7159
  (f) => !f.primaryKey && f.name !== "createdAt" && f.name !== "updatedAt"
6982
7160
  );
6983
- const flatFields = flattenFields(allFormFields);
6984
- const tabFieldNames = /* @__PURE__ */ new Set();
6985
- for (const f of schema.fields) {
6986
- if (f.type === "tabs" && f.tabs) {
6987
- for (const tab of f.tabs) {
6988
- if (tab.fields) for (const tf of tab.fields) tabFieldNames.add(tf.name);
6989
- }
6990
- }
6991
- }
7161
+ const cardGroups = parseCardGroups(allFormFields, schema.label);
6992
7162
  const hasBoolean = hasFieldType(schema.fields, "boolean");
6993
7163
  const hasImage = hasFieldType(schema.fields, "image");
6994
7164
  const hasVideo = hasFieldType(schema.fields, "video");
@@ -7003,48 +7173,20 @@ function generateSingleForm(schema, cwd, pagesDir, options = {}) {
7003
7173
  const hasSeparator = hasFieldType(schema.fields, "separator");
7004
7174
  const hasRelationship = hasFieldType(schema.fields, "relationship");
7005
7175
  const hasTabsField = schema.fields.some((f) => f.type === "tabs");
7006
- const tabsField = schema.fields.find((f) => f.type === "tabs");
7007
- const firstTabName = tabsField?.tabs?.[0]?.name || "";
7008
- const mainRelFields = collectRelationshipFields(schema.fields, tabFieldNames);
7009
- const listFieldsWithNested = [];
7010
- function collectListFieldsSingle(fields) {
7011
- for (const f of fields) {
7012
- if (f.type === "list" && f.fields && f.fields.length > 0 && !f.hidden) {
7013
- listFieldsWithNested.push(f);
7014
- }
7015
- if (f.type === "group" && f.fields) collectListFieldsSingle(f.fields);
7016
- if (f.type === "tabs" && f.tabs) {
7017
- for (const tab of f.tabs) {
7018
- if (tab.fields) collectListFieldsSingle(tab.fields);
7019
- }
7020
- }
7021
- }
7022
- }
7023
- collectListFieldsSingle(allFormFields);
7176
+ const flatFields = flattenFields(allFormFields);
7024
7177
  const hasList = hasFieldType(schema.fields, "list");
7025
- const hasNestedList = listFieldsWithNested.length > 0;
7178
+ const hasNestedList = cardGroups.some((g) => analyzeGroup(g.fields).listFieldsWithNested.length > 0);
7026
7179
  const hasSimpleList = hasList && flatFields.some((f) => f.type === "list" && (!f.fields || f.fields.length === 0));
7027
- const zodFields = buildZodFields2(flatFields);
7028
- const defaultValues = buildDefaultValues2(flatFields);
7029
- const formFieldsJSX = allFormFields.map((f) => {
7030
- if (f.type === "tabs" && f.tabs) {
7031
- const tabsList = f.tabs.map((t) => ` <TabsTrigger value="${t.name}">${t.label}</TabsTrigger>`).join("\n");
7032
- const tabsContent = f.tabs.map((t) => {
7033
- const tabFields = (t.fields || []).map((tf) => generateFieldJSX2(tf, " ")).join("\n");
7034
- return ` <TabsContent value="${t.name}" className="space-y-6 p-6 rounded-2xl border bg-card">
7035
- ${tabFields}
7036
- </TabsContent>`;
7037
- }).join("\n");
7038
- return ` <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
7039
- <TabsList>
7040
- ${tabsList}
7041
- </TabsList>
7042
- ${tabsContent}
7043
- </Tabs>`;
7180
+ const allRelFields = [];
7181
+ for (const g of cardGroups) {
7182
+ const a = analyzeGroup(g.fields);
7183
+ for (const rf of a.relFields) {
7184
+ if (!allRelFields.some((e) => e.relationship === rf.relationship)) {
7185
+ allRelFields.push(rf);
7186
+ }
7044
7187
  }
7045
- if (tabFieldNames.has(f.name)) return "";
7046
- return generateFieldJSX2(f);
7047
- }).filter(Boolean).join("\n");
7188
+ }
7189
+ const needsReact = allRelFields.length > 0 || hasNestedList;
7048
7190
  const uiImports = buildUiImports({
7049
7191
  hasBoolean,
7050
7192
  hasTextarea,
@@ -7063,30 +7205,29 @@ ${tabsContent}
7063
7205
  hasSimpleList,
7064
7206
  hasNestedList
7065
7207
  });
7208
+ uiImports.push(`import {
7209
+ Card,
7210
+ CardContent,
7211
+ CardDescription,
7212
+ CardFooter,
7213
+ CardHeader,
7214
+ CardTitle
7215
+ } from '@cms/components/ui/card'`);
7066
7216
  const lucideIcons = [];
7067
7217
  if (hasRelationship) lucideIcons.push("Check", "ChevronsUpDown");
7068
7218
  if (hasNestedList) {
7069
7219
  if (!lucideIcons.includes("Plus")) lucideIcons.push("Plus");
7070
7220
  if (!lucideIcons.includes("X")) lucideIcons.push("X");
7071
7221
  }
7072
- const relHookImports = mainRelFields.map((f) => {
7222
+ const relHookImports = allRelFields.map((f) => {
7073
7223
  const relPlural = toPascalCase(pluralize(f.relationship || ""));
7074
7224
  return `import { use${relPlural} } from '@cms/hooks/use-${f.relationship}'`;
7075
7225
  }).filter((v, i, a) => a.indexOf(v) === i).join("\n");
7076
- const relState = mainRelFields.map((f) => {
7077
- const relPlural = toPascalCase(pluralize(f.relationship || ""));
7078
- return ` const [${f.name}Open, set${toPascalCase(f.name)}Open] = React.useState(false)
7079
- const { data: ${f.relationship}Data } = use${relPlural}()`;
7080
- }).filter((v, i, a) => a.indexOf(v) === i).join("\n");
7081
- const fieldArrayHooks = listFieldsWithNested.map((field) => {
7082
- const pascalFieldName = toPascalCase(field.name);
7083
- return ` const [${field.name}Expanded, set${pascalFieldName}Expanded] = React.useState<string | undefined>(undefined)
7084
- const ${field.name}FieldArray = useFieldArray({
7085
- control: form.control,
7086
- name: '${field.name}'
7087
- })`;
7088
- }).join("\n");
7089
- const needsReact = mainRelFields.length > 0 || hasNestedList;
7226
+ const cardComponents = cardGroups.map((group3) => {
7227
+ const analysis = analyzeGroup(group3.fields);
7228
+ return generateCardComponent(group3, schema, analysis);
7229
+ });
7230
+ const cardRenders = cardGroups.map((g) => ` <${g.componentName} initialData={initialData} />`).join("\n");
7090
7231
  const content = `'use client'
7091
7232
  ${needsReact ? "\nimport * as React from 'react'" : ""}
7092
7233
  import { zodResolver } from '@hookform/resolvers/zod'
@@ -7103,75 +7244,17 @@ import type {
7103
7244
  } from '@cms/actions/${schema.name}'
7104
7245
  import { upsert${Singular} } from '@cms/actions/${schema.name}'
7105
7246
 
7106
- const formSchema = z.object({
7107
- ${zodFields}
7108
- })
7109
-
7110
- export type FormValues = z.infer<typeof formSchema>
7247
+ ${cardComponents.join("\n\n")}
7111
7248
 
7112
7249
  interface ${Singular}FormProps {
7113
7250
  initialData?: ${Singular}Data | null
7114
7251
  }
7115
7252
 
7116
7253
  export function ${Singular}Form({ initialData }: ${Singular}FormProps) {
7117
- const queryClient = useQueryClient()${hasTabsField ? `
7118
- const [activeTab, setActiveTab] = useQueryState('tab', { defaultValue: '${firstTabName}' })` : ""}${relState ? `
7119
- ${relState}` : ""}
7120
-
7121
- const upsertMutation = useMutation({
7122
- mutationFn: (data: Upsert${Singular}Input) => upsert${Singular}(data),
7123
- onSuccess: () => {
7124
- toast.success('${schema.label} saved successfully')
7125
- queryClient.invalidateQueries({ queryKey: ['${schema.name}'] })
7126
- },
7127
- onError: (error: Error) => {
7128
- toast.error(error.message || 'Failed to save ${schema.label.toLowerCase()}')
7129
- }
7130
- })
7131
-
7132
- const isPending = upsertMutation.isPending
7133
-
7134
- const form = useForm<FormValues>({
7135
- resolver: zodResolver(formSchema),
7136
- defaultValues: {
7137
- ${defaultValues}
7138
- }
7139
- })
7140
- ${fieldArrayHooks ? `
7141
- ${fieldArrayHooks}
7142
- ` : ""}
7143
- function onSubmit(values: FormValues) {
7144
- const cleanedValues = Object.fromEntries(
7145
- Object.entries(values).map(([key, value]) => [key, value === undefined ? '' : value])
7146
- ) as FormValues
7147
-
7148
- upsertMutation.mutate(cleanedValues as Upsert${Singular}Input)
7149
- }
7150
-
7151
7254
  return (
7152
- <Form {...form}>
7153
- <form id="${schema.name}-form" onSubmit={form.handleSubmit(onSubmit, (errors) => {
7154
- console.error('Form validation errors:', errors)
7155
- const firstError = Object.values(errors)[0]
7156
- if (firstError?.message) {
7157
- toast.error(String(firstError.message))
7158
- } else {
7159
- toast.error('Please fix the form errors before submitting')
7160
- }
7161
- })} className="space-y-6">
7162
- <div className="space-y-6 p-6 rounded-2xl border bg-card">
7163
- ${formFieldsJSX}
7164
- </div>
7165
-
7166
- <div className="flex items-center fixed bottom-0 md:left-[calc(var(--sidebar-width))] w-screen md:w-[calc(100svw-var(--sidebar-width)-4px)] right-0 bg-secondary border-t">
7167
- <div className="flex mx-auto py-4 w-full max-w-5xl items-center justify-end gap-2">
7168
- <Button type="submit" disabled={isPending} size="lg">
7169
- {isPending ? 'Saving...' : 'Save'}
7170
- </Button>
7171
- </div>
7172
- </div>
7173
- </form>
7174
- </Form>
7255
+ <div className="space-y-6">
7256
+ ${cardRenders}
7257
+ </div>
7175
7258
  )
7176
7259
  }
7177
7260
  `;
@@ -9998,14 +10081,28 @@ function defaultSettingsSchema() {
9998
10081
  name: "settings",
9999
10082
  type: "single",
10000
10083
  label: "Settings",
10001
- description: "General site settings",
10084
+ description: "General Settings",
10002
10085
  icon: "Settings",
10003
10086
  fields: [
10004
- { name: "siteName", type: "string", label: "Site Name", default: "BetterStart" },
10005
- { name: "tagline", type: "string", label: "Tagline" },
10006
- { name: "separator1", type: "separator" },
10007
- { name: "logo", type: "image", label: "Logo" },
10008
- { name: "favicon", type: "image", label: "Favicon" }
10087
+ {
10088
+ type: "section",
10089
+ name: "siteSettings",
10090
+ label: "Site Settings",
10091
+ description: "General settings for the site",
10092
+ fields: [
10093
+ { name: "appName", type: "string", label: "App Name", hint: "Displayed in the sidebar and throughout the dashboard", default: "BetterStart" },
10094
+ { name: "appDescription", type: "text", label: "App Description", hint: "A brief description of the application" }
10095
+ ]
10096
+ },
10097
+ {
10098
+ type: "section",
10099
+ name: "branding",
10100
+ label: "Branding",
10101
+ description: "Logo and visual identity",
10102
+ fields: [
10103
+ { name: "logo", type: "image", label: "Logo" }
10104
+ ]
10105
+ }
10009
10106
  ]
10010
10107
  },
10011
10108
  null,