@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.
- package/dist/index.js +851 -405
- package/package.json +2 -2
- package/templates/app/app/dashboard/actions.ts +38 -5
- package/templates/app/app/dashboard/costs/page.tsx +93 -0
- package/templates/app/app/dashboard/page.tsx +4 -7
- package/templates/app/app/globals.css +0 -6
- package/templates/app/app/layout.tsx +37 -28
- package/templates/app/app/page.tsx +10 -0
- package/templates/app/components/dashboard/cost-estimator.tsx +77 -0
- package/templates/app/components/dashboard/fields/field-widget.tsx +237 -186
- package/templates/app/components/dashboard/fields/file-field.tsx +5 -1
- package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
- package/templates/app/components/dashboard/import-dialog.tsx +2 -2
- package/templates/app/components/dashboard/record-actions.tsx +9 -9
- package/templates/app/components/dashboard/record-detail.tsx +6 -3
- package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
- package/templates/app/components/dashboard/resource-form-sheet.tsx +86 -0
- package/templates/app/components/dashboard/resource-form.tsx +261 -165
- package/templates/app/components/dashboard/resource-icon.tsx +87 -85
- package/templates/app/components/dashboard/resource-stats.tsx +10 -9
- package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
- package/templates/app/components/dashboard/resource-table.tsx +12 -9
- package/templates/app/components/dashboard/row-actions.tsx +5 -5
- package/templates/app/lib/costs.ts +138 -0
- package/templates/app/lib/dashboard-nav.ts +5 -1
- package/templates/app/lib/dashboard.ts +66 -61
- package/templates/app/lib/number.ts +33 -0
- package/templates/app/lib/site.ts +56 -56
- package/templates/app/lib/suggest.ts +35 -0
- package/templates/app/lib/usage.ts +32 -0
- package/templates/locks/package-lock.json +9 -9
- package/templates/locks/pnpm-lock.yaml +12 -12
- package/templates/locks/yarn.lock +3 -3
- package/templates/next/.env.example +21 -0
- package/templates/next/README.md +16 -0
- package/templates/next/app/api/health/route.ts +7 -0
- package/templates/next/app/dashboard/account/password/page.tsx +19 -0
- package/templates/next/app/dashboard/account/security/page.tsx +31 -0
- package/templates/next/app/dashboard/actions.ts +277 -0
- package/templates/next/app/dashboard/layout.tsx +64 -0
- package/templates/next/app/dashboard/page.tsx +127 -0
- package/templates/next/components/dashboard/resource-chart.tsx +86 -0
- package/templates/next/lib/analytics.ts +71 -0
- package/templates/next/lib/audit.ts +80 -0
- package/templates/next/lib/auth-emails.ts +92 -0
- package/templates/next/lib/auth.ts +156 -0
- package/templates/next/lib/dashboard.ts +161 -0
- package/templates/next/lib/db.ts +24 -0
- package/templates/next/lib/mail.ts +35 -0
- package/templates/next/lib/realtime.ts +23 -0
- package/templates/next/lib/redis.ts +17 -0
- package/templates/next/lib/storage.ts +57 -0
- package/templates/next/lib/theme.ts +83 -0
- package/templates/next/lib/views.ts +23 -0
- package/templates/next/next.config.ts +13 -0
- package/templates/next/prisma/schema/base.prisma +162 -0
- package/templates/next/prisma.config.ts +15 -0
- package/templates/next/tsconfig.json +24 -0
- 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
}
|