@flaredev/cli 0.4.1 → 0.5.1

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.
Files changed (48) hide show
  1. package/dist/index.js +698 -341
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/costs/page.tsx +93 -0
  4. package/templates/app/app/dashboard/page.tsx +4 -7
  5. package/templates/app/app/page.tsx +10 -0
  6. package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
  7. package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
  8. package/templates/app/components/dashboard/record-actions.tsx +7 -7
  9. package/templates/app/components/dashboard/record-detail.tsx +1 -1
  10. package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
  11. package/templates/app/components/dashboard/resource-form.tsx +129 -33
  12. package/templates/app/components/dashboard/resource-icon.tsx +87 -85
  13. package/templates/app/components/dashboard/resource-stats.tsx +10 -9
  14. package/templates/app/components/dashboard/resource-table.tsx +4 -4
  15. package/templates/app/components/dashboard/row-actions.tsx +3 -3
  16. package/templates/app/lib/costs.ts +138 -0
  17. package/templates/app/lib/dashboard-nav.ts +5 -1
  18. package/templates/app/lib/dashboard.ts +66 -61
  19. package/templates/app/lib/number.ts +33 -0
  20. package/templates/app/lib/site.ts +56 -56
  21. package/templates/app/lib/suggest.ts +35 -0
  22. package/templates/app/lib/usage.ts +32 -0
  23. package/templates/next/.env.example +21 -0
  24. package/templates/next/README.md +16 -0
  25. package/templates/next/app/api/health/route.ts +7 -0
  26. package/templates/next/app/dashboard/account/password/page.tsx +19 -0
  27. package/templates/next/app/dashboard/account/security/page.tsx +31 -0
  28. package/templates/next/app/dashboard/actions.ts +277 -0
  29. package/templates/next/app/dashboard/layout.tsx +64 -0
  30. package/templates/next/app/dashboard/page.tsx +127 -0
  31. package/templates/next/components/dashboard/resource-chart.tsx +86 -0
  32. package/templates/next/lib/analytics.ts +71 -0
  33. package/templates/next/lib/audit.ts +80 -0
  34. package/templates/next/lib/auth-emails.ts +92 -0
  35. package/templates/next/lib/auth.ts +156 -0
  36. package/templates/next/lib/dashboard.ts +161 -0
  37. package/templates/next/lib/db.ts +24 -0
  38. package/templates/next/lib/mail.ts +35 -0
  39. package/templates/next/lib/realtime.ts +23 -0
  40. package/templates/next/lib/redis.ts +17 -0
  41. package/templates/next/lib/storage.ts +57 -0
  42. package/templates/next/lib/theme.ts +83 -0
  43. package/templates/next/lib/views.ts +23 -0
  44. package/templates/next/next.config.ts +13 -0
  45. package/templates/next/prisma/schema/base.prisma +162 -0
  46. package/templates/next/prisma.config.ts +15 -0
  47. package/templates/next/tsconfig.json +24 -0
  48. package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
@@ -2,8 +2,8 @@
2
2
 
3
3
  import Link from "next/link";
4
4
  import { useRouter } from "next/navigation";
5
- import { useMemo, useState, useTransition, type FormEvent } from "react";
6
- import { createValidators, formValuesToInput, initialFormValues, issuesByField, storedFields, type ClientResource } from "@flaredev/core";
5
+ import { useMemo, useState, useTransition, type FormEvent, type MouseEvent } from "react";
6
+ import { createValidators, formValuesToInput, initialFormValues, issuesByField, storedFields, type ClientResource, type StoredField } from "@flaredev/core";
7
7
  import { toast } from "sonner";
8
8
  import { createRecordAction, updateRecordAction } from "@/app/dashboard/actions";
9
9
  import { Alert, AlertDescription } from "@/components/ui/alert";
@@ -11,6 +11,8 @@ import { Button } from "@/components/ui/button";
11
11
  import { Card, CardContent, CardFooter } from "@/components/ui/card";
12
12
  import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
13
13
  import { Spinner } from "@/components/ui/spinner";
