@flaredev/cli 0.4.0 → 0.5.0

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 (59) hide show
  1. package/dist/index.js +851 -405
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/actions.ts +38 -5
  4. package/templates/app/app/dashboard/costs/page.tsx +93 -0
  5. package/templates/app/app/dashboard/page.tsx +4 -7
  6. package/templates/app/app/globals.css +0 -6
  7. package/templates/app/app/layout.tsx +37 -28
  8. package/templates/app/app/page.tsx +10 -0
  9. package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
  10. package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
  11. package/templates/app/components/dashboard/fields/file-field.tsx +5 -1
  12. package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
  13. package/templates/app/components/dashboard/import-dialog.tsx +2 -2
  14. package/templates/app/components/dashboard/record-actions.tsx +9 -9
  15. package/templates/app/components/dashboard/record-detail.tsx +6 -3
  16. package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
  17. package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
  18. package/templates/app/components/dashboard/resource-form.tsx +261 -165
  19. package/templates/app/components/dashboard/resource-icon.tsx +87 -85
  20. package/templates/app/components/dashboard/resource-stats.tsx +10 -9
  21. package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
  22. package/templates/app/components/dashboard/resource-table.tsx +12 -9
  23. package/templates/app/components/dashboard/row-actions.tsx +5 -5
  24. package/templates/app/lib/costs.ts +138 -0
  25. package/templates/app/lib/dashboard-nav.ts +5 -1
  26. package/templates/app/lib/dashboard.ts +66 -61
  27. package/templates/app/lib/number.ts +33 -0
  28. package/templates/app/lib/site.ts +56 -56
  29. package/templates/app/lib/suggest.ts +35 -0
  30. package/templates/app/lib/usage.ts +32 -0
  31. package/templates/locks/package-lock.json +9 -9
  32. package/templates/locks/pnpm-lock.yaml +12 -12
  33. package/templates/locks/yarn.lock +3 -3
  34. package/templates/next/.env.example +21 -0
  35. package/templates/next/README.md +16 -0
  36. package/templates/next/app/api/health/route.ts +7 -0
  37. package/templates/next/app/dashboard/account/password/page.tsx +19 -0
  38. package/templates/next/app/dashboard/account/security/page.tsx +31 -0
  39. package/templates/next/app/dashboard/actions.ts +277 -0
  40. package/templates/next/app/dashboard/layout.tsx +64 -0
  41. package/templates/next/app/dashboard/page.tsx +127 -0
  42. package/templates/next/components/dashboard/resource-chart.tsx +86 -0
  43. package/templates/next/lib/analytics.ts +71 -0
  44. package/templates/next/lib/audit.ts +80 -0
  45. package/templates/next/lib/auth-emails.ts +92 -0
  46. package/templates/next/lib/auth.ts +156 -0
  47. package/templates/next/lib/dashboard.ts +161 -0
  48. package/templates/next/lib/db.ts +24 -0
  49. package/templates/next/lib/mail.ts +35 -0
  50. package/templates/next/lib/realtime.ts +23 -0
  51. package/templates/next/lib/redis.ts +17 -0
  52. package/templates/next/lib/storage.ts +57 -0
  53. package/templates/next/lib/theme.ts +83 -0
  54. package/templates/next/lib/views.ts +23 -0
  55. package/templates/next/next.config.ts +13 -0
  56. package/templates/next/prisma/schema/base.prisma +162 -0
  57. package/templates/next/prisma.config.ts +15 -0
  58. package/templates/next/tsconfig.json +24 -0
  59. package/templates/app/components/dashboard/resource-form-dialog.tsx +0 -80
