@flaredev/cli 0.3.0 → 0.4.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.
- package/dist/index.js +498 -220
- package/package.json +2 -2
- package/templates/app/app/dashboard/account/layout.tsx +30 -0
- package/templates/app/app/dashboard/account/page.tsx +11 -33
- package/templates/app/app/dashboard/account/password/page.tsx +22 -0
- package/templates/app/app/dashboard/account/security/page.tsx +34 -0
- package/templates/app/app/dashboard/account/sessions/page.tsx +9 -0
- package/templates/app/app/dashboard/actions.ts +73 -5
- package/templates/app/app/dashboard/layout.tsx +28 -4
- package/templates/app/app/dashboard/observability/page.tsx +4 -3
- package/templates/app/app/dashboard/page.tsx +2 -2
- package/templates/app/app/globals.css +0 -6
- package/templates/app/app/layout.tsx +37 -28
- package/templates/app/components/account/account-nav.tsx +39 -0
- package/templates/app/components/account/{security-settings.tsx → sections.tsx} +507 -522
- package/templates/app/components/dashboard/column-menu.tsx +66 -0
- package/templates/app/components/dashboard/dashboard-header.tsx +115 -78
- package/templates/app/components/dashboard/dashboard-sidebar.tsx +86 -24
- package/templates/app/components/dashboard/dropzone.tsx +92 -0
- package/templates/app/components/dashboard/editable.ts +12 -0
- package/templates/app/components/dashboard/fields/file-field.tsx +310 -302
- package/templates/app/components/dashboard/fields/relation-field.tsx +25 -3
- package/templates/app/components/dashboard/import-dialog.tsx +62 -9
- package/templates/app/components/dashboard/inline-cell.tsx +165 -0
- package/templates/app/components/dashboard/local-time.tsx +77 -0
- package/templates/app/components/dashboard/query.ts +2 -0
- package/templates/app/components/dashboard/record-actions.tsx +113 -0
- package/templates/app/components/dashboard/record-detail.tsx +203 -0
- package/templates/app/components/dashboard/resource-chart.tsx +91 -0
- package/templates/app/components/dashboard/resource-form-dialog.tsx +3 -3
- package/templates/app/components/dashboard/resource-form-page.tsx +64 -61
- package/templates/app/components/dashboard/resource-form.tsx +165 -165
- package/templates/app/components/dashboard/resource-stats.tsx +6 -11
- package/templates/app/components/dashboard/resource-table-toolbar.tsx +2 -2
- package/templates/app/components/dashboard/resource-table.tsx +408 -331
- package/templates/app/components/dashboard/row-actions.tsx +156 -144
- package/templates/app/components/dashboard/save-view-button.tsx +86 -0
- package/templates/app/components/dashboard/user-menu.tsx +91 -0
- package/templates/app/components/ui/sonner.tsx +33 -13
- package/templates/app/db/flare-schema.ts +22 -0
- package/templates/app/lib/api.ts +12 -0
- package/templates/app/lib/dashboard.ts +165 -138
- package/templates/app/lib/views.ts +29 -0
- package/templates/app/migrations/0000_init.sql +11 -0
- package/templates/app/migrations/meta/0000_snapshot.json +63 -1
- package/templates/app/vite.config.ts +41 -31
- package/templates/locks/package-lock.json +9 -9
- package/templates/locks/pnpm-lock.yaml +12 -12
- package/templates/locks/yarn.lock +3548 -0
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import Link from "next/link";
|
|
4
|
-
import { useRouter } from "next/navigation";
|
|
5
|
-
import { useMemo, useState, useTransition, type FormEvent } from "react";
|
|
6
|
-
import { createValidators, formValuesToInput, initialFormValues, issuesByField, storedFields, type
|
|
7
|
-
import { toast } from "sonner";
|
|
8
|
-
import { createRecordAction, updateRecordAction } from "@/app/dashboard/actions";
|
|
9
|
-
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
10
|
-
import { Button } from "@/components/ui/button";
|
|
11
|
-
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
|
12
|
-
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
13
|
-
import { Spinner } from "@/components/ui/spinner";
|
|
14
|
-
import { FieldWidget, type RelationMeta } from "./fields/field-widget";
|
|
15
|
-
|
|
16
|
-
export interface ResourceFormProps {
|
|
17
|
-
resource:
|
|
18
|
-
mode: "create" | "edit";
|
|
19
|
-
/** Record id (edit mode). */
|
|
20
|
-
id?: string;
|
|
21
|
-
record?: Record<string, unknown> | null;
|
|
22
|
-
/** belongsTo field key → related resource metadata and the current value's title. */
|
|
23
|
-
relations?: Record<string, RelationMeta & { initialTitle?: string }>;
|
|
24
|
-
/** Where Cancel and a successful save go, when the form owns the page. */
|
|
25
|
-
listHref: string;
|
|
26
|
-
/** In a dialog: close it instead of navigating, and drop the card around the fields. */
|
|
27
|
-
onDone?: () => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const WIDE = new Set(["text", "file"]);
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Create/edit form rendered from field metadata. Values are validated in the browser
|
|
34
|
-
* with the descriptor's zod schemas, then again on the server by the same store the
|
|
35
|
-
* REST API uses; server errors (e.g. unique conflicts) land on their fields.
|
|
36
|
-
*/
|
|
37
|
-
export function ResourceForm({ resource, mode, id, record, relations = {}, listHref, onDone }: ResourceFormProps) {
|
|
38
|
-
const router = useRouter();
|
|
39
|
-
const validators = useMemo(() => createValidators(resource), [resource]);
|
|
40
|
-
const fields = useMemo(() => storedFields(resource), [resource]);
|
|
41
|
-
const [values, setValues] = useState(() => initialFormValues(resource, record));
|
|
42
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
43
|
-
const [pending, startTransition] = useTransition();
|
|
44
|
-
|
|
45
|
-
const setValue = (key: string, value: string | boolean) => {
|
|
46
|
-
setValues((current) => ({ ...current, [key]: value }));
|
|
47
|
-
if (errors[key]) setErrors(({ [key]: _removed, ...rest }) => rest);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
function focusFirstError(fieldErrors: Record<string, string>) {
|
|
51
|
-
const first = fields.find(([key]) => fieldErrors[key]);
|
|
52
|
-
if (first) document.getElementById(`field-${first[0]}`)?.focus();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function onSubmit(event: FormEvent) {
|
|
56
|
-
event.preventDefault();
|
|
57
|
-
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);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
startTransition(async () => {
|
|
67
|
-
const result = mode === "create" ? await createRecordAction(resource.name, input) : await updateRecordAction(resource.name, id!, input);
|
|
68
|
-
if (result.ok) {
|
|
69
|
-
toast.success(`${resource.label} ${mode === "create" ? "created" : "saved"}.`);
|
|
70
|
-
if (onDone) onDone();
|
|
71
|
-
else router.push(listHref);
|
|
72
|
-
router.refresh();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const fieldErrors = result.issues ? issuesByField(result.issues) : result.field ? { [result.field]: result.error } : { _form: result.error };
|
|
76
|
-
setErrors(fieldErrors);
|
|
77
|
-
focusFirstError(fieldErrors);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const Frame = onDone ? DialogFrame : Card;
|
|
82
|
-
const Body = onDone ? DialogBody : CardContent;
|
|
83
|
-
const Footer = onDone ? DialogFooterRow : CardFooter;
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<form onSubmit={onSubmit} noValidate>
|
|
87
|
-
<Frame>
|
|
88
|
-
<Body>
|
|
89
|
-
{errors._form && (
|
|
90
|
-
<Alert variant="destructive" className="mb-6">
|
|
91
|
-
<AlertDescription>{errors._form}</AlertDescription>
|
|
92
|
-
</Alert>
|
|
93
|
-
)}
|
|
94
|
-
<FieldGroup className="grid gap-6 md:grid-cols-2">
|
|
95
|
-
{fields.map(([key, def]) => {
|
|
96
|
-
const error = errors[key];
|
|
97
|
-
const widgetId = `field-${key}`;
|
|
98
|
-
const horizontal = def.kind === "boolean";
|
|
99
|
-
return (
|
|
100
|
-
<Field
|
|
101
|
-
key={key}
|
|
102
|
-
data-invalid={error ? true : undefined}
|
|
103
|
-
className={WIDE.has(def.kind) ? "md:col-span-2" : undefined}
|
|
104
|
-
>
|
|
105
|
-
<FieldLabel htmlFor={widgetId}>
|
|
106
|
-
{def.label}
|
|
107
|
-
{def.required && def.kind !== "boolean" && (
|
|
108
|
-
<span className="ml-0.5 text-muted-foreground" aria-hidden>
|
|
109
|
-
*
|
|
110
|
-
</span>
|
|
111
|
-
)}
|
|
112
|
-
</FieldLabel>
|
|
113
|
-
<div className={horizontal ? "flex h-9 items-center" : undefined}>
|
|
114
|
-
<FieldWidget
|
|
115
|
-
id={widgetId}
|
|
116
|
-
name={key}
|
|
117
|
-
resourceName={resource.name}
|
|
118
|
-
fieldKey={key}
|
|
119
|
-
field={def}
|
|
120
|
-
value={values[key] ?? ""}
|
|
121
|
-
onChange={(value) => setValue(key, value)}
|
|
122
|
-
invalid={Boolean(error)}
|
|
123
|
-
disabled={pending}
|
|
124
|
-
relation={relations[key]}
|
|
125
|
-
/>
|
|
126
|
-
</div>
|
|
127
|
-
{def.helpText && <FieldDescription>{def.helpText}</FieldDescription>}
|
|
128
|
-
{error && <FieldError>{error}</FieldError>}
|
|
129
|
-
</Field>
|
|
130
|
-
);
|
|
131
|
-
})}
|
|
132
|
-
</FieldGroup>
|
|
133
|
-
</Body>
|
|
134
|
-
<Footer className="flex justify-end gap-2 border-t pt-4">
|
|
135
|
-
{onDone ? (
|
|
136
|
-
<Button type="button" variant="outline" onClick={onDone} disabled={pending}>
|
|
137
|
-
Cancel
|
|
138
|
-
</Button>
|
|
139
|
-
) : (
|
|
140
|
-
<Button variant="outline" asChild>
|
|
141
|
-
<Link href={listHref}>Cancel</Link>
|
|
142
|
-
</Button>
|
|
143
|
-
)}
|
|
144
|
-
<Button type="submit" disabled={pending}>
|
|
145
|
-
{pending && <Spinner data-icon="inline-start" />}
|
|
146
|
-
{mode === "create" ? `Create ${resource.label.toLowerCase()}` : "Save changes"}
|
|
147
|
-
</Button>
|
|
148
|
-
</Footer>
|
|
149
|
-
</Frame>
|
|
150
|
-
</form>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
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>;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function DialogBody({ children }: { children: React.ReactNode }) {
|
|
160
|
-
return <div className="max-h-[60vh] overflow-y-auto px-1">{children}</div>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function DialogFooterRow({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
164
|
-
return <div className={className}>{children}</div>;
|
|
165
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
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";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { createRecordAction, updateRecordAction } from "@/app/dashboard/actions";
|
|
9
|
+
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
|
12
|
+
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
13
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
14
|
+
import { FieldWidget, type RelationMeta } from "./fields/field-widget";
|
|
15
|
+
|
|
16
|
+
export interface ResourceFormProps {
|
|
17
|
+
resource: ClientResource;
|
|
18
|
+
mode: "create" | "edit";
|
|
19
|
+
/** Record id (edit mode). */
|
|
20
|
+
id?: string;
|
|
21
|
+
record?: Record<string, unknown> | null;
|
|
22
|
+
/** belongsTo field key → related resource metadata and the current value's title. */
|
|
23
|
+
relations?: Record<string, RelationMeta & { initialTitle?: string }>;
|
|
24
|
+
/** Where Cancel and a successful save go, when the form owns the page. */
|
|
25
|
+
listHref: string;
|
|
26
|
+
/** In a dialog: close it instead of navigating, and drop the card around the fields. */
|
|
27
|
+
onDone?: () => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const WIDE = new Set(["text", "file"]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create/edit form rendered from field metadata. Values are validated in the browser
|
|
34
|
+
* with the descriptor's zod schemas, then again on the server by the same store the
|
|
35
|
+
* REST API uses; server errors (e.g. unique conflicts) land on their fields.
|
|
36
|
+
*/
|
|
37
|
+
export function ResourceForm({ resource, mode, id, record, relations = {}, listHref, onDone }: ResourceFormProps) {
|
|
38
|
+
const router = useRouter();
|
|
39
|
+
const validators = useMemo(() => createValidators(resource), [resource]);
|
|
40
|
+
const fields = useMemo(() => storedFields(resource), [resource]);
|
|
41
|
+
const [values, setValues] = useState(() => initialFormValues(resource, record));
|
|
42
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
43
|
+
const [pending, startTransition] = useTransition();
|
|
44
|
+
|
|
45
|
+
const setValue = (key: string, value: string | boolean) => {
|
|
46
|
+
setValues((current) => ({ ...current, [key]: value }));
|
|
47
|
+
if (errors[key]) setErrors(({ [key]: _removed, ...rest }) => rest);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function focusFirstError(fieldErrors: Record<string, string>) {
|
|
51
|
+
const first = fields.find(([key]) => fieldErrors[key]);
|
|
52
|
+
if (first) document.getElementById(`field-${first[0]}`)?.focus();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onSubmit(event: FormEvent) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
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);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
startTransition(async () => {
|
|
67
|
+
const result = mode === "create" ? await createRecordAction(resource.name, input) : await updateRecordAction(resource.name, id!, input);
|
|
68
|
+
if (result.ok) {
|
|
69
|
+
toast.success(`${resource.label} ${mode === "create" ? "created" : "saved"}.`);
|
|
70
|
+
if (onDone) onDone();
|
|
71
|
+
else router.push(listHref);
|
|
72
|
+
router.refresh();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const fieldErrors = result.issues ? issuesByField(result.issues) : result.field ? { [result.field]: result.error } : { _form: result.error };
|
|
76
|
+
setErrors(fieldErrors);
|
|
77
|
+
focusFirstError(fieldErrors);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const Frame = onDone ? DialogFrame : Card;
|
|
82
|
+
const Body = onDone ? DialogBody : CardContent;
|
|
83
|
+
const Footer = onDone ? DialogFooterRow : CardFooter;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<form onSubmit={onSubmit} noValidate>
|
|
87
|
+
<Frame>
|
|
88
|
+
<Body>
|
|
89
|
+
{errors._form && (
|
|
90
|
+
<Alert variant="destructive" className="mb-6">
|
|
91
|
+
<AlertDescription>{errors._form}</AlertDescription>
|
|
92
|
+
</Alert>
|
|
93
|
+
)}
|
|
94
|
+
<FieldGroup className="grid gap-6 md:grid-cols-2">
|
|
95
|
+
{fields.map(([key, def]) => {
|
|
96
|
+
const error = errors[key];
|
|
97
|
+
const widgetId = `field-${key}`;
|
|
98
|
+
const horizontal = def.kind === "boolean";
|
|
99
|
+
return (
|
|
100
|
+
<Field
|
|
101
|
+
key={key}
|
|
102
|
+
data-invalid={error ? true : undefined}
|
|
103
|
+
className={WIDE.has(def.kind) ? "md:col-span-2" : undefined}
|
|
104
|
+
>
|
|
105
|
+
<FieldLabel htmlFor={widgetId}>
|
|
106
|
+
{def.label}
|
|
107
|
+
{def.required && def.kind !== "boolean" && (
|
|
108
|
+
<span className="ml-0.5 text-muted-foreground" aria-hidden>
|
|
109
|
+
*
|
|
110
|
+
</span>
|
|
111
|
+
)}
|
|
112
|
+
</FieldLabel>
|
|
113
|
+
<div className={horizontal ? "flex h-9 items-center" : undefined}>
|
|
114
|
+
<FieldWidget
|
|
115
|
+
id={widgetId}
|
|
116
|
+
name={key}
|
|
117
|
+
resourceName={resource.name}
|
|
118
|
+
fieldKey={key}
|
|
119
|
+
field={def}
|
|
120
|
+
value={values[key] ?? ""}
|
|
121
|
+
onChange={(value) => setValue(key, value)}
|
|
122
|
+
invalid={Boolean(error)}
|
|
123
|
+
disabled={pending}
|
|
124
|
+
relation={relations[key]}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
{def.helpText && <FieldDescription>{def.helpText}</FieldDescription>}
|
|
128
|
+
{error && <FieldError>{error}</FieldError>}
|
|
129
|
+
</Field>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</FieldGroup>
|
|
133
|
+
</Body>
|
|
134
|
+
<Footer className="flex justify-end gap-2 border-t pt-4">
|
|
135
|
+
{onDone ? (
|
|
136
|
+
<Button type="button" variant="outline" onClick={onDone} disabled={pending}>
|
|
137
|
+
Cancel
|
|
138
|
+
</Button>
|
|
139
|
+
) : (
|
|
140
|
+
<Button variant="outline" asChild>
|
|
141
|
+
<Link href={listHref}>Cancel</Link>
|
|
142
|
+
</Button>
|
|
143
|
+
)}
|
|
144
|
+
<Button type="submit" disabled={pending}>
|
|
145
|
+
{pending && <Spinner data-icon="inline-start" />}
|
|
146
|
+
{mode === "create" ? `Create ${resource.label.toLowerCase()}` : "Save changes"}
|
|
147
|
+
</Button>
|
|
148
|
+
</Footer>
|
|
149
|
+
</Frame>
|
|
150
|
+
</form>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
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>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function DialogBody({ children }: { children: React.ReactNode }) {
|
|
160
|
+
return <div className="max-h-[60vh] overflow-y-auto px-1">{children}</div>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function DialogFooterRow({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
164
|
+
return <div className={className}>{children}</div>;
|
|
165
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { storedFields, optionLabel, type Resource } from "@flaredev/core";
|
|
2
|
-
import {
|
|
2
|
+
import { recentCounts, recordCount, trend, valueCounts } from "@/lib/dashboard";
|
|
3
3
|
import { resourceIcon } from "./resource-icon";
|
|
4
4
|
import { StatCards, type Stat } from "./stat-card";
|
|
5
5
|
|
|
@@ -31,19 +31,14 @@ export async function ResourceStats({ resource }: { resource: Resource }) {
|
|
|
31
31
|
if (statusField && total > 0) {
|
|
32
32
|
const [key, def] = statusField;
|
|
33
33
|
if (def.kind === "enum") {
|
|
34
|
-
const
|
|
34
|
+
const counts = await valueCounts(resource.name, key);
|
|
35
35
|
// Two options at most: a strip of eight cards is a wall, not a summary.
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const result = await store.list(new URLSearchParams({ perPage: "1", [`filter[${key}]`]: option }));
|
|
39
|
-
return { option, total: result.ok ? result.data.meta.total : 0 };
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
42
|
-
for (const { option, total: count } of counts) {
|
|
36
|
+
for (const option of def.options.slice(0, 2)) {
|
|
37
|
+
const value = counts[option] ?? 0;
|
|
43
38
|
stats.push({
|
|
44
39
|
label: optionLabel(def, option),
|
|
45
|
-
value
|
|
46
|
-
hint:
|
|
40
|
+
value,
|
|
41
|
+
hint: `${Math.round((value / total) * 100)}% of all`,
|
|
47
42
|
});
|
|
48
43
|
}
|
|
49
44
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
4
4
|
import { useState, type FormEvent } from "react";
|
|
5
5
|
import { SearchIcon, XIcon } from "lucide-react";
|
|
6
|
-
import { optionLabel, storedFields, type
|
|
6
|
+
import { optionLabel, storedFields, type ClientResource } from "@flaredev/core";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
|
|
9
9
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
@@ -12,7 +12,7 @@ import { hrefWith } from "./query";
|
|
|
12
12
|
const ALL = "__all__";
|
|
13
13
|
|
|
14
14
|
/** Search box and enum/boolean filters for ResourceTable; all state goes into the URL. */
|
|
15
|
-
export function ResourceTableToolbar({ resource }: { resource:
|
|
15
|
+
export function ResourceTableToolbar({ resource }: { resource: ClientResource }) {
|
|
16
16
|
const router = useRouter();
|
|
17
17
|
const pathname = usePathname();
|
|
18
18
|
const params = new URLSearchParams(useSearchParams().toString());
|