14
+ import { canSuggest, suggestValue } from "@/lib/suggest";
15
+ import { cn } from "@/lib/utils";
14
16
  import { FieldWidget, type RelationMeta } from "./fields/field-widget";
15
17
 
16
18
  export interface ResourceFormProps {
@@ -23,12 +25,33 @@ export interface ResourceFormProps {
23
25
  relations?: Record<string, RelationMeta & { initialTitle?: string }>;
24
26
  /** Where Cancel and a successful save go, when the form owns the page. */
25
27
  listHref: string;
26
- /** In a dialog: close it instead of navigating, and drop the card around the fields. */
28
+ /** In a sheet: close it instead of navigating, and drop the card around the fields. */
27
29
  onDone?: () => void;
28
30
  }
29
31
 
30
32
  const WIDE = new Set(["text", "file"]);
31
33
 
34
+ /**
35
+ * More fields than this and the form is split into steps.
36
+ *
37
+ * Six inputs in one column is where a form starts to read as a chore rather than a
38
+ * question, and a step at a time also means the first error appears before everything
39
+ * else has been filled in.
40
+ */
41
+ const FIELDS_PER_STEP = 5;
42
+
43
+ type Entry = [string, StoredField & { label: string }];
44
+
45
+ /** Fields in declaration order, cut into steps of at most {@link FIELDS_PER_STEP}. */
46
+ function toSteps(fields: Entry[]): Entry[][] {
47
+ if (fields.length <= FIELDS_PER_STEP) return [fields];
48
+ const steps: Entry[][] = [];
49
+ for (let index = 0; index < fields.length; index += FIELDS_PER_STEP) {
50
+ steps.push(fields.slice(index, index + FIELDS_PER_STEP));
51
+ }
52
+ return steps;
53
+ }
54
+
32
55
  /**
33
56
  * Create/edit form rendered from field metadata. Values are validated in the browser
34
57
  * with the descriptor's zod schemas, then again on the server by the same store the
@@ -38,28 +61,63 @@ export function ResourceForm({ resource, mode, id, record, relations = {}, listH
38
61
  const router = useRouter();
39
62
  const validators = useMemo(() => createValidators(resource), [resource]);
40
63
  const fields = useMemo(() => storedFields(resource), [resource]);
64
+ const steps = useMemo(() => toSteps(fields), [fields]);
65
+ const [step, setStep] = useState(0);
41
66
  const [values, setValues] = useState(() => initialFormValues(resource, record));
42
67
  const [errors, setErrors] = useState<Record<string, string>>({});
43
68
  const [pending, startTransition] = useTransition();
44
69
 
70
+ const last = step === steps.length - 1;
71
+ const visible = steps[step] ?? [];
72
+
45
73
  const setValue = (key: string, value: string | boolean) => {
46
74
  setValues((current) => ({ ...current, [key]: value }));
47
75
  if (errors[key]) setErrors(({ [key]: _removed, ...rest }) => rest);
48
76
  };
49
77
 
50
- function focusFirstError(fieldErrors: Record<string, string>) {
51
- const first = fields.find(([key]) => fieldErrors[key]);
78
+ function focusFirstError(fieldErrors: Record<string, string>, within: Entry[] = fields) {
79
+ const first = within.find(([key]) => fieldErrors[key]);
52
80
  if (first) document.getElementById(`field-${first[0]}`)?.focus();
53
81
  }
54
82
 
83
+ /** Everything the descriptor objects to, whichever step it is on. */
84
+ function validate(): Record<string, string> {
85
+ const input = formValuesToInput(resource, values, mode);
86
+ const parsed = (mode === "create" ? validators.create : validators.update).safeParse(input);
87
+ return parsed.success ? {} : issuesByField(parsed.error.issues);
88
+ }
89
+
90
+ /**
91
+ * Move on only when this step is right; problems further on can wait their turn.
92
+ *
93
+ * The click is cancelled first. This button and the submit button on the last step are
94
+ * the same node to the browser, and React has already turned this one into the submit
95
+ * button by the time the click's default action runs — without this, going to the last
96
+ * step also saves the record.
97
+ */
98
+ function next(event: MouseEvent<HTMLButtonElement>) {
99
+ event.preventDefault();
100
+ const found = validate();
101
+ const here = Object.fromEntries(Object.entries(found).filter(([key]) => visible.some(([field]) => field === key)));
102
+ if (Object.keys(here).length > 0) {
103
+ setErrors(here);
104
+ focusFirstError(here, visible);
105
+ return;
106
+ }
107
+ setErrors({});
108
+ setStep((current) => Math.min(current + 1, steps.length - 1));
109
+ }
110
+
55
111
  function onSubmit(event: FormEvent) {
56
112
  event.preventDefault();
57
113
  const input = formValuesToInput(resource, values, mode);
58
- const parsed = (mode === "create" ? validators.create : validators.update).safeParse(input);
59
- if (!parsed.success) {
60
- const fieldErrors = issuesByField(parsed.error.issues);
61
- setErrors(fieldErrors);
62
- focusFirstError(fieldErrors);
114
+ const found = validate();
115
+ if (Object.keys(found).length > 0) {
116
+ setErrors(found);
117
+ // Send them to the step that holds the problem, not just to the field.
118
+ const broken = steps.findIndex((entries) => entries.some(([key]) => found[key]));
119
+ if (broken >= 0 && broken !== step) setStep(broken);
120
+ focusFirstError(found, broken >= 0 ? steps[broken]! : fields);
63
121
  return;
64
122
  }
65
123
 
@@ -74,17 +132,20 @@ export function ResourceForm({ resource, mode, id, record, relations = {}, listH
74
132
  }
75
133
  const fieldErrors = result.issues ? issuesByField(result.issues) : result.field ? { [result.field]: result.error } : { _form: result.error };
76
134
  setErrors(fieldErrors);
77
- focusFirstError(fieldErrors);
135
+ const broken = steps.findIndex((entries) => entries.some(([key]) => fieldErrors[key]));
136
+ if (broken >= 0 && broken !== step) setStep(broken);
137
+ focusFirstError(fieldErrors, broken >= 0 ? steps[broken]! : fields);
78
138
  });
79
139
  }
80
140
 
81
- const Frame = onDone ? DialogFrame : Card;
82
- const Body = onDone ? DialogBody : CardContent;
83
- const Footer = onDone ? DialogFooterRow : CardFooter;
141
+ const Frame = onDone ? SheetFrame : Card;
142
+ const Body = onDone ? SheetBody : CardContent;
143
+ const Footer = onDone ? SheetFooterRow : CardFooter;
84
144
 
85
145
  return (
86
- <form onSubmit={onSubmit} noValidate>
146
+ <form onSubmit={onSubmit} noValidate className={onDone ? "flex min-h-0 flex-1 flex-col" : undefined}>
87
147
  <Frame>
148
+ {steps.length > 1 && <Steps count={steps.length} current={step} onGo={setStep} />}
88
149
  <Body>
89
150
  {errors._form && (
90
151
  <Alert variant="destructive" className="mb-6">
@@ -92,16 +153,12 @@ export function ResourceForm({ resource, mode, id, record, relations = {}, listH
92
153
  </Alert>
93
154
  )}
94
155
  <FieldGroup className="grid gap-6 md:grid-cols-2">
95
- {fields.map(([key, def]) => {
156
+ {visible.map(([key, def]) => {
96
157
  const error = errors[key];
97
158
  const widgetId = `field-${key}`;
98
159
  const horizontal = def.kind === "boolean";
99
160
  return (
100
- <Field
101
- key={key}
102
- data-invalid={error ? true : undefined}
103
- className={WIDE.has(def.kind) ? "md:col-span-2" : undefined}
104
- >
161
+ <Field key={key} data-invalid={error ? true : undefined} className={WIDE.has(def.kind) ? "md:col-span-2" : undefined}>
105
162
  <FieldLabel htmlFor={widgetId}>
106
163
  {def.label}
107
164
  {def.required && def.kind !== "boolean" && (
@@ -122,6 +179,7 @@ export function ResourceForm({ resource, mode, id, record, relations = {}, listH
122
179
  invalid={Boolean(error)}
123
180
  disabled={pending}
124
181
  relation={relations[key]}
182
+ suggest={canSuggest(def) ? () => suggestValue(key, def, String(values[resource.titleField] ?? "")) : undefined}
125
183
  />
126
184
  </div>
127
185
  {def.helpText && <FieldDescription>{def.helpText}</FieldDescription>}
@@ -131,8 +189,12 @@ export function ResourceForm({ resource, mode, id, record, relations = {}, listH
131
189
  })}
132
190
  </FieldGroup>
133
191
  </Body>
134
- <Footer className="flex justify-end gap-2 border-t pt-4">
135
- {onDone ? (
192
+ <Footer className="flex items-center justify-end gap-2 border-t pt-4">
193
+ {step > 0 ? (
194
+ <Button type="button" variant="outline" onClick={() => setStep((current) => current - 1)} disabled={pending}>
195
+ Back
196
+ </Button>
197
+ ) : onDone ? (
136
198
  <Button type="button" variant="outline" onClick={onDone} disabled={pending}>
137
199
  Cancel
138
200
  </Button>
@@ -141,25 +203,59 @@ export function ResourceForm({ resource, mode, id, record, relations = {}, listH
141
203
  <Link href={listHref}>Cancel</Link>
142
204
  </Button>
143
205
  )}
144
- <Button type="submit" disabled={pending}>
145
- {pending && <Spinner data-icon="inline-start" />}
146
- {mode === "create" ? `Create ${resource.label.toLowerCase()}` : "Save changes"}
147
- </Button>
206
+ {last ? (
207
+ <Button key="submit" type="submit" disabled={pending}>
208
+ {pending && <Spinner data-icon="inline-start" />}
209
+ {mode === "create" ? `Create ${resource.label.toLowerCase()}` : "Save changes"}
210
+ </Button>
211
+ ) : (
212
+ <Button key="next" type="button" onClick={next} disabled={pending}>
213
+ Next
214
+ </Button>
215
+ )}
148
216
  </Footer>
149
217
  </Frame>
150
218
  </form>
151
219
  );
152
220
  }
153
221
 
154
- /** Plain wrappers so the same form works in a page (a card) and in a dialog (no card). */
155
- function DialogFrame({ children }: { children: React.ReactNode }) {
156
- return <div className="flex flex-col gap-4">{children}</div>;
222
+ /** Where you are in a multi-step form, and a way back to a step already passed. */
223
+ function Steps({ count, current, onGo }: { count: number; current: number; onGo: (step: number) => void }) {
224
+ return (
225
+ <div className="flex items-center gap-3 pb-4">
226
+ <div className="flex flex-1 gap-1.5" role="presentation">
227
+ {Array.from({ length: count }, (_, index) => (
228
+ <button
229
+ key={index}
230
+ type="button"
231
+ aria-label={`Step ${index + 1} of ${count}`}
232
+ aria-current={index === current ? "step" : undefined}
233
+ disabled={index > current}
234
+ onClick={() => onGo(index)}
235
+ className={cn(
236
+ "h-1 flex-1 rounded-full transition-colors",
237
+ index <= current ? "bg-primary" : "bg-muted",
238
+ index < current && "cursor-pointer",
239
+ )}
240
+ />
241
+ ))}
242
+ </div>
243
+ <span className="text-xs text-muted-foreground tabular-nums">
244
+ Step {current + 1} of {count}
245
+ </span>
246
+ </div>
247
+ );
248
+ }
249
+
250
+ /** Plain wrappers so the same form works in a page (a card) and in a sheet (no card). */
251
+ function SheetFrame({ children }: { children: React.ReactNode }) {
252
+ return <div className="flex min-h-0 flex-1 flex-col gap-4">{children}</div>;
157
253
  }
158
254
 
159
- function DialogBody({ children }: { children: React.ReactNode }) {
160
- return <div className="max-h-[60vh] overflow-y-auto px-1">{children}</div>;
255
+ function SheetBody({ children }: { children: React.ReactNode }) {
256
+ return <div className="min-h-0 flex-1 overflow-y-auto px-1">{children}</div>;
161
257
  }
162
258
 
163
- function DialogFooterRow({ children, className }: { children: React.ReactNode; className?: string }) {
259
+ function SheetFooterRow({ children, className }: { children: React.ReactNode; className?: string }) {
164
260
  return <div className={className}>{children}</div>;
165
261
  }
@@ -1,85 +1,87 @@
1
- import {
2
- BellIcon,
3
- BookIcon,
4
- BoxIcon,
5
- BriefcaseIcon,
6
- Building2Icon,
7
- CalendarIcon,
8
- ChartBarIcon,
9
- ClipboardListIcon,
10
- CreditCardIcon,
11
- DatabaseIcon,
12
- DollarSignIcon,
13
- FileTextIcon,
14
- FolderIcon,
15
- GlobeIcon,
16
- HeartIcon,
17
- ImageIcon,
18
- LayersIcon,
19
- MailIcon,
20
- MapPinIcon,
21
- MessageSquareIcon,
22
- PackageIcon,
23
- PhoneIcon,
24
- ReceiptIcon,
25
- SettingsIcon,
26
- ShieldAlertIcon,
27
- RadioIcon,
28
- ShieldIcon,
29
- ShoppingCartIcon,
30
- StarIcon,
31
- TagIcon,
32
- TicketIcon,
33
- TruckIcon,
34
- UserIcon,
35
- UsersIcon,
36
- WrenchIcon,
37
- type LucideIcon,
38
- } from "lucide-react";
39
-
40
- /**
41
- * Icons a resource descriptor can name (`icon: "users"`). Imported by name so unused
42
- * ones are tree-shaken; add the lucide icons your app needs.
43
- */
44
- export const RESOURCE_ICONS: Record<string, LucideIcon> = {
45
- bell: BellIcon,
46
- book: BookIcon,
47
- box: BoxIcon,
48
- briefcase: BriefcaseIcon,
49
- building: Building2Icon,
50
- calendar: CalendarIcon,
51
- chart: ChartBarIcon,
52
- clipboard: ClipboardListIcon,
53
- "credit-card": CreditCardIcon,
54
- database: DatabaseIcon,
55
- dollar: DollarSignIcon,
56
- file: FileTextIcon,
57
- folder: FolderIcon,
58
- globe: GlobeIcon,
59
- heart: HeartIcon,
60
- image: ImageIcon,
61
- layers: LayersIcon,
62
- mail: MailIcon,
63
- "map-pin": MapPinIcon,
64
- message: MessageSquareIcon,
65
- package: PackageIcon,
66
- phone: PhoneIcon,
67
- receipt: ReceiptIcon,
68
- settings: SettingsIcon,
69
- radio: RadioIcon,
70
- shield: ShieldIcon,
71
- "shield-alert": ShieldAlertIcon,
72
- cart: ShoppingCartIcon,
73
- star: StarIcon,
74
- tag: TagIcon,
75
- ticket: TicketIcon,
76
- truck: TruckIcon,
77
- user: UserIcon,
78
- users: UsersIcon,
79
- wrench: WrenchIcon,
80
- };
81
-
82
- /** The icon for a resource, falling back to a generic document icon. */
83
- export function resourceIcon(name: string | undefined): LucideIcon {
84
- return (name && RESOURCE_ICONS[name]) || FileTextIcon;
85
- }
1
+ import {
2
+ BellIcon,
3
+ BookIcon,
4
+ WalletIcon,
5
+ BoxIcon,
6
+ BriefcaseIcon,
7
+ Building2Icon,
8
+ CalendarIcon,
9
+ ChartBarIcon,
10
+ ClipboardListIcon,
11
+ CreditCardIcon,
12
+ DatabaseIcon,
13
+ DollarSignIcon,
14
+ FileTextIcon,
15
+ FolderIcon,
16
+ GlobeIcon,
17
+ HeartIcon,
18
+ ImageIcon,
19
+ LayersIcon,
20
+ MailIcon,
21
+ MapPinIcon,
22
+ MessageSquareIcon,
23
+ PackageIcon,
24
+ PhoneIcon,
25
+ ReceiptIcon,
26
+ SettingsIcon,
27
+ ShieldAlertIcon,
28
+ RadioIcon,
29
+ ShieldIcon,
30
+ ShoppingCartIcon,
31
+ StarIcon,
32
+ TagIcon,
33
+ TicketIcon,
34
+ TruckIcon,
35
+ UserIcon,
36
+ UsersIcon,
37
+ WrenchIcon,
38
+ type LucideIcon,
39
+ } from "lucide-react";
40
+
41
+ /**
42
+ * Icons a resource descriptor can name (`icon: "users"`). Imported by name so unused
43
+ * ones are tree-shaken; add the lucide icons your app needs.
44
+ */
45
+ export const RESOURCE_ICONS: Record<string, LucideIcon> = {
46
+ bell: BellIcon,
47
+ book: BookIcon,
48
+ wallet: WalletIcon,
49
+ box: BoxIcon,
50
+ briefcase: BriefcaseIcon,
51
+ building: Building2Icon,
52
+ calendar: CalendarIcon,
53
+ chart: ChartBarIcon,
54
+ clipboard: ClipboardListIcon,
55
+ "credit-card": CreditCardIcon,
56
+ database: DatabaseIcon,
57
+ dollar: DollarSignIcon,
58
+ file: FileTextIcon,
59
+ folder: FolderIcon,
60
+ globe: GlobeIcon,
61
+ heart: HeartIcon,
62
+ image: ImageIcon,
63
+ layers: LayersIcon,
64
+ mail: MailIcon,
65
+ "map-pin": MapPinIcon,
66
+ message: MessageSquareIcon,
67
+ package: PackageIcon,
68
+ phone: PhoneIcon,
69
+ receipt: ReceiptIcon,
70
+ settings: SettingsIcon,
71
+ radio: RadioIcon,
72
+ shield: ShieldIcon,
73
+ "shield-alert": ShieldAlertIcon,
74
+ cart: ShoppingCartIcon,
75
+ star: StarIcon,
76
+ tag: TagIcon,
77
+ ticket: TicketIcon,
78
+ truck: TruckIcon,
79
+ user: UserIcon,
80
+ users: UsersIcon,
81
+ wrench: WrenchIcon,
82
+ };
83
+
84
+ /** The icon for a resource, falling back to a generic document icon. */
85
+ export function resourceIcon(name: string | undefined): LucideIcon {
86
+ return (name && RESOURCE_ICONS[name]) || FileTextIcon;
87
+ }
@@ -1,5 +1,5 @@
1
1
  import { storedFields, optionLabel, type Resource } from "@flaredev/core";
2
- import { recentCounts, recordCount, trend, valueCounts } from "@/lib/dashboard";
2
+ import { resourceStats, trend } from "@/lib/dashboard";
3
3
  import { resourceIcon } from "./resource-icon";
4
4
  import { StatCards, type Stat } from "./stat-card";
5
5
 
@@ -11,7 +11,10 @@ import { StatCards, type Stat } from "./stat-card";
11
11
  * in practice is the status of the thing — a deal's stage, an invoice's state.
12
12
  */
13
13
  export async function ResourceStats({ resource }: { resource: Resource }) {
14
- const [total, recent] = await Promise.all([recordCount(resource.name), recentCounts(resource.name)]);
14
+ // The status split is asked for up front so the totals, the trend and the split all
15
+ // come from the one grouped query rather than three.
16
+ const statusField = storedFields(resource).find(([, def]) => def.kind === "enum" && def.filterable !== false);
17
+ const { total, current, previous, values } = await resourceStats(resource.name, statusField?.[0]);
15
18
 
16
19
  const stats: Stat[] = [
17
20
  {
@@ -21,20 +24,18 @@ export async function ResourceStats({ resource }: { resource: Resource }) {
21
24
  },
22
25
  {
23
26
  label: "New this week",
24
- value: recent.current,
25
- change: trend(recent.current, recent.previous),
26
- hint: `${recent.previous.toLocaleString()} the week before`,
27
+ value: current,
28
+ change: trend(current, previous),
29
+ hint: `${previous.toLocaleString()} the week before`,
27
30
  },
28
31
  ];
29
32
 
30
- const statusField = storedFields(resource).find(([, def]) => def.kind === "enum" && def.filterable !== false);
31
33
  if (statusField && total > 0) {
32
- const [key, def] = statusField;
34
+ const [, def] = statusField;
33
35
  if (def.kind === "enum") {
34
- const counts = await valueCounts(resource.name, key);
35
36
  // Two options at most: a strip of eight cards is a wall, not a summary.
36
37
  for (const option of def.options.slice(0, 2)) {
37
- const value = counts[option] ?? 0;
38
+ const value = values[option] ?? 0;
38
39
  stats.push({
39
40
  label: optionLabel(def, option),
40
41
  value,
@@ -26,7 +26,7 @@ import type { RelationMeta } from "./fields/field-widget";
26
26
  import { ImportDialog } from "./import-dialog";
27
27
  import { LocalTime } from "./local-time";
28
28
  import { hrefWith, toSearchParams, type SearchParams } from "./query";
29
- import { NewRecordButton } from "./resource-form-dialog";
29
+ import { NewRecordButton } from "./resource-form-sheet";
30
30
  import { ResourceTableToolbar } from "./resource-table-toolbar";
31
31
  import { SaveViewButton } from "./save-view-button";
32
32
  import { RowActions } from "./row-actions";
@@ -90,7 +90,7 @@ export async function ResourceTable({ resource, searchParams }: { resource: Reso
90
90
  relations[key] = { name: target.name, label: target.label, pluralLabel: target.pluralLabel, slug: target.slug, titleField: target.titleField };
91
91
  }
92
92
 
93
- const modalForms = site.dashboard.forms === "modal";
93
+ const overlayForms = site.dashboard.forms === "sheet";
94
94
  const rowIds = rows.map((row) => String(row.id));
95
95
  const exportColumns = [{ key: "id", label: "Id" }, ...fields.filter(([, def]) => def.kind !== "file").map(([key, def]) => ({ key, label: def.label }))];
96
96
 
@@ -112,7 +112,7 @@ export async function ResourceTable({ resource, searchParams }: { resource: Reso
112
112
  <ExportButton resourceName={resource.name} pluralLabel={resource.pluralLabel} />
113
113
  {permissions.create && <ImportDialog resource={forClient} />}
114
114
  {permissions.create &&
115
- (modalForms ? (
115
+ (overlayForms ? (
116
116
  <NewRecordButton resource={forClient} relations={relations} listHref={basePath} />
117
117
  ) : (
118
118
  <Button asChild>
@@ -286,7 +286,7 @@ export async function ResourceTable({ resource, searchParams }: { resource: Reso
286
286
  <RowActions
287
287
  resource={forClient}
288
288
  id={id}
289
- record={modalForms ? row : undefined}
289
+ record={overlayForms ? row : undefined}
290
290
  relations={relations}
291
291
  listHref={basePath}
292
292
  editHref={resourcePath(resource, id, "edit")}
@@ -26,11 +26,11 @@ import {
26
26
  DropdownMenuTrigger,
27
27
  } from "@/components/ui/dropdown-menu";
28
28
  import { Spinner } from "@/components/ui/spinner";
29
- import { ResourceFormDialog, type FormRelations } from "./resource-form-dialog";
29
+ import { ResourceFormSheet, type FormRelations } from "./resource-form-sheet";
30
30
 
31
31
  /**
32
32
  * Edit and delete for one row. Edit opens a dialog when the row's record came with it
33
- * (site.dashboard.forms is "modal"), and otherwise goes to the form page.
33
+ * (site.dashboard.forms is "sheet"), and otherwise goes to the form page.
34
34
  */
35
35
  export function RowActions({
36
36
  resource,
@@ -124,7 +124,7 @@ export function RowActions({
124
124
  </DropdownMenu>
125
125
 
126
126
  {record && canUpdate && (
127
- <ResourceFormDialog
127
+ <ResourceFormSheet
128
128
  resource={resource}
129
129
  relations={relations}
130
130
  listHref={listHref}