@@ -1,61 +1,64 @@
1
- import { notFound } from "next/navigation";
2
- import { formatValue, storedFields, type Resource } from "@flaredev/core";
3
- import { resourcePath, allResources, dashboardStore, requireAccess } from "@/lib/dashboard";
4
- import type { RelationMeta } from "./fields/field-widget";
5
- import { PageHeader } from "./page-header";
6
- import { ResourceForm } from "./resource-form";
7
-
8
- /** Server wrapper for create/edit pages: loads the record and relation titles, renders the heading and form. */
9
- export async function ResourceFormPage({ resource, id }: { resource: Resource; id?: string }) {
10
- await requireAccess(resource, id ? "update" : "create");
11
- let record: Record<string, unknown> | null = null;
12
- if (id) {
13
- const result = await dashboardStore(resource.name).get(id);
14
- if (!result.ok) notFound();
15
- record = result.data;
16
- }
17
-
18
- const byName = new Map(allResources().map((r) => [r.name, r]));
19
- const relations: Record<string, RelationMeta & { initialTitle?: string }> = {};
20
- for (const [key, def] of storedFields(resource)) {
21
- if (def.kind !== "belongsTo") continue;
22
- const target = byName.get(def.target);
23
- if (!target) continue;
24
- const value = record?.[key];
25
- const titles = typeof value === "string" ? await dashboardStore(target.name).titles([value]) : {};
26
- relations[key] = {
27
- name: target.name,
28
- label: target.label,
29
- pluralLabel: target.pluralLabel,
30
- slug: target.slug,
31
- titleField: target.titleField,
32
- initialTitle: typeof value === "string" ? titles[value] : undefined,
33
- };
34
- }
35
-
36
- const titleDef = resource.fields[resource.titleField];
37
- const title =
38
- record && titleDef && titleDef.kind !== "hasMany" ? formatValue(titleDef, record[resource.titleField]) : `New ${resource.label.toLowerCase()}`;
39
-
40
- return (
41
- <div className="flex flex-col gap-6">
42
- <PageHeader
43
- title={title}
44
- description={record ? `Edit this ${resource.label.toLowerCase()}.` : `Add a ${resource.label.toLowerCase()} to ${resource.pluralLabel.toLowerCase()}.`}
45
- crumbs={[
46
- { label: "Dashboard", href: "/dashboard" },
47
- { label: resource.pluralLabel, href: resourcePath(resource) },
48
- { label: record ? "Edit" : "New" },
49
- ]}
50
- />
51
- <ResourceForm
52
- resource={resource}
53
- mode={record ? "edit" : "create"}
54
- id={id}
55
- record={record}
56
- relations={relations}
57
- listHref={resourcePath(resource)}
58
- />
59
- </div>
60
- );
61
- }
1
+ import { notFound } from "next/navigation";
2
+ import { clientResource, formatValue, storedFields, type Resource } from "@flaredev/core";
3
+ import { resourcePath, allResources, dashboardStore, requireAccess } from "@/lib/dashboard";
4
+ import type { RelationMeta } from "./fields/field-widget";
5
+ import { PageHeader } from "./page-header";
6
+ import { ResourceForm } from "./resource-form";
7
+
8
+ /** Server wrapper for create/edit pages: loads the record and relation titles, renders the heading and form. */
9
+ export async function ResourceFormPage({ resource, id }: { resource: Resource; id?: string }) {
10
+ await requireAccess(resource, id ? "update" : "create");
11
+ // Hooks and computed values are functions, and a function can't be sent to the browser.
12
+ // This is the same resource without them, for the client components below.
13
+ const forClient = clientResource(resource);
14
+ let record: Record<string, unknown> | null = null;
15
+ if (id) {
16
+ const result = await dashboardStore(resource.name).get(id);
17
+ if (!result.ok) notFound();
18
+ record = result.data;
19
+ }
20
+
21
+ const byName = new Map(allResources().map((r) => [r.name, r]));
22
+ const relations: Record<string, RelationMeta & { initialTitle?: string }> = {};
23
+ for (const [key, def] of storedFields(resource)) {
24
+ if (def.kind !== "belongsTo") continue;
25
+ const target = byName.get(def.target);
26
+ if (!target) continue;
27
+ const value = record?.[key];
28
+ const titles = typeof value === "string" ? await dashboardStore(target.name).titles([value]) : {};
29
+ relations[key] = {
30
+ name: target.name,
31
+ label: target.label,
32
+ pluralLabel: target.pluralLabel,
33
+ slug: target.slug,
34
+ titleField: target.titleField,
35
+ initialTitle: typeof value === "string" ? titles[value] : undefined,
36
+ };
37
+ }
38
+
39
+ const titleDef = resource.fields[resource.titleField];
40
+ const title =
41
+ record && titleDef && titleDef.kind !== "hasMany" ? formatValue(titleDef, record[resource.titleField]) : `New ${resource.label.toLowerCase()}`;
42
+
43
+ return (
44
+ <div className="flex flex-col gap-6">
45
+ <PageHeader
46
+ title={title}
47
+ description={record ? `Edit this ${resource.label.toLowerCase()}.` : `Add a ${resource.label.toLowerCase()} to ${resource.pluralLabel.toLowerCase()}.`}
48
+ crumbs={[
49
+ { label: "Dashboard", href: "/dashboard" },
50
+ { label: resource.pluralLabel, href: resourcePath(resource) },
51
+ { label: record ? "Edit" : "New" },
52
+ ]}
53
+ />
54
+ <ResourceForm
55
+ resource={forClient}
56
+ mode={record ? "edit" : "create"}
57
+ id={id}
58
+ record={record}
59
+ relations={relations}
60
+ listHref={resourcePath(resource)}
61
+ />
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { PlusIcon } from "lucide-react";
5
+ import type { ClientResource } from "@flaredev/core";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
8
+ import type { RelationMeta } from "./fields/field-widget";
9
+ import { ResourceForm } from "./resource-form";
10
+
11
+ export type FormRelations = Record<string, RelationMeta & { initialTitle?: string }>;
12
+
13
+ interface Props {
14
+ resource: ClientResource;
15
+ relations: FormRelations;
16
+ listHref: string;
17
+ mode: "create" | "edit";
18
+ /** Edit: the record and its id. */
19
+ id?: string;
20
+ record?: Record<string, unknown> | null;
21
+ /** Controlled use (a row menu opens it); otherwise pass `trigger`. */
22
+ open?: boolean;
23
+ onOpenChange?: (open: boolean) => void;
24
+ trigger?: React.ReactNode;
25
+ }
26
+
27
+ /**
28
+ * The create/edit form in a sheet, so adding a record doesn't lose the list, its filters
29
+ * or the place in it.
30
+ *
31
+ * A sheet rather than a centred dialog: a form is a column of fields, and a panel down
32
+ * the side of the screen has room for them without the page underneath jumping about or
33
+ * the fields being squeezed into a box. Apps that would rather have a page set
34
+ * `site.dashboard.forms` to "page" (lib/site.ts) and get the same form full width.
35
+ */
36
+ export function ResourceFormSheet({ resource, relations, listHref, mode, id, record, open, onOpenChange, trigger }: Props) {
37
+ const [internalOpen, setInternalOpen] = useState(false);
38
+ const isOpen = open ?? internalOpen;
39
+ const setOpen = onOpenChange ?? setInternalOpen;
40
+ const noun = resource.label.toLowerCase();
41
+
42
+ return (
43
+ <Sheet open={isOpen} onOpenChange={setOpen}>
44
+ {trigger && <SheetTrigger asChild>{trigger}</SheetTrigger>}
45
+ <SheetContent side="right" className="flex w-full flex-col gap-0 p-0 sm:max-w-xl">
46
+ <SheetHeader className="border-b">
47
+ <SheetTitle>{mode === "create" ? `New ${noun}` : `Edit ${noun}`}</SheetTitle>
48
+ <SheetDescription>{mode === "create" ? "Fill this in and it appears in the list." : "Changes save straight away."}</SheetDescription>
49
+ </SheetHeader>
50
+ {/* Remounting on open clears whatever was typed and abandoned last time. */}
51
+ {isOpen && (
52
+ <div className="flex min-h-0 flex-1 flex-col p-4">
53
+ <ResourceForm
54
+ key={`${mode}-${id ?? "new"}`}
55
+ resource={resource}
56
+ mode={mode}
57
+ id={id}
58
+ record={record}
59
+ relations={relations}
60
+ listHref={listHref}
61
+ onDone={() => setOpen(false)}
62
+ />
63
+ </div>
64
+ )}
65
+ </SheetContent>
66
+ </Sheet>
67
+ );
68
+ }
69
+
70
+ /** The list's "New …" button, with the sheet it opens. */
71
+ export function NewRecordButton({ resource, relations, listHref }: { resource: ClientResource; relations: FormRelations; listHref: string }) {
72
+ return (
73
+ <ResourceFormSheet
74
+ resource={resource}
75
+ relations={relations}
76
+ listHref={listHref}
77
+ mode="create"
78
+ trigger={
79
+ <Button>
80
+ <PlusIcon data-icon="inline-start" />
81
+ New {resource.label.toLowerCase()}
82
+ </Button>
83
+ }
84
+ />
85
+ );
86
+ }