@maykonpaulo/maestro-admin 0.1.0-next.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/README.md +77 -0
- package/dist/index.d.ts +220 -0
- package/dist/index.js +663 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/src/styles.css +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @maykonpaulo/maestro-admin
|
|
2
|
+
|
|
3
|
+
The **generic, metadata-driven admin UI** for Maestro, per [ADR 0008 — Camada Turnkey](../../docs/adr/0008-turnkey-server-and-admin-ui.md).
|
|
4
|
+
|
|
5
|
+
Point it at a running [`@maykonpaulo/maestro-server`](../server) and it reads `GET /metadata` and **auto-builds the whole admin**: a navigation sidebar, a list table, a detail view and create/update forms **for every entity/collection** — no per-entity code. It respects each entity's `capabilities` (a read-only collection shows no New/Edit/Delete) and the server's RBAC (a 403 surfaces as an error, never a broken screen).
|
|
6
|
+
|
|
7
|
+
Built with **React 19 + Vite + Tailwind**. It ships two ways:
|
|
8
|
+
|
|
9
|
+
- a **runnable static app** (`vite build`) you configure with an env var, and
|
|
10
|
+
- **exported React components** (`import { MaestroAdmin } from '@maykonpaulo/maestro-admin'`) to embed in your own app.
|
|
11
|
+
|
|
12
|
+
It depends only on the Maestro **HTTP contract** — never on `@maykonpaulo/maestro-core` — so it stays fully decoupled from the engine.
|
|
13
|
+
|
|
14
|
+
## Run it as a standalone admin
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# point it at your maestro-server and start it
|
|
18
|
+
VITE_MAESTRO_API_URL=http://localhost:3000 pnpm --filter @maykonpaulo/maestro-admin dev
|
|
19
|
+
# or build the static bundle
|
|
20
|
+
VITE_MAESTRO_API_URL=http://localhost:3000 pnpm --filter @maykonpaulo/maestro-admin build:app
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`VITE_MAESTRO_API_URL` is the base URL of the server (defaults to same-origin). `VITE_MAESTRO_ROLE` optionally sets an `X-Role` header — handy against a dev server's `actorResolver`.
|
|
24
|
+
|
|
25
|
+
## Embed the component
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { MaestroAdmin } from '@maykonpaulo/maestro-admin';
|
|
29
|
+
import '@maykonpaulo/maestro-admin/src/styles.css'; // or include the package in your Tailwind content
|
|
30
|
+
|
|
31
|
+
export function AdminPage() {
|
|
32
|
+
return (
|
|
33
|
+
<MaestroAdmin
|
|
34
|
+
apiUrl="http://localhost:3000"
|
|
35
|
+
headers={() => ({ Authorization: `Bearer ${getToken()}` })}
|
|
36
|
+
title="Finans Admin"
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`headers` may be a function so a fresh auth token is read on every request. Pass a pre-built `client` (a `MaestroClient`) instead of `apiUrl`/`headers` for full control (custom `fetch`, SSR).
|
|
43
|
+
|
|
44
|
+
### Styling
|
|
45
|
+
|
|
46
|
+
Tailwind v4. Either import the package's source stylesheet (`@maykonpaulo/maestro-admin/src/styles.css`) or, if you already run Tailwind, add the package to your content so its class names are scanned:
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
/* your app.css */
|
|
50
|
+
@import 'tailwindcss';
|
|
51
|
+
@source '../node_modules/@maykonpaulo/maestro-admin/dist';
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Compose your own layout
|
|
55
|
+
|
|
56
|
+
Beyond the all-in-one `<MaestroAdmin>`, the building blocks are exported so you can assemble a custom shell:
|
|
57
|
+
|
|
58
|
+
- Components: `EntityList`, `EntityDetail`, `EntityForm`, `Sidebar`, `FieldValue`, `FieldInput`
|
|
59
|
+
- Hooks: `useMetadata`, `useEntityList`, `useEntityRecord`, `useAsync`, and `listFields`/`detailFields`/`formFields`
|
|
60
|
+
- Client: `MaestroClient`, `MaestroApiError`, `AdminClientProvider`, `useClient`
|
|
61
|
+
|
|
62
|
+
All of them talk to the server through a single `MaestroClient` provided via `AdminClientProvider`.
|
|
63
|
+
|
|
64
|
+
## What it renders from metadata
|
|
65
|
+
|
|
66
|
+
| Metadata | Drives |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `entities[]` | the sidebar navigation |
|
|
69
|
+
| `field.list.visible` / `order` | table columns |
|
|
70
|
+
| `field.detail.visible` / `order` | the detail view |
|
|
71
|
+
| `field.form.creatable` / `editable` / `order` | the create/update form fields |
|
|
72
|
+
| `field.type` | the right table cell and form control (checkbox, number, select, date, json, …) |
|
|
73
|
+
| `field.enumOptions` | select options |
|
|
74
|
+
| `field.sensitive` | masked values |
|
|
75
|
+
| `capabilities` | which of New / Edit / Delete appear |
|
|
76
|
+
|
|
77
|
+
Fields the server marks as E2EE-encrypted (a Finans reality) come across as opaque values — the admin shows them as-is; it never decrypts.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
type FieldType = 'string' | 'text' | 'integer' | 'float' | 'boolean' | 'date' | 'datetime' | 'enum' | 'json' | 'relation' | 'uuid' | 'email' | (string & {});
|
|
4
|
+
interface EnumOption {
|
|
5
|
+
value: string | number;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
interface FieldListConfig {
|
|
9
|
+
visible: boolean;
|
|
10
|
+
width?: number;
|
|
11
|
+
align?: 'left' | 'center' | 'right';
|
|
12
|
+
order?: number;
|
|
13
|
+
}
|
|
14
|
+
interface FieldFormConfig {
|
|
15
|
+
creatable: boolean;
|
|
16
|
+
editable: boolean;
|
|
17
|
+
cloneable: boolean;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
helpText?: string;
|
|
20
|
+
section?: string;
|
|
21
|
+
order?: number;
|
|
22
|
+
}
|
|
23
|
+
interface FieldDetailConfig {
|
|
24
|
+
visible: boolean;
|
|
25
|
+
group?: string;
|
|
26
|
+
order?: number;
|
|
27
|
+
}
|
|
28
|
+
interface FieldMetadata {
|
|
29
|
+
name: string;
|
|
30
|
+
label: string;
|
|
31
|
+
type: FieldType;
|
|
32
|
+
required: boolean;
|
|
33
|
+
readonly: boolean;
|
|
34
|
+
hidden: boolean;
|
|
35
|
+
sensitive: boolean;
|
|
36
|
+
searchable: boolean;
|
|
37
|
+
sortable: boolean;
|
|
38
|
+
filterable: boolean;
|
|
39
|
+
exportable: boolean;
|
|
40
|
+
description?: string;
|
|
41
|
+
enumOptions?: EnumOption[];
|
|
42
|
+
relationEntity?: string;
|
|
43
|
+
list: FieldListConfig;
|
|
44
|
+
detail: FieldDetailConfig;
|
|
45
|
+
form: FieldFormConfig;
|
|
46
|
+
}
|
|
47
|
+
interface EntityCapabilities {
|
|
48
|
+
list: boolean;
|
|
49
|
+
detail: boolean;
|
|
50
|
+
create: boolean;
|
|
51
|
+
update: boolean;
|
|
52
|
+
clone: boolean;
|
|
53
|
+
delete: boolean;
|
|
54
|
+
softDelete: boolean;
|
|
55
|
+
export: boolean;
|
|
56
|
+
bulkActions: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface EntityMetadata {
|
|
59
|
+
id: string;
|
|
60
|
+
label: {
|
|
61
|
+
singular: string;
|
|
62
|
+
plural: string;
|
|
63
|
+
};
|
|
64
|
+
description?: string;
|
|
65
|
+
datasource: string;
|
|
66
|
+
table: string;
|
|
67
|
+
primaryKey: string;
|
|
68
|
+
displayField: string;
|
|
69
|
+
fields: FieldMetadata[];
|
|
70
|
+
capabilities: EntityCapabilities;
|
|
71
|
+
relations: string[];
|
|
72
|
+
}
|
|
73
|
+
interface MaestroMetadata {
|
|
74
|
+
entities: EntityMetadata[];
|
|
75
|
+
}
|
|
76
|
+
type Record_ = globalThis.Record<string, unknown>;
|
|
77
|
+
interface ListResult {
|
|
78
|
+
records: Record_[];
|
|
79
|
+
total: number;
|
|
80
|
+
page?: number;
|
|
81
|
+
pageSize?: number;
|
|
82
|
+
totalPages?: number;
|
|
83
|
+
nextCursor?: string;
|
|
84
|
+
}
|
|
85
|
+
interface SortInput {
|
|
86
|
+
field: string;
|
|
87
|
+
direction: 'asc' | 'desc';
|
|
88
|
+
}
|
|
89
|
+
interface ListQuery {
|
|
90
|
+
page?: number;
|
|
91
|
+
pageSize?: number;
|
|
92
|
+
search?: string;
|
|
93
|
+
searchFields?: string[];
|
|
94
|
+
sort?: SortInput[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface MaestroClientConfig {
|
|
98
|
+
/** Base URL of the running maestro-server, e.g. `http://localhost:3000` or `/api/admin`. */
|
|
99
|
+
apiUrl: string;
|
|
100
|
+
/** Extra headers sent on every request — the place to inject an auth token (`Authorization`, `X-Role`, …). */
|
|
101
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
102
|
+
/** Custom fetch (SSR, tests). Defaults to the global `fetch`. */
|
|
103
|
+
fetch?: typeof fetch;
|
|
104
|
+
}
|
|
105
|
+
/** Thrown for any non-2xx response, carrying the HTTP status and the parsed error body when present. */
|
|
106
|
+
declare class MaestroApiError extends Error {
|
|
107
|
+
readonly status: number;
|
|
108
|
+
readonly code: string;
|
|
109
|
+
constructor(status: number, code: string, message: string);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A thin, framework-free client for the Maestro HTTP contract. Every admin component talks to the server
|
|
113
|
+
* through this — it is the single seam where auth headers, base URL and error handling live.
|
|
114
|
+
*/
|
|
115
|
+
declare class MaestroClient {
|
|
116
|
+
private readonly config;
|
|
117
|
+
private readonly base;
|
|
118
|
+
private readonly doFetch;
|
|
119
|
+
constructor(config: MaestroClientConfig);
|
|
120
|
+
private resolveHeaders;
|
|
121
|
+
private request;
|
|
122
|
+
getMetadata(): Promise<MaestroMetadata>;
|
|
123
|
+
getEntityMetadata(entityId: string): Promise<EntityMetadata>;
|
|
124
|
+
list(entityId: string, query?: ListQuery): Promise<ListResult>;
|
|
125
|
+
findById(entityId: string, id: string): Promise<Record_>;
|
|
126
|
+
create(entityId: string, data: Record_): Promise<Record_>;
|
|
127
|
+
update(entityId: string, id: string, data: Record_): Promise<Record_>;
|
|
128
|
+
remove(entityId: string, id: string): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface MaestroAdminProps {
|
|
132
|
+
/** Base URL of the running maestro-server. Ignored when `client` is given. */
|
|
133
|
+
apiUrl?: string;
|
|
134
|
+
/** Per-request headers (auth). Ignored when `client` is given. */
|
|
135
|
+
headers?: MaestroClientConfig['headers'];
|
|
136
|
+
/** A pre-built client, for full control (custom fetch, SSR, tests). */
|
|
137
|
+
client?: MaestroClient;
|
|
138
|
+
/** Sidebar title. Defaults to "Maestro Admin". */
|
|
139
|
+
title?: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The whole metadata-driven admin in one component. Point it at a running maestro-server and it reads
|
|
143
|
+
* `GET /metadata` and auto-builds navigation, tables, detail views and create/update forms for every
|
|
144
|
+
* entity — respecting each entity's capabilities and the server's RBAC. No per-entity code.
|
|
145
|
+
*/
|
|
146
|
+
declare function MaestroAdmin(props: MaestroAdminProps): react.JSX.Element;
|
|
147
|
+
|
|
148
|
+
declare const AdminClientProvider: react.Provider<MaestroClient | null>;
|
|
149
|
+
/** Returns the `MaestroClient` provided at the root of the admin. Throws if used outside the provider. */
|
|
150
|
+
declare function useClient(): MaestroClient;
|
|
151
|
+
|
|
152
|
+
interface AsyncState<T> {
|
|
153
|
+
data: T | undefined;
|
|
154
|
+
error: Error | undefined;
|
|
155
|
+
loading: boolean;
|
|
156
|
+
reload: () => void;
|
|
157
|
+
}
|
|
158
|
+
/** Runs an async producer, tracking loading/error and exposing a manual `reload`. Re-runs when `deps` change. */
|
|
159
|
+
declare function useAsync<T>(producer: () => Promise<T>, deps: readonly unknown[]): AsyncState<T>;
|
|
160
|
+
/** Loads the full metadata document (`GET /metadata`). */
|
|
161
|
+
declare function useMetadata(): AsyncState<MaestroMetadata>;
|
|
162
|
+
/** Loads a page of records for one entity. */
|
|
163
|
+
declare function useEntityList(entityId: string, query: ListQuery): AsyncState<ListResult>;
|
|
164
|
+
/** Loads a single record by id. */
|
|
165
|
+
declare function useEntityRecord(entityId: string, id: string | undefined): AsyncState<globalThis.Record<string, unknown>>;
|
|
166
|
+
/** Fields shown in the list table, ordered, honoring per-field list visibility. */
|
|
167
|
+
declare function listFields(entity: EntityMetadata): FieldMetadata[];
|
|
168
|
+
/** Fields shown in the detail view, ordered. */
|
|
169
|
+
declare function detailFields(entity: EntityMetadata): FieldMetadata[];
|
|
170
|
+
/** Fields editable in the create/update form, ordered, per mode. */
|
|
171
|
+
declare function formFields(entity: EntityMetadata, mode: 'create' | 'edit'): FieldMetadata[];
|
|
172
|
+
|
|
173
|
+
declare function EntityList({ entity, onOpen, onCreate, onEdit, }: {
|
|
174
|
+
entity: EntityMetadata;
|
|
175
|
+
onOpen: (id: string) => void;
|
|
176
|
+
onCreate: () => void;
|
|
177
|
+
onEdit: (id: string) => void;
|
|
178
|
+
}): react.JSX.Element;
|
|
179
|
+
|
|
180
|
+
declare function EntityDetail({ entity, id, onBack, onEdit, }: {
|
|
181
|
+
entity: EntityMetadata;
|
|
182
|
+
id: string;
|
|
183
|
+
onBack: () => void;
|
|
184
|
+
onEdit: () => void;
|
|
185
|
+
}): react.JSX.Element;
|
|
186
|
+
|
|
187
|
+
declare function EntityForm({ entity, mode, id, onCancel, onSaved, }: {
|
|
188
|
+
entity: EntityMetadata;
|
|
189
|
+
mode: 'create' | 'edit';
|
|
190
|
+
id?: string;
|
|
191
|
+
onCancel: () => void;
|
|
192
|
+
onSaved: (record: Record_) => void;
|
|
193
|
+
}): react.JSX.Element;
|
|
194
|
+
|
|
195
|
+
declare function Sidebar({ entities, activeId, onSelect, title, }: {
|
|
196
|
+
entities: EntityMetadata[];
|
|
197
|
+
activeId: string | undefined;
|
|
198
|
+
onSelect: (entityId: string) => void;
|
|
199
|
+
title: string;
|
|
200
|
+
}): react.JSX.Element;
|
|
201
|
+
|
|
202
|
+
/** Renders a single record value read-only, shaped by the field's type (for tables and detail views). */
|
|
203
|
+
declare function FieldValue({ field, value }: {
|
|
204
|
+
field: FieldMetadata;
|
|
205
|
+
value: unknown;
|
|
206
|
+
}): react.JSX.Element;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Renders the right form control for a field's type and reports its next value. Values are kept as strings
|
|
210
|
+
* for text/enum, numbers for numeric types, booleans for checkboxes, and parsed objects for json — matching
|
|
211
|
+
* what the create/update endpoints expect.
|
|
212
|
+
*/
|
|
213
|
+
declare function FieldInput({ field, value, onChange, disabled, }: {
|
|
214
|
+
field: FieldMetadata;
|
|
215
|
+
value: unknown;
|
|
216
|
+
onChange: (next: unknown) => void;
|
|
217
|
+
disabled?: boolean;
|
|
218
|
+
}): react.JSX.Element;
|
|
219
|
+
|
|
220
|
+
export { AdminClientProvider, type AsyncState, type EntityCapabilities, EntityDetail, EntityForm, EntityList, type EntityMetadata, FieldInput, type FieldMetadata, FieldValue, type ListQuery, type ListResult, MaestroAdmin, type MaestroAdminProps, MaestroApiError, MaestroClient, type MaestroClientConfig, type MaestroMetadata, Sidebar, detailFields, formFields, listFields, useAsync, useClient, useEntityList, useEntityRecord, useMetadata };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
// src/components/MaestroAdmin.tsx
|
|
2
|
+
import { useMemo as useMemo2, useState as useState4 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/client/MaestroClient.ts
|
|
5
|
+
var MaestroApiError = class extends Error {
|
|
6
|
+
constructor(status, code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "MaestroApiError";
|
|
11
|
+
}
|
|
12
|
+
status;
|
|
13
|
+
code;
|
|
14
|
+
};
|
|
15
|
+
function buildListParams(query = {}) {
|
|
16
|
+
const params = new URLSearchParams();
|
|
17
|
+
if (query.page !== void 0) params.set("page", String(query.page));
|
|
18
|
+
if (query.pageSize !== void 0) params.set("pageSize", String(query.pageSize));
|
|
19
|
+
if (query.search) {
|
|
20
|
+
params.set("search", query.search);
|
|
21
|
+
if (query.searchFields?.length) params.set("searchFields", query.searchFields.join(","));
|
|
22
|
+
}
|
|
23
|
+
for (const s of query.sort ?? []) params.append("sort", `${s.field}:${s.direction}`);
|
|
24
|
+
const qs = params.toString();
|
|
25
|
+
return qs ? `?${qs}` : "";
|
|
26
|
+
}
|
|
27
|
+
var MaestroClient = class {
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.base = config.apiUrl.replace(/\/$/, "");
|
|
31
|
+
this.doFetch = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
32
|
+
}
|
|
33
|
+
config;
|
|
34
|
+
base;
|
|
35
|
+
doFetch;
|
|
36
|
+
resolveHeaders() {
|
|
37
|
+
const h = typeof this.config.headers === "function" ? this.config.headers() : this.config.headers;
|
|
38
|
+
return { ...h };
|
|
39
|
+
}
|
|
40
|
+
async request(path, init) {
|
|
41
|
+
const res = await this.doFetch(`${this.base}${path}`, {
|
|
42
|
+
...init,
|
|
43
|
+
headers: {
|
|
44
|
+
Accept: "application/json",
|
|
45
|
+
...init?.body ? { "Content-Type": "application/json" } : {},
|
|
46
|
+
...this.resolveHeaders(),
|
|
47
|
+
...init?.headers
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (res.status === 204) return void 0;
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
const body = text ? JSON.parse(text) : void 0;
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
const err = body;
|
|
55
|
+
throw new MaestroApiError(res.status, err?.error ?? "ERROR", err?.message ?? res.statusText);
|
|
56
|
+
}
|
|
57
|
+
return body;
|
|
58
|
+
}
|
|
59
|
+
async getMetadata() {
|
|
60
|
+
return this.request("/metadata");
|
|
61
|
+
}
|
|
62
|
+
async getEntityMetadata(entityId) {
|
|
63
|
+
return this.request(`/metadata/${encodeURIComponent(entityId)}`);
|
|
64
|
+
}
|
|
65
|
+
async list(entityId, query) {
|
|
66
|
+
return this.request(`/entities/${encodeURIComponent(entityId)}${buildListParams(query)}`);
|
|
67
|
+
}
|
|
68
|
+
async findById(entityId, id) {
|
|
69
|
+
return this.request(`/entities/${encodeURIComponent(entityId)}/${encodeURIComponent(id)}`);
|
|
70
|
+
}
|
|
71
|
+
async create(entityId, data) {
|
|
72
|
+
return this.request(`/entities/${encodeURIComponent(entityId)}`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
body: JSON.stringify(data)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async update(entityId, id, data) {
|
|
78
|
+
return this.request(`/entities/${encodeURIComponent(entityId)}/${encodeURIComponent(id)}`, {
|
|
79
|
+
method: "PATCH",
|
|
80
|
+
body: JSON.stringify(data)
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async remove(entityId, id) {
|
|
84
|
+
await this.request(`/entities/${encodeURIComponent(entityId)}/${encodeURIComponent(id)}`, {
|
|
85
|
+
method: "DELETE"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/AdminContext.tsx
|
|
91
|
+
import { createContext, useContext } from "react";
|
|
92
|
+
var ClientContext = createContext(null);
|
|
93
|
+
var AdminClientProvider = ClientContext.Provider;
|
|
94
|
+
function useClient() {
|
|
95
|
+
const client = useContext(ClientContext);
|
|
96
|
+
if (!client) {
|
|
97
|
+
throw new Error("useClient must be used within a <MaestroAdmin> (no MaestroClient in context).");
|
|
98
|
+
}
|
|
99
|
+
return client;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/hooks.ts
|
|
103
|
+
import { useCallback, useEffect, useState } from "react";
|
|
104
|
+
function useAsync(producer, deps) {
|
|
105
|
+
const [data, setData] = useState(void 0);
|
|
106
|
+
const [error, setError] = useState(void 0);
|
|
107
|
+
const [loading, setLoading] = useState(true);
|
|
108
|
+
const [tick, setTick] = useState(0);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
let cancelled = false;
|
|
111
|
+
setLoading(true);
|
|
112
|
+
setError(void 0);
|
|
113
|
+
producer().then((result) => {
|
|
114
|
+
if (!cancelled) setData(result);
|
|
115
|
+
}).catch((err) => {
|
|
116
|
+
if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
|
|
117
|
+
}).finally(() => {
|
|
118
|
+
if (!cancelled) setLoading(false);
|
|
119
|
+
});
|
|
120
|
+
return () => {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
};
|
|
123
|
+
}, [...deps, tick]);
|
|
124
|
+
const reload = useCallback(() => setTick((t) => t + 1), []);
|
|
125
|
+
return { data, error, loading, reload };
|
|
126
|
+
}
|
|
127
|
+
function useMetadata() {
|
|
128
|
+
const client = useClient();
|
|
129
|
+
return useAsync(() => client.getMetadata(), []);
|
|
130
|
+
}
|
|
131
|
+
function useEntityList(entityId, query) {
|
|
132
|
+
const client = useClient();
|
|
133
|
+
const key = JSON.stringify(query);
|
|
134
|
+
return useAsync(() => client.list(entityId, query), [entityId, key]);
|
|
135
|
+
}
|
|
136
|
+
function useEntityRecord(entityId, id) {
|
|
137
|
+
const client = useClient();
|
|
138
|
+
return useAsync(async () => id ? client.findById(entityId, id) : {}, [entityId, id]);
|
|
139
|
+
}
|
|
140
|
+
function listFields(entity) {
|
|
141
|
+
return entity.fields.filter((f) => f.list.visible && !f.hidden).sort((a, b) => (a.list.order ?? 0) - (b.list.order ?? 0));
|
|
142
|
+
}
|
|
143
|
+
function detailFields(entity) {
|
|
144
|
+
return entity.fields.filter((f) => f.detail.visible).sort((a, b) => (a.detail.order ?? 0) - (b.detail.order ?? 0));
|
|
145
|
+
}
|
|
146
|
+
function formFields(entity, mode) {
|
|
147
|
+
return entity.fields.filter((f) => mode === "create" ? f.form.creatable : f.form.editable).sort((a, b) => (a.form.order ?? 0) - (b.form.order ?? 0));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/components/Sidebar.tsx
|
|
151
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
152
|
+
function Sidebar({
|
|
153
|
+
entities,
|
|
154
|
+
activeId,
|
|
155
|
+
onSelect,
|
|
156
|
+
title
|
|
157
|
+
}) {
|
|
158
|
+
return /* @__PURE__ */ jsxs("aside", { className: "flex w-60 shrink-0 flex-col border-r border-slate-200 bg-slate-50", children: [
|
|
159
|
+
/* @__PURE__ */ jsx("div", { className: "border-b border-slate-200 px-4 py-4", children: /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold tracking-tight text-slate-900", children: title }) }),
|
|
160
|
+
/* @__PURE__ */ jsx("nav", { className: "flex-1 overflow-y-auto p-2", children: entities.map((e) => /* @__PURE__ */ jsx(
|
|
161
|
+
"button",
|
|
162
|
+
{
|
|
163
|
+
onClick: () => onSelect(e.id),
|
|
164
|
+
className: `flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm transition-colors ${e.id === activeId ? "bg-indigo-100 font-medium text-indigo-700" : "text-slate-700 hover:bg-slate-100"}`,
|
|
165
|
+
children: /* @__PURE__ */ jsx("span", { className: "truncate", children: e.label.plural })
|
|
166
|
+
},
|
|
167
|
+
e.id
|
|
168
|
+
)) }),
|
|
169
|
+
/* @__PURE__ */ jsxs("div", { className: "border-t border-slate-200 px-4 py-3 text-xs text-slate-400", children: [
|
|
170
|
+
entities.length,
|
|
171
|
+
" entities \xB7 Maestro"
|
|
172
|
+
] })
|
|
173
|
+
] });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/components/EntityList.tsx
|
|
177
|
+
import { useMemo, useState as useState2 } from "react";
|
|
178
|
+
|
|
179
|
+
// src/components/ui.tsx
|
|
180
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
181
|
+
var VARIANTS = {
|
|
182
|
+
primary: "bg-indigo-600 text-white hover:bg-indigo-500 disabled:bg-indigo-300",
|
|
183
|
+
secondary: "bg-white text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50",
|
|
184
|
+
danger: "bg-red-600 text-white hover:bg-red-500 disabled:bg-red-300",
|
|
185
|
+
ghost: "text-slate-600 hover:bg-slate-100"
|
|
186
|
+
};
|
|
187
|
+
function Button({
|
|
188
|
+
variant = "secondary",
|
|
189
|
+
className = "",
|
|
190
|
+
...props
|
|
191
|
+
}) {
|
|
192
|
+
return /* @__PURE__ */ jsx2(
|
|
193
|
+
"button",
|
|
194
|
+
{
|
|
195
|
+
className: `inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed ${VARIANTS[variant]} ${className}`,
|
|
196
|
+
...props
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
function Badge({ children, tone = "slate" }) {
|
|
201
|
+
const tones = {
|
|
202
|
+
slate: "bg-slate-100 text-slate-700",
|
|
203
|
+
green: "bg-green-100 text-green-700",
|
|
204
|
+
red: "bg-red-100 text-red-700",
|
|
205
|
+
amber: "bg-amber-100 text-amber-700",
|
|
206
|
+
indigo: "bg-indigo-100 text-indigo-700"
|
|
207
|
+
};
|
|
208
|
+
return /* @__PURE__ */ jsx2("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${tones[tone]}`, children });
|
|
209
|
+
}
|
|
210
|
+
function Spinner({ label }) {
|
|
211
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-sm text-slate-500", role: "status", children: [
|
|
212
|
+
/* @__PURE__ */ jsx2("span", { className: "h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-indigo-600" }),
|
|
213
|
+
label ?? "Loading\u2026"
|
|
214
|
+
] });
|
|
215
|
+
}
|
|
216
|
+
function EmptyState({ title, hint }) {
|
|
217
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rounded-lg border border-dashed border-slate-300 p-8 text-center", children: [
|
|
218
|
+
/* @__PURE__ */ jsx2("p", { className: "text-sm font-medium text-slate-700", children: title }),
|
|
219
|
+
hint && /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-slate-500", children: hint })
|
|
220
|
+
] });
|
|
221
|
+
}
|
|
222
|
+
function ErrorBanner({ error }) {
|
|
223
|
+
return /* @__PURE__ */ jsx2("div", { className: "rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700", role: "alert", children: error.message });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/components/FieldValue.tsx
|
|
227
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
228
|
+
function enumLabel(field, value) {
|
|
229
|
+
const opt = field.enumOptions?.find((o) => o.value === value);
|
|
230
|
+
return opt ? opt.label : String(value);
|
|
231
|
+
}
|
|
232
|
+
function FieldValue({ field, value }) {
|
|
233
|
+
if (value === null || value === void 0 || value === "") {
|
|
234
|
+
return /* @__PURE__ */ jsx3("span", { className: "text-slate-400", children: "\u2014" });
|
|
235
|
+
}
|
|
236
|
+
if (field.sensitive) {
|
|
237
|
+
return /* @__PURE__ */ jsx3("span", { className: "text-slate-400", children: "\u2022\u2022\u2022\u2022\u2022\u2022" });
|
|
238
|
+
}
|
|
239
|
+
switch (field.type) {
|
|
240
|
+
case "boolean":
|
|
241
|
+
return value ? /* @__PURE__ */ jsx3(Badge, { tone: "green", children: "true" }) : /* @__PURE__ */ jsx3(Badge, { tone: "slate", children: "false" });
|
|
242
|
+
case "enum":
|
|
243
|
+
return /* @__PURE__ */ jsx3(Badge, { tone: "indigo", children: enumLabel(field, value) });
|
|
244
|
+
case "date":
|
|
245
|
+
case "datetime": {
|
|
246
|
+
const d = new Date(String(value));
|
|
247
|
+
return /* @__PURE__ */ jsx3("span", { children: Number.isNaN(d.getTime()) ? String(value) : d.toLocaleString() });
|
|
248
|
+
}
|
|
249
|
+
case "json":
|
|
250
|
+
return /* @__PURE__ */ jsx3("code", { className: "block max-w-md overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700", children: JSON.stringify(value) });
|
|
251
|
+
default:
|
|
252
|
+
return /* @__PURE__ */ jsx3("span", { className: "text-slate-700", children: String(value) });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/components/EntityList.tsx
|
|
257
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
258
|
+
var PAGE_SIZE = 20;
|
|
259
|
+
function EntityList({
|
|
260
|
+
entity,
|
|
261
|
+
onOpen,
|
|
262
|
+
onCreate,
|
|
263
|
+
onEdit
|
|
264
|
+
}) {
|
|
265
|
+
const client = useClient();
|
|
266
|
+
const columns = useMemo(() => listFields(entity), [entity]);
|
|
267
|
+
const searchFields = useMemo(() => entity.fields.filter((f) => f.searchable).map((f) => f.name), [entity]);
|
|
268
|
+
const [page, setPage] = useState2(1);
|
|
269
|
+
const [search, setSearch] = useState2("");
|
|
270
|
+
const [sort, setSort] = useState2(void 0);
|
|
271
|
+
const query = {
|
|
272
|
+
page,
|
|
273
|
+
pageSize: PAGE_SIZE,
|
|
274
|
+
search: search || void 0,
|
|
275
|
+
searchFields,
|
|
276
|
+
sort: sort ? [sort] : void 0
|
|
277
|
+
};
|
|
278
|
+
const { data, error, loading, reload } = useEntityList(entity.id, query);
|
|
279
|
+
const toggleSort = (fieldName) => {
|
|
280
|
+
setSort(
|
|
281
|
+
(prev) => prev?.field === fieldName ? { field: fieldName, direction: prev.direction === "asc" ? "desc" : "asc" } : { field: fieldName, direction: "asc" }
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
const onDelete = async (id) => {
|
|
285
|
+
if (!globalThis.confirm(`Delete this ${entity.label.singular.toLowerCase()}? This cannot be undone.`)) return;
|
|
286
|
+
await client.remove(entity.id, id);
|
|
287
|
+
reload();
|
|
288
|
+
};
|
|
289
|
+
const totalPages = data?.totalPages ?? (data ? Math.max(1, Math.ceil(data.total / PAGE_SIZE)) : 1);
|
|
290
|
+
return /* @__PURE__ */ jsxs3("section", { className: "space-y-4", children: [
|
|
291
|
+
/* @__PURE__ */ jsxs3("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
292
|
+
/* @__PURE__ */ jsxs3("div", { children: [
|
|
293
|
+
/* @__PURE__ */ jsx4("h1", { className: "text-lg font-semibold text-slate-900", children: entity.label.plural }),
|
|
294
|
+
data && /* @__PURE__ */ jsxs3("p", { className: "text-sm text-slate-500", children: [
|
|
295
|
+
data.total,
|
|
296
|
+
" records"
|
|
297
|
+
] })
|
|
298
|
+
] }),
|
|
299
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
300
|
+
searchFields.length > 0 && /* @__PURE__ */ jsx4(
|
|
301
|
+
"input",
|
|
302
|
+
{
|
|
303
|
+
type: "search",
|
|
304
|
+
placeholder: `Search ${entity.label.plural.toLowerCase()}\u2026`,
|
|
305
|
+
value: search,
|
|
306
|
+
onChange: (e) => {
|
|
307
|
+
setSearch(e.target.value);
|
|
308
|
+
setPage(1);
|
|
309
|
+
},
|
|
310
|
+
className: "rounded-md border border-slate-300 px-3 py-1.5 text-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
311
|
+
}
|
|
312
|
+
),
|
|
313
|
+
entity.capabilities.create && /* @__PURE__ */ jsx4(Button, { variant: "primary", onClick: onCreate, children: "+ New" })
|
|
314
|
+
] })
|
|
315
|
+
] }),
|
|
316
|
+
error && /* @__PURE__ */ jsx4(ErrorBanner, { error }),
|
|
317
|
+
loading && !data ? /* @__PURE__ */ jsx4(Spinner, { label: `Loading ${entity.label.plural.toLowerCase()}\u2026` }) : data && data.records.length === 0 ? /* @__PURE__ */ jsx4(EmptyState, { title: "No records", hint: search ? "Try a different search." : void 0 }) : /* @__PURE__ */ jsx4("div", { className: "overflow-x-auto rounded-lg border border-slate-200", children: /* @__PURE__ */ jsxs3("table", { className: "min-w-full divide-y divide-slate-200 text-sm", children: [
|
|
318
|
+
/* @__PURE__ */ jsx4("thead", { className: "bg-slate-50", children: /* @__PURE__ */ jsxs3("tr", { children: [
|
|
319
|
+
columns.map((f) => /* @__PURE__ */ jsxs3(
|
|
320
|
+
"th",
|
|
321
|
+
{
|
|
322
|
+
onClick: () => f.sortable && toggleSort(f.name),
|
|
323
|
+
className: `px-3 py-2 text-left font-medium text-slate-600 ${f.sortable ? "cursor-pointer select-none hover:text-slate-900" : ""}`,
|
|
324
|
+
children: [
|
|
325
|
+
f.label,
|
|
326
|
+
sort?.field === f.name && (sort.direction === "asc" ? " \u25B2" : " \u25BC")
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
f.name
|
|
330
|
+
)),
|
|
331
|
+
/* @__PURE__ */ jsx4("th", { className: "px-3 py-2" })
|
|
332
|
+
] }) }),
|
|
333
|
+
/* @__PURE__ */ jsx4("tbody", { className: "divide-y divide-slate-100 bg-white", children: data?.records.map((record, i) => {
|
|
334
|
+
const id = String(record[entity.primaryKey]);
|
|
335
|
+
return /* @__PURE__ */ jsxs3("tr", { className: "hover:bg-slate-50", children: [
|
|
336
|
+
columns.map((f) => /* @__PURE__ */ jsx4(
|
|
337
|
+
"td",
|
|
338
|
+
{
|
|
339
|
+
className: "cursor-pointer px-3 py-2 align-top",
|
|
340
|
+
onClick: () => onOpen(id),
|
|
341
|
+
children: /* @__PURE__ */ jsx4(FieldValue, { field: f, value: record[f.name] })
|
|
342
|
+
},
|
|
343
|
+
f.name
|
|
344
|
+
)),
|
|
345
|
+
/* @__PURE__ */ jsxs3("td", { className: "whitespace-nowrap px-3 py-2 text-right", children: [
|
|
346
|
+
entity.capabilities.update && /* @__PURE__ */ jsx4(Button, { variant: "ghost", onClick: () => onEdit(id), children: "Edit" }),
|
|
347
|
+
entity.capabilities.delete && /* @__PURE__ */ jsx4(Button, { variant: "ghost", className: "text-red-600", onClick: () => onDelete(id), children: "Delete" })
|
|
348
|
+
] })
|
|
349
|
+
] }, id || i);
|
|
350
|
+
}) })
|
|
351
|
+
] }) }),
|
|
352
|
+
data && totalPages > 1 && /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-end gap-2 text-sm", children: [
|
|
353
|
+
/* @__PURE__ */ jsx4(Button, { variant: "secondary", disabled: page <= 1, onClick: () => setPage((p) => p - 1), children: "Previous" }),
|
|
354
|
+
/* @__PURE__ */ jsxs3("span", { className: "text-slate-500", children: [
|
|
355
|
+
"Page ",
|
|
356
|
+
page,
|
|
357
|
+
" of ",
|
|
358
|
+
totalPages
|
|
359
|
+
] }),
|
|
360
|
+
/* @__PURE__ */ jsx4(Button, { variant: "secondary", disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), children: "Next" })
|
|
361
|
+
] })
|
|
362
|
+
] });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/components/EntityDetail.tsx
|
|
366
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
367
|
+
function EntityDetail({
|
|
368
|
+
entity,
|
|
369
|
+
id,
|
|
370
|
+
onBack,
|
|
371
|
+
onEdit
|
|
372
|
+
}) {
|
|
373
|
+
const { data, error, loading } = useEntityRecord(entity.id, id);
|
|
374
|
+
const fields = detailFields(entity);
|
|
375
|
+
return /* @__PURE__ */ jsxs4("section", { className: "space-y-4", children: [
|
|
376
|
+
/* @__PURE__ */ jsxs4("header", { className: "flex items-center justify-between gap-3", children: [
|
|
377
|
+
/* @__PURE__ */ jsxs4("div", { children: [
|
|
378
|
+
/* @__PURE__ */ jsxs4("button", { onClick: onBack, className: "text-sm text-indigo-600 hover:underline", children: [
|
|
379
|
+
"\u2190 ",
|
|
380
|
+
entity.label.plural
|
|
381
|
+
] }),
|
|
382
|
+
/* @__PURE__ */ jsxs4("h1", { className: "text-lg font-semibold text-slate-900", children: [
|
|
383
|
+
entity.label.singular,
|
|
384
|
+
" ",
|
|
385
|
+
data ? String(data[entity.displayField] ?? id) : ""
|
|
386
|
+
] })
|
|
387
|
+
] }),
|
|
388
|
+
entity.capabilities.update && /* @__PURE__ */ jsx5(Button, { variant: "primary", onClick: onEdit, children: "Edit" })
|
|
389
|
+
] }),
|
|
390
|
+
error && /* @__PURE__ */ jsx5(ErrorBanner, { error }),
|
|
391
|
+
loading && !data ? /* @__PURE__ */ jsx5(Spinner, {}) : data && /* @__PURE__ */ jsx5("dl", { className: "divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white", children: fields.map((f) => /* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-3 gap-4 px-4 py-3", children: [
|
|
392
|
+
/* @__PURE__ */ jsx5("dt", { className: "text-sm font-medium text-slate-500", children: f.label }),
|
|
393
|
+
/* @__PURE__ */ jsx5("dd", { className: "col-span-2 text-sm", children: /* @__PURE__ */ jsx5(FieldValue, { field: f, value: data[f.name] }) })
|
|
394
|
+
] }, f.name)) })
|
|
395
|
+
] });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/components/EntityForm.tsx
|
|
399
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
400
|
+
|
|
401
|
+
// src/components/FieldInput.tsx
|
|
402
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
403
|
+
var inputClass = "w-full rounded-md border border-slate-300 px-3 py-1.5 text-sm text-slate-800 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:bg-slate-100";
|
|
404
|
+
function FieldInput({
|
|
405
|
+
field,
|
|
406
|
+
value,
|
|
407
|
+
onChange,
|
|
408
|
+
disabled
|
|
409
|
+
}) {
|
|
410
|
+
const id = `field-${field.name}`;
|
|
411
|
+
switch (field.type) {
|
|
412
|
+
case "boolean":
|
|
413
|
+
return /* @__PURE__ */ jsx6(
|
|
414
|
+
"input",
|
|
415
|
+
{
|
|
416
|
+
id,
|
|
417
|
+
type: "checkbox",
|
|
418
|
+
checked: Boolean(value),
|
|
419
|
+
disabled,
|
|
420
|
+
onChange: (e) => onChange(e.target.checked),
|
|
421
|
+
className: "h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
case "integer":
|
|
425
|
+
case "number":
|
|
426
|
+
case "float":
|
|
427
|
+
case "decimal":
|
|
428
|
+
case "currency":
|
|
429
|
+
return /* @__PURE__ */ jsx6(
|
|
430
|
+
"input",
|
|
431
|
+
{
|
|
432
|
+
id,
|
|
433
|
+
type: "number",
|
|
434
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
435
|
+
placeholder: field.form.placeholder,
|
|
436
|
+
disabled,
|
|
437
|
+
step: field.type === "integer" ? 1 : "any",
|
|
438
|
+
onChange: (e) => onChange(e.target.value === "" ? null : Number(e.target.value)),
|
|
439
|
+
className: inputClass
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
case "enum":
|
|
443
|
+
return /* @__PURE__ */ jsxs5(
|
|
444
|
+
"select",
|
|
445
|
+
{
|
|
446
|
+
id,
|
|
447
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
448
|
+
disabled,
|
|
449
|
+
onChange: (e) => onChange(e.target.value),
|
|
450
|
+
className: inputClass,
|
|
451
|
+
children: [
|
|
452
|
+
/* @__PURE__ */ jsx6("option", { value: "", children: "\u2014" }),
|
|
453
|
+
field.enumOptions?.map((o) => /* @__PURE__ */ jsx6("option", { value: String(o.value), children: o.label }, String(o.value)))
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
case "date":
|
|
458
|
+
return /* @__PURE__ */ jsx6(
|
|
459
|
+
"input",
|
|
460
|
+
{
|
|
461
|
+
id,
|
|
462
|
+
type: "date",
|
|
463
|
+
value: value ? String(value).slice(0, 10) : "",
|
|
464
|
+
disabled,
|
|
465
|
+
onChange: (e) => onChange(e.target.value || null),
|
|
466
|
+
className: inputClass
|
|
467
|
+
}
|
|
468
|
+
);
|
|
469
|
+
case "text":
|
|
470
|
+
case "json":
|
|
471
|
+
return /* @__PURE__ */ jsx6(
|
|
472
|
+
"textarea",
|
|
473
|
+
{
|
|
474
|
+
id,
|
|
475
|
+
rows: field.type === "json" ? 5 : 3,
|
|
476
|
+
value: field.type === "json" && value !== null && typeof value === "object" ? JSON.stringify(value, null, 2) : value === null || value === void 0 ? "" : String(value),
|
|
477
|
+
placeholder: field.form.placeholder,
|
|
478
|
+
disabled,
|
|
479
|
+
onChange: (e) => {
|
|
480
|
+
if (field.type === "json") {
|
|
481
|
+
try {
|
|
482
|
+
onChange(e.target.value ? JSON.parse(e.target.value) : null);
|
|
483
|
+
} catch {
|
|
484
|
+
onChange(e.target.value);
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
onChange(e.target.value);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
className: `${inputClass} font-mono`
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
default:
|
|
494
|
+
return /* @__PURE__ */ jsx6(
|
|
495
|
+
"input",
|
|
496
|
+
{
|
|
497
|
+
id,
|
|
498
|
+
type: field.type === "email" ? "email" : "text",
|
|
499
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
500
|
+
placeholder: field.form.placeholder,
|
|
501
|
+
disabled,
|
|
502
|
+
onChange: (e) => onChange(e.target.value),
|
|
503
|
+
className: inputClass
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/components/EntityForm.tsx
|
|
510
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
511
|
+
function EntityForm({
|
|
512
|
+
entity,
|
|
513
|
+
mode,
|
|
514
|
+
id,
|
|
515
|
+
onCancel,
|
|
516
|
+
onSaved
|
|
517
|
+
}) {
|
|
518
|
+
const client = useClient();
|
|
519
|
+
const fields = formFields(entity, mode);
|
|
520
|
+
const existing = useEntityRecord(entity.id, mode === "edit" ? id : void 0);
|
|
521
|
+
const [values, setValues] = useState3({});
|
|
522
|
+
const [saving, setSaving] = useState3(false);
|
|
523
|
+
const [error, setError] = useState3(void 0);
|
|
524
|
+
useEffect2(() => {
|
|
525
|
+
if (mode === "edit" && existing.data) setValues(existing.data);
|
|
526
|
+
}, [mode, existing.data]);
|
|
527
|
+
const submit = async (e) => {
|
|
528
|
+
e.preventDefault();
|
|
529
|
+
setSaving(true);
|
|
530
|
+
setError(void 0);
|
|
531
|
+
try {
|
|
532
|
+
const payload = {};
|
|
533
|
+
for (const f of fields) {
|
|
534
|
+
if (values[f.name] !== void 0) payload[f.name] = values[f.name];
|
|
535
|
+
}
|
|
536
|
+
const saved = mode === "create" ? await client.create(entity.id, payload) : await client.update(entity.id, id, payload);
|
|
537
|
+
onSaved(saved);
|
|
538
|
+
} catch (err) {
|
|
539
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
540
|
+
} finally {
|
|
541
|
+
setSaving(false);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
if (mode === "edit" && existing.loading && !existing.data) {
|
|
545
|
+
return /* @__PURE__ */ jsx7(Spinner, {});
|
|
546
|
+
}
|
|
547
|
+
return /* @__PURE__ */ jsxs6("section", { className: "max-w-2xl space-y-4", children: [
|
|
548
|
+
/* @__PURE__ */ jsxs6("header", { children: [
|
|
549
|
+
/* @__PURE__ */ jsxs6("button", { onClick: onCancel, className: "text-sm text-indigo-600 hover:underline", children: [
|
|
550
|
+
"\u2190 ",
|
|
551
|
+
entity.label.plural
|
|
552
|
+
] }),
|
|
553
|
+
/* @__PURE__ */ jsx7("h1", { className: "text-lg font-semibold text-slate-900", children: mode === "create" ? `New ${entity.label.singular}` : `Edit ${entity.label.singular}` })
|
|
554
|
+
] }),
|
|
555
|
+
error && /* @__PURE__ */ jsx7(ErrorBanner, { error }),
|
|
556
|
+
/* @__PURE__ */ jsxs6("form", { onSubmit: submit, className: "space-y-4 rounded-lg border border-slate-200 bg-white p-5", children: [
|
|
557
|
+
fields.map((f) => /* @__PURE__ */ jsxs6("div", { className: "space-y-1", children: [
|
|
558
|
+
/* @__PURE__ */ jsxs6("label", { htmlFor: `field-${f.name}`, className: "block text-sm font-medium text-slate-700", children: [
|
|
559
|
+
f.label,
|
|
560
|
+
f.required && /* @__PURE__ */ jsx7("span", { className: "ml-0.5 text-red-500", children: "*" })
|
|
561
|
+
] }),
|
|
562
|
+
/* @__PURE__ */ jsx7(
|
|
563
|
+
FieldInput,
|
|
564
|
+
{
|
|
565
|
+
field: f,
|
|
566
|
+
value: values[f.name],
|
|
567
|
+
disabled: saving || f.readonly,
|
|
568
|
+
onChange: (next) => setValues((v) => ({ ...v, [f.name]: next }))
|
|
569
|
+
}
|
|
570
|
+
),
|
|
571
|
+
f.form.helpText && /* @__PURE__ */ jsx7("p", { className: "text-xs text-slate-500", children: f.form.helpText })
|
|
572
|
+
] }, f.name)),
|
|
573
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex justify-end gap-2 border-t border-slate-100 pt-4", children: [
|
|
574
|
+
/* @__PURE__ */ jsx7(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: saving, children: "Cancel" }),
|
|
575
|
+
/* @__PURE__ */ jsx7(Button, { type: "submit", variant: "primary", disabled: saving, children: saving ? "Saving\u2026" : mode === "create" ? "Create" : "Save" })
|
|
576
|
+
] })
|
|
577
|
+
] })
|
|
578
|
+
] });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/components/MaestroAdmin.tsx
|
|
582
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
583
|
+
function AdminShell({ title }) {
|
|
584
|
+
const { data, error, loading } = useMetadata();
|
|
585
|
+
const [view, setView] = useState4(void 0);
|
|
586
|
+
const entities = data?.entities ?? [];
|
|
587
|
+
const activeId = view?.entityId ?? entities[0]?.id;
|
|
588
|
+
const activeEntity = entities.find((e) => e.id === activeId);
|
|
589
|
+
const effectiveView = view ?? (activeId ? { kind: "list", entityId: activeId } : void 0);
|
|
590
|
+
if (loading && !data) {
|
|
591
|
+
return /* @__PURE__ */ jsx8("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx8(Spinner, { label: "Loading metadata\u2026" }) });
|
|
592
|
+
}
|
|
593
|
+
return /* @__PURE__ */ jsxs7("div", { className: "flex h-full min-h-screen bg-white text-slate-800", children: [
|
|
594
|
+
/* @__PURE__ */ jsx8(
|
|
595
|
+
Sidebar,
|
|
596
|
+
{
|
|
597
|
+
entities,
|
|
598
|
+
activeId,
|
|
599
|
+
title,
|
|
600
|
+
onSelect: (entityId) => setView({ kind: "list", entityId })
|
|
601
|
+
}
|
|
602
|
+
),
|
|
603
|
+
/* @__PURE__ */ jsxs7("main", { className: "flex-1 overflow-y-auto p-6", children: [
|
|
604
|
+
error && /* @__PURE__ */ jsx8(ErrorBanner, { error }),
|
|
605
|
+
activeEntity && effectiveView && effectiveView.kind === "list" && /* @__PURE__ */ jsx8(
|
|
606
|
+
EntityList,
|
|
607
|
+
{
|
|
608
|
+
entity: activeEntity,
|
|
609
|
+
onOpen: (id) => setView({ kind: "detail", entityId: activeEntity.id, id }),
|
|
610
|
+
onCreate: () => setView({ kind: "create", entityId: activeEntity.id }),
|
|
611
|
+
onEdit: (id) => setView({ kind: "edit", entityId: activeEntity.id, id })
|
|
612
|
+
}
|
|
613
|
+
),
|
|
614
|
+
activeEntity && effectiveView?.kind === "detail" && /* @__PURE__ */ jsx8(
|
|
615
|
+
EntityDetail,
|
|
616
|
+
{
|
|
617
|
+
entity: activeEntity,
|
|
618
|
+
id: effectiveView.id,
|
|
619
|
+
onBack: () => setView({ kind: "list", entityId: activeEntity.id }),
|
|
620
|
+
onEdit: () => setView({ kind: "edit", entityId: activeEntity.id, id: effectiveView.id })
|
|
621
|
+
}
|
|
622
|
+
),
|
|
623
|
+
activeEntity && (effectiveView?.kind === "create" || effectiveView?.kind === "edit") && /* @__PURE__ */ jsx8(
|
|
624
|
+
EntityForm,
|
|
625
|
+
{
|
|
626
|
+
entity: activeEntity,
|
|
627
|
+
mode: effectiveView.kind,
|
|
628
|
+
id: effectiveView.kind === "edit" ? effectiveView.id : void 0,
|
|
629
|
+
onCancel: () => setView({ kind: "list", entityId: activeEntity.id }),
|
|
630
|
+
onSaved: () => setView({ kind: "list", entityId: activeEntity.id })
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
] })
|
|
634
|
+
] });
|
|
635
|
+
}
|
|
636
|
+
function MaestroAdmin(props) {
|
|
637
|
+
const client = useMemo2(
|
|
638
|
+
() => props.client ?? new MaestroClient({ apiUrl: props.apiUrl ?? "", headers: props.headers }),
|
|
639
|
+
[props.client, props.apiUrl, props.headers]
|
|
640
|
+
);
|
|
641
|
+
return /* @__PURE__ */ jsx8(AdminClientProvider, { value: client, children: /* @__PURE__ */ jsx8(AdminShell, { title: props.title ?? "Maestro Admin" }) });
|
|
642
|
+
}
|
|
643
|
+
export {
|
|
644
|
+
AdminClientProvider,
|
|
645
|
+
EntityDetail,
|
|
646
|
+
EntityForm,
|
|
647
|
+
EntityList,
|
|
648
|
+
FieldInput,
|
|
649
|
+
FieldValue,
|
|
650
|
+
MaestroAdmin,
|
|
651
|
+
MaestroApiError,
|
|
652
|
+
MaestroClient,
|
|
653
|
+
Sidebar,
|
|
654
|
+
detailFields,
|
|
655
|
+
formFields,
|
|
656
|
+
listFields,
|
|
657
|
+
useAsync,
|
|
658
|
+
useClient,
|
|
659
|
+
useEntityList,
|
|
660
|
+
useEntityRecord,
|
|
661
|
+
useMetadata
|
|
662
|
+
};
|
|
663
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/MaestroAdmin.tsx","../src/client/MaestroClient.ts","../src/AdminContext.tsx","../src/hooks.ts","../src/components/Sidebar.tsx","../src/components/EntityList.tsx","../src/components/ui.tsx","../src/components/FieldValue.tsx","../src/components/EntityDetail.tsx","../src/components/EntityForm.tsx","../src/components/FieldInput.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { MaestroClient, type MaestroClientConfig } from '../client/MaestroClient.js';\nimport { AdminClientProvider } from '../AdminContext.js';\nimport { useMetadata } from '../hooks.js';\nimport { Sidebar } from './Sidebar.js';\nimport { EntityList } from './EntityList.js';\nimport { EntityDetail } from './EntityDetail.js';\nimport { EntityForm } from './EntityForm.js';\nimport { ErrorBanner, Spinner } from './ui.js';\n\nexport interface MaestroAdminProps {\n /** Base URL of the running maestro-server. Ignored when `client` is given. */\n apiUrl?: string;\n /** Per-request headers (auth). Ignored when `client` is given. */\n headers?: MaestroClientConfig['headers'];\n /** A pre-built client, for full control (custom fetch, SSR, tests). */\n client?: MaestroClient;\n /** Sidebar title. Defaults to \"Maestro Admin\". */\n title?: string;\n}\n\ntype View =\n | { kind: 'list'; entityId: string }\n | { kind: 'detail'; entityId: string; id: string }\n | { kind: 'create'; entityId: string }\n | { kind: 'edit'; entityId: string; id: string };\n\nfunction AdminShell({ title }: { title: string }) {\n const { data, error, loading } = useMetadata();\n const [view, setView] = useState<View | undefined>(undefined);\n\n const entities = data?.entities ?? [];\n const activeId = view?.entityId ?? entities[0]?.id;\n const activeEntity = entities.find((e) => e.id === activeId);\n const effectiveView: View | undefined = view ?? (activeId ? { kind: 'list', entityId: activeId } : undefined);\n\n if (loading && !data) {\n return (\n <div className=\"flex h-full items-center justify-center\">\n <Spinner label=\"Loading metadata…\" />\n </div>\n );\n }\n\n return (\n <div className=\"flex h-full min-h-screen bg-white text-slate-800\">\n <Sidebar\n entities={entities}\n activeId={activeId}\n title={title}\n onSelect={(entityId) => setView({ kind: 'list', entityId })}\n />\n <main className=\"flex-1 overflow-y-auto p-6\">\n {error && <ErrorBanner error={error} />}\n {activeEntity && effectiveView && effectiveView.kind === 'list' && (\n <EntityList\n entity={activeEntity}\n onOpen={(id) => setView({ kind: 'detail', entityId: activeEntity.id, id })}\n onCreate={() => setView({ kind: 'create', entityId: activeEntity.id })}\n onEdit={(id) => setView({ kind: 'edit', entityId: activeEntity.id, id })}\n />\n )}\n {activeEntity && effectiveView?.kind === 'detail' && (\n <EntityDetail\n entity={activeEntity}\n id={effectiveView.id}\n onBack={() => setView({ kind: 'list', entityId: activeEntity.id })}\n onEdit={() => setView({ kind: 'edit', entityId: activeEntity.id, id: effectiveView.id })}\n />\n )}\n {activeEntity && (effectiveView?.kind === 'create' || effectiveView?.kind === 'edit') && (\n <EntityForm\n entity={activeEntity}\n mode={effectiveView.kind}\n id={effectiveView.kind === 'edit' ? effectiveView.id : undefined}\n onCancel={() => setView({ kind: 'list', entityId: activeEntity.id })}\n onSaved={() => setView({ kind: 'list', entityId: activeEntity.id })}\n />\n )}\n </main>\n </div>\n );\n}\n\n/**\n * The whole metadata-driven admin in one component. Point it at a running maestro-server and it reads\n * `GET /metadata` and auto-builds navigation, tables, detail views and create/update forms for every\n * entity — respecting each entity's capabilities and the server's RBAC. No per-entity code.\n */\nexport function MaestroAdmin(props: MaestroAdminProps) {\n const client = useMemo(\n () => props.client ?? new MaestroClient({ apiUrl: props.apiUrl ?? '', headers: props.headers }),\n [props.client, props.apiUrl, props.headers],\n );\n\n return (\n <AdminClientProvider value={client}>\n <AdminShell title={props.title ?? 'Maestro Admin'} />\n </AdminClientProvider>\n );\n}\n","import type { EntityMetadata, ListQuery, ListResult, MaestroMetadata, Record_ } from './types.js';\n\nexport interface MaestroClientConfig {\n /** Base URL of the running maestro-server, e.g. `http://localhost:3000` or `/api/admin`. */\n apiUrl: string;\n /** Extra headers sent on every request — the place to inject an auth token (`Authorization`, `X-Role`, …). */\n headers?: Record<string, string> | (() => Record<string, string>);\n /** Custom fetch (SSR, tests). Defaults to the global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/** Thrown for any non-2xx response, carrying the HTTP status and the parsed error body when present. */\nexport class MaestroApiError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = 'MaestroApiError';\n }\n}\n\nfunction buildListParams(query: ListQuery = {}): string {\n const params = new URLSearchParams();\n if (query.page !== undefined) params.set('page', String(query.page));\n if (query.pageSize !== undefined) params.set('pageSize', String(query.pageSize));\n if (query.search) {\n params.set('search', query.search);\n if (query.searchFields?.length) params.set('searchFields', query.searchFields.join(','));\n }\n for (const s of query.sort ?? []) params.append('sort', `${s.field}:${s.direction}`);\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n\n/**\n * A thin, framework-free client for the Maestro HTTP contract. Every admin component talks to the server\n * through this — it is the single seam where auth headers, base URL and error handling live.\n */\nexport class MaestroClient {\n private readonly base: string;\n private readonly doFetch: typeof fetch;\n\n constructor(private readonly config: MaestroClientConfig) {\n this.base = config.apiUrl.replace(/\\/$/, '');\n this.doFetch = config.fetch ?? globalThis.fetch.bind(globalThis);\n }\n\n private resolveHeaders(): Record<string, string> {\n const h = typeof this.config.headers === 'function' ? this.config.headers() : this.config.headers;\n return { ...h };\n }\n\n private async request<T>(path: string, init?: RequestInit): Promise<T> {\n const res = await this.doFetch(`${this.base}${path}`, {\n ...init,\n headers: {\n Accept: 'application/json',\n ...(init?.body ? { 'Content-Type': 'application/json' } : {}),\n ...this.resolveHeaders(),\n ...(init?.headers as Record<string, string> | undefined),\n },\n });\n\n if (res.status === 204) return undefined as T;\n\n const text = await res.text();\n const body = text ? (JSON.parse(text) as unknown) : undefined;\n\n if (!res.ok) {\n const err = body as { error?: string; message?: string } | undefined;\n throw new MaestroApiError(res.status, err?.error ?? 'ERROR', err?.message ?? res.statusText);\n }\n return body as T;\n }\n\n async getMetadata(): Promise<MaestroMetadata> {\n return this.request<MaestroMetadata>('/metadata');\n }\n\n async getEntityMetadata(entityId: string): Promise<EntityMetadata> {\n return this.request<EntityMetadata>(`/metadata/${encodeURIComponent(entityId)}`);\n }\n\n async list(entityId: string, query?: ListQuery): Promise<ListResult> {\n return this.request<ListResult>(`/entities/${encodeURIComponent(entityId)}${buildListParams(query)}`);\n }\n\n async findById(entityId: string, id: string): Promise<Record_> {\n return this.request<Record_>(`/entities/${encodeURIComponent(entityId)}/${encodeURIComponent(id)}`);\n }\n\n async create(entityId: string, data: Record_): Promise<Record_> {\n return this.request<Record_>(`/entities/${encodeURIComponent(entityId)}`, {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n async update(entityId: string, id: string, data: Record_): Promise<Record_> {\n return this.request<Record_>(`/entities/${encodeURIComponent(entityId)}/${encodeURIComponent(id)}`, {\n method: 'PATCH',\n body: JSON.stringify(data),\n });\n }\n\n async remove(entityId: string, id: string): Promise<void> {\n await this.request<void>(`/entities/${encodeURIComponent(entityId)}/${encodeURIComponent(id)}`, {\n method: 'DELETE',\n });\n }\n}\n","import { createContext, useContext } from 'react';\nimport type { MaestroClient } from './client/MaestroClient.js';\n\nconst ClientContext = createContext<MaestroClient | null>(null);\n\nexport const AdminClientProvider = ClientContext.Provider;\n\n/** Returns the `MaestroClient` provided at the root of the admin. Throws if used outside the provider. */\nexport function useClient(): MaestroClient {\n const client = useContext(ClientContext);\n if (!client) {\n throw new Error('useClient must be used within a <MaestroAdmin> (no MaestroClient in context).');\n }\n return client;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport { useClient } from './AdminContext.js';\nimport type { EntityMetadata, ListQuery, ListResult, MaestroMetadata } from './client/types.js';\n\nexport interface AsyncState<T> {\n data: T | undefined;\n error: Error | undefined;\n loading: boolean;\n reload: () => void;\n}\n\n/** Runs an async producer, tracking loading/error and exposing a manual `reload`. Re-runs when `deps` change. */\nexport function useAsync<T>(producer: () => Promise<T>, deps: readonly unknown[]): AsyncState<T> {\n const [data, setData] = useState<T | undefined>(undefined);\n const [error, setError] = useState<Error | undefined>(undefined);\n const [loading, setLoading] = useState(true);\n const [tick, setTick] = useState(0);\n\n useEffect(() => {\n let cancelled = false;\n setLoading(true);\n setError(undefined);\n producer()\n .then((result) => {\n if (!cancelled) setData(result);\n })\n .catch((err: unknown) => {\n if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n }, [...deps, tick]);\n\n const reload = useCallback(() => setTick((t) => t + 1), []);\n return { data, error, loading, reload };\n}\n\n/** Loads the full metadata document (`GET /metadata`). */\nexport function useMetadata(): AsyncState<MaestroMetadata> {\n const client = useClient();\n return useAsync(() => client.getMetadata(), []);\n}\n\n/** Loads a page of records for one entity. */\nexport function useEntityList(entityId: string, query: ListQuery): AsyncState<ListResult> {\n const client = useClient();\n const key = JSON.stringify(query);\n return useAsync(() => client.list(entityId, query), [entityId, key]);\n}\n\n/** Loads a single record by id. */\nexport function useEntityRecord(\n entityId: string,\n id: string | undefined,\n): AsyncState<globalThis.Record<string, unknown>> {\n const client = useClient();\n return useAsync(async () => (id ? client.findById(entityId, id) : {}), [entityId, id]);\n}\n\n/** Fields shown in the list table, ordered, honoring per-field list visibility. */\nexport function listFields(entity: EntityMetadata) {\n return entity.fields\n .filter((f) => f.list.visible && !f.hidden)\n .sort((a, b) => (a.list.order ?? 0) - (b.list.order ?? 0));\n}\n\n/** Fields shown in the detail view, ordered. */\nexport function detailFields(entity: EntityMetadata) {\n return entity.fields\n .filter((f) => f.detail.visible)\n .sort((a, b) => (a.detail.order ?? 0) - (b.detail.order ?? 0));\n}\n\n/** Fields editable in the create/update form, ordered, per mode. */\nexport function formFields(entity: EntityMetadata, mode: 'create' | 'edit') {\n return entity.fields\n .filter((f) => (mode === 'create' ? f.form.creatable : f.form.editable))\n .sort((a, b) => (a.form.order ?? 0) - (b.form.order ?? 0));\n}\n","import type { EntityMetadata } from '../client/types.js';\n\nexport function Sidebar({\n entities,\n activeId,\n onSelect,\n title,\n}: {\n entities: EntityMetadata[];\n activeId: string | undefined;\n onSelect: (entityId: string) => void;\n title: string;\n}) {\n return (\n <aside className=\"flex w-60 shrink-0 flex-col border-r border-slate-200 bg-slate-50\">\n <div className=\"border-b border-slate-200 px-4 py-4\">\n <span className=\"text-sm font-semibold tracking-tight text-slate-900\">{title}</span>\n </div>\n <nav className=\"flex-1 overflow-y-auto p-2\">\n {entities.map((e) => (\n <button\n key={e.id}\n onClick={() => onSelect(e.id)}\n className={`flex w-full items-center justify-between rounded-md px-3 py-2 text-left text-sm transition-colors ${\n e.id === activeId ? 'bg-indigo-100 font-medium text-indigo-700' : 'text-slate-700 hover:bg-slate-100'\n }`}\n >\n <span className=\"truncate\">{e.label.plural}</span>\n </button>\n ))}\n </nav>\n <div className=\"border-t border-slate-200 px-4 py-3 text-xs text-slate-400\">\n {entities.length} entities · Maestro\n </div>\n </aside>\n );\n}\n","import { useMemo, useState } from 'react';\nimport type { EntityMetadata, ListQuery, SortInput } from '../client/types.js';\nimport { useClient } from '../AdminContext.js';\nimport { listFields, useEntityList } from '../hooks.js';\nimport { FieldValue } from './FieldValue.js';\nimport { Button, EmptyState, ErrorBanner, Spinner } from './ui.js';\n\nconst PAGE_SIZE = 20;\n\nexport function EntityList({\n entity,\n onOpen,\n onCreate,\n onEdit,\n}: {\n entity: EntityMetadata;\n onOpen: (id: string) => void;\n onCreate: () => void;\n onEdit: (id: string) => void;\n}) {\n const client = useClient();\n const columns = useMemo(() => listFields(entity), [entity]);\n const searchFields = useMemo(() => entity.fields.filter((f) => f.searchable).map((f) => f.name), [entity]);\n\n const [page, setPage] = useState(1);\n const [search, setSearch] = useState('');\n const [sort, setSort] = useState<SortInput | undefined>(undefined);\n\n const query: ListQuery = {\n page,\n pageSize: PAGE_SIZE,\n search: search || undefined,\n searchFields,\n sort: sort ? [sort] : undefined,\n };\n const { data, error, loading, reload } = useEntityList(entity.id, query);\n\n const toggleSort = (fieldName: string): void => {\n setSort((prev) =>\n prev?.field === fieldName\n ? { field: fieldName, direction: prev.direction === 'asc' ? 'desc' : 'asc' }\n : { field: fieldName, direction: 'asc' },\n );\n };\n\n const onDelete = async (id: string): Promise<void> => {\n if (!globalThis.confirm(`Delete this ${entity.label.singular.toLowerCase()}? This cannot be undone.`)) return;\n await client.remove(entity.id, id);\n reload();\n };\n\n const totalPages = data?.totalPages ?? (data ? Math.max(1, Math.ceil(data.total / PAGE_SIZE)) : 1);\n\n return (\n <section className=\"space-y-4\">\n <header className=\"flex flex-wrap items-center justify-between gap-3\">\n <div>\n <h1 className=\"text-lg font-semibold text-slate-900\">{entity.label.plural}</h1>\n {data && <p className=\"text-sm text-slate-500\">{data.total} records</p>}\n </div>\n <div className=\"flex items-center gap-2\">\n {searchFields.length > 0 && (\n <input\n type=\"search\"\n placeholder={`Search ${entity.label.plural.toLowerCase()}…`}\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n className=\"rounded-md border border-slate-300 px-3 py-1.5 text-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500\"\n />\n )}\n {entity.capabilities.create && (\n <Button variant=\"primary\" onClick={onCreate}>\n + New\n </Button>\n )}\n </div>\n </header>\n\n {error && <ErrorBanner error={error} />}\n\n {loading && !data ? (\n <Spinner label={`Loading ${entity.label.plural.toLowerCase()}…`} />\n ) : data && data.records.length === 0 ? (\n <EmptyState title=\"No records\" hint={search ? 'Try a different search.' : undefined} />\n ) : (\n <div className=\"overflow-x-auto rounded-lg border border-slate-200\">\n <table className=\"min-w-full divide-y divide-slate-200 text-sm\">\n <thead className=\"bg-slate-50\">\n <tr>\n {columns.map((f) => (\n <th\n key={f.name}\n onClick={() => f.sortable && toggleSort(f.name)}\n className={`px-3 py-2 text-left font-medium text-slate-600 ${f.sortable ? 'cursor-pointer select-none hover:text-slate-900' : ''}`}\n >\n {f.label}\n {sort?.field === f.name && (sort.direction === 'asc' ? ' ▲' : ' ▼')}\n </th>\n ))}\n <th className=\"px-3 py-2\" />\n </tr>\n </thead>\n <tbody className=\"divide-y divide-slate-100 bg-white\">\n {data?.records.map((record, i) => {\n const id = String(record[entity.primaryKey]);\n return (\n <tr key={id || i} className=\"hover:bg-slate-50\">\n {columns.map((f) => (\n <td\n key={f.name}\n className=\"cursor-pointer px-3 py-2 align-top\"\n onClick={() => onOpen(id)}\n >\n <FieldValue field={f} value={record[f.name]} />\n </td>\n ))}\n <td className=\"whitespace-nowrap px-3 py-2 text-right\">\n {entity.capabilities.update && (\n <Button variant=\"ghost\" onClick={() => onEdit(id)}>\n Edit\n </Button>\n )}\n {entity.capabilities.delete && (\n <Button variant=\"ghost\" className=\"text-red-600\" onClick={() => onDelete(id)}>\n Delete\n </Button>\n )}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n )}\n\n {data && totalPages > 1 && (\n <div className=\"flex items-center justify-end gap-2 text-sm\">\n <Button variant=\"secondary\" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>\n Previous\n </Button>\n <span className=\"text-slate-500\">\n Page {page} of {totalPages}\n </span>\n <Button variant=\"secondary\" disabled={page >= totalPages} onClick={() => setPage((p) => p + 1)}>\n Next\n </Button>\n </div>\n )}\n </section>\n );\n}\n","import type { ButtonHTMLAttributes, ReactNode } from 'react';\n\ntype Variant = 'primary' | 'secondary' | 'danger' | 'ghost';\n\nconst VARIANTS: Record<Variant, string> = {\n primary: 'bg-indigo-600 text-white hover:bg-indigo-500 disabled:bg-indigo-300',\n secondary: 'bg-white text-slate-700 ring-1 ring-inset ring-slate-300 hover:bg-slate-50',\n danger: 'bg-red-600 text-white hover:bg-red-500 disabled:bg-red-300',\n ghost: 'text-slate-600 hover:bg-slate-100',\n};\n\nexport function Button({\n variant = 'secondary',\n className = '',\n ...props\n}: ButtonHTMLAttributes<HTMLButtonElement> & { variant?: Variant }) {\n return (\n <button\n className={`inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-not-allowed ${VARIANTS[variant]} ${className}`}\n {...props}\n />\n );\n}\n\nexport function Badge({ children, tone = 'slate' }: { children: ReactNode; tone?: 'slate' | 'green' | 'red' | 'amber' | 'indigo' }) {\n const tones: Record<string, string> = {\n slate: 'bg-slate-100 text-slate-700',\n green: 'bg-green-100 text-green-700',\n red: 'bg-red-100 text-red-700',\n amber: 'bg-amber-100 text-amber-700',\n indigo: 'bg-indigo-100 text-indigo-700',\n };\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${tones[tone]}`}>\n {children}\n </span>\n );\n}\n\nexport function Spinner({ label }: { label?: string }) {\n return (\n <div className=\"flex items-center gap-2 text-sm text-slate-500\" role=\"status\">\n <span className=\"h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-indigo-600\" />\n {label ?? 'Loading…'}\n </div>\n );\n}\n\nexport function EmptyState({ title, hint }: { title: string; hint?: string }) {\n return (\n <div className=\"rounded-lg border border-dashed border-slate-300 p-8 text-center\">\n <p className=\"text-sm font-medium text-slate-700\">{title}</p>\n {hint && <p className=\"mt-1 text-sm text-slate-500\">{hint}</p>}\n </div>\n );\n}\n\nexport function ErrorBanner({ error }: { error: Error }) {\n return (\n <div className=\"rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700\" role=\"alert\">\n {error.message}\n </div>\n );\n}\n","import type { FieldMetadata } from '../client/types.js';\nimport { Badge } from './ui.js';\n\nfunction enumLabel(field: FieldMetadata, value: unknown): string {\n const opt = field.enumOptions?.find((o) => o.value === value);\n return opt ? opt.label : String(value);\n}\n\n/** Renders a single record value read-only, shaped by the field's type (for tables and detail views). */\nexport function FieldValue({ field, value }: { field: FieldMetadata; value: unknown }) {\n if (value === null || value === undefined || value === '') {\n return <span className=\"text-slate-400\">—</span>;\n }\n\n if (field.sensitive) {\n return <span className=\"text-slate-400\">••••••</span>;\n }\n\n switch (field.type) {\n case 'boolean':\n return value ? <Badge tone=\"green\">true</Badge> : <Badge tone=\"slate\">false</Badge>;\n case 'enum':\n return <Badge tone=\"indigo\">{enumLabel(field, value)}</Badge>;\n case 'date':\n case 'datetime': {\n const d = new Date(String(value));\n return <span>{Number.isNaN(d.getTime()) ? String(value) : d.toLocaleString()}</span>;\n }\n case 'json':\n return (\n <code className=\"block max-w-md overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700\">\n {JSON.stringify(value)}\n </code>\n );\n default:\n return <span className=\"text-slate-700\">{String(value)}</span>;\n }\n}\n","import type { EntityMetadata } from '../client/types.js';\nimport { detailFields, useEntityRecord } from '../hooks.js';\nimport { FieldValue } from './FieldValue.js';\nimport { Button, ErrorBanner, Spinner } from './ui.js';\n\nexport function EntityDetail({\n entity,\n id,\n onBack,\n onEdit,\n}: {\n entity: EntityMetadata;\n id: string;\n onBack: () => void;\n onEdit: () => void;\n}) {\n const { data, error, loading } = useEntityRecord(entity.id, id);\n const fields = detailFields(entity);\n\n return (\n <section className=\"space-y-4\">\n <header className=\"flex items-center justify-between gap-3\">\n <div>\n <button onClick={onBack} className=\"text-sm text-indigo-600 hover:underline\">\n ← {entity.label.plural}\n </button>\n <h1 className=\"text-lg font-semibold text-slate-900\">\n {entity.label.singular} {data ? String(data[entity.displayField] ?? id) : ''}\n </h1>\n </div>\n {entity.capabilities.update && (\n <Button variant=\"primary\" onClick={onEdit}>\n Edit\n </Button>\n )}\n </header>\n\n {error && <ErrorBanner error={error} />}\n {loading && !data ? (\n <Spinner />\n ) : (\n data && (\n <dl className=\"divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white\">\n {fields.map((f) => (\n <div key={f.name} className=\"grid grid-cols-3 gap-4 px-4 py-3\">\n <dt className=\"text-sm font-medium text-slate-500\">{f.label}</dt>\n <dd className=\"col-span-2 text-sm\">\n <FieldValue field={f} value={data[f.name]} />\n </dd>\n </div>\n ))}\n </dl>\n )\n )}\n </section>\n );\n}\n","import { useEffect, useState } from 'react';\nimport type { EntityMetadata, Record_ } from '../client/types.js';\nimport { useClient } from '../AdminContext.js';\nimport { formFields, useEntityRecord } from '../hooks.js';\nimport { FieldInput } from './FieldInput.js';\nimport { Button, ErrorBanner, Spinner } from './ui.js';\n\nexport function EntityForm({\n entity,\n mode,\n id,\n onCancel,\n onSaved,\n}: {\n entity: EntityMetadata;\n mode: 'create' | 'edit';\n id?: string;\n onCancel: () => void;\n onSaved: (record: Record_) => void;\n}) {\n const client = useClient();\n const fields = formFields(entity, mode);\n const existing = useEntityRecord(entity.id, mode === 'edit' ? id : undefined);\n\n const [values, setValues] = useState<Record_>({});\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<Error | undefined>(undefined);\n\n useEffect(() => {\n if (mode === 'edit' && existing.data) setValues(existing.data);\n }, [mode, existing.data]);\n\n const submit = async (e: React.FormEvent): Promise<void> => {\n e.preventDefault();\n setSaving(true);\n setError(undefined);\n try {\n const payload: Record_ = {};\n for (const f of fields) {\n if (values[f.name] !== undefined) payload[f.name] = values[f.name];\n }\n const saved =\n mode === 'create'\n ? await client.create(entity.id, payload)\n : await client.update(entity.id, id!, payload);\n onSaved(saved);\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n setSaving(false);\n }\n };\n\n if (mode === 'edit' && existing.loading && !existing.data) {\n return <Spinner />;\n }\n\n return (\n <section className=\"max-w-2xl space-y-4\">\n <header>\n <button onClick={onCancel} className=\"text-sm text-indigo-600 hover:underline\">\n ← {entity.label.plural}\n </button>\n <h1 className=\"text-lg font-semibold text-slate-900\">\n {mode === 'create' ? `New ${entity.label.singular}` : `Edit ${entity.label.singular}`}\n </h1>\n </header>\n\n {error && <ErrorBanner error={error} />}\n\n <form onSubmit={submit} className=\"space-y-4 rounded-lg border border-slate-200 bg-white p-5\">\n {fields.map((f) => (\n <div key={f.name} className=\"space-y-1\">\n <label htmlFor={`field-${f.name}`} className=\"block text-sm font-medium text-slate-700\">\n {f.label}\n {f.required && <span className=\"ml-0.5 text-red-500\">*</span>}\n </label>\n <FieldInput\n field={f}\n value={values[f.name]}\n disabled={saving || f.readonly}\n onChange={(next) => setValues((v) => ({ ...v, [f.name]: next }))}\n />\n {f.form.helpText && <p className=\"text-xs text-slate-500\">{f.form.helpText}</p>}\n </div>\n ))}\n\n <div className=\"flex justify-end gap-2 border-t border-slate-100 pt-4\">\n <Button type=\"button\" variant=\"secondary\" onClick={onCancel} disabled={saving}>\n Cancel\n </Button>\n <Button type=\"submit\" variant=\"primary\" disabled={saving}>\n {saving ? 'Saving…' : mode === 'create' ? 'Create' : 'Save'}\n </Button>\n </div>\n </form>\n </section>\n );\n}\n","import type { FieldMetadata } from '../client/types.js';\n\nconst inputClass =\n 'w-full rounded-md border border-slate-300 px-3 py-1.5 text-sm text-slate-800 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:bg-slate-100';\n\n/**\n * Renders the right form control for a field's type and reports its next value. Values are kept as strings\n * for text/enum, numbers for numeric types, booleans for checkboxes, and parsed objects for json — matching\n * what the create/update endpoints expect.\n */\nexport function FieldInput({\n field,\n value,\n onChange,\n disabled,\n}: {\n field: FieldMetadata;\n value: unknown;\n onChange: (next: unknown) => void;\n disabled?: boolean;\n}) {\n const id = `field-${field.name}`;\n\n switch (field.type) {\n case 'boolean':\n return (\n <input\n id={id}\n type=\"checkbox\"\n checked={Boolean(value)}\n disabled={disabled}\n onChange={(e) => onChange(e.target.checked)}\n className=\"h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500\"\n />\n );\n\n case 'integer':\n case 'number':\n case 'float':\n case 'decimal':\n case 'currency':\n return (\n <input\n id={id}\n type=\"number\"\n value={value === null || value === undefined ? '' : String(value)}\n placeholder={field.form.placeholder}\n disabled={disabled}\n step={field.type === 'integer' ? 1 : 'any'}\n onChange={(e) => onChange(e.target.value === '' ? null : Number(e.target.value))}\n className={inputClass}\n />\n );\n\n case 'enum':\n return (\n <select\n id={id}\n value={value === null || value === undefined ? '' : String(value)}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n className={inputClass}\n >\n <option value=\"\">—</option>\n {field.enumOptions?.map((o) => (\n <option key={String(o.value)} value={String(o.value)}>\n {o.label}\n </option>\n ))}\n </select>\n );\n\n case 'date':\n return (\n <input\n id={id}\n type=\"date\"\n value={value ? String(value).slice(0, 10) : ''}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value || null)}\n className={inputClass}\n />\n );\n\n case 'text':\n case 'json':\n return (\n <textarea\n id={id}\n rows={field.type === 'json' ? 5 : 3}\n value={\n field.type === 'json' && value !== null && typeof value === 'object'\n ? JSON.stringify(value, null, 2)\n : value === null || value === undefined\n ? ''\n : String(value)\n }\n placeholder={field.form.placeholder}\n disabled={disabled}\n onChange={(e) => {\n if (field.type === 'json') {\n try {\n onChange(e.target.value ? JSON.parse(e.target.value) : null);\n } catch {\n onChange(e.target.value);\n }\n } else {\n onChange(e.target.value);\n }\n }}\n className={`${inputClass} font-mono`}\n />\n );\n\n default:\n return (\n <input\n id={id}\n type={field.type === 'email' ? 'email' : 'text'}\n value={value === null || value === undefined ? '' : String(value)}\n placeholder={field.form.placeholder}\n disabled={disabled}\n onChange={(e) => onChange(e.target.value)}\n className={inputClass}\n />\n );\n }\n}\n"],"mappings":"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","names":["useMemo","useState","useState","jsx","jsxs","jsx","jsx","jsxs","useState","jsx","jsxs","useEffect","useState","jsx","jsxs","jsx","jsxs","useState","useEffect","jsx","jsxs","useState","useMemo"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maykonpaulo/maestro-admin",
|
|
3
|
+
"version": "0.1.0-next.1",
|
|
4
|
+
"description": "Generic, metadata-driven admin UI for @maykonpaulo/maestro-core (via @maykonpaulo/maestro-server). Reads GET /metadata and auto-builds navigation, tables, detail views and create/update forms for every entity/collection — no per-entity code. Ships as a runnable React app and as exported components (ADR 0008).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./src/styles.css": "./src/styles.css"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src/styles.css",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
26
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@playwright/test": "^1.48.0",
|
|
31
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
32
|
+
"@testing-library/dom": "^10.4.0",
|
|
33
|
+
"@testing-library/jest-dom": "^6.6.0",
|
|
34
|
+
"@testing-library/react": "^16.1.0",
|
|
35
|
+
"@testing-library/user-event": "^14.5.2",
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"@types/react-dom": "^19.0.0",
|
|
38
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
39
|
+
"jsdom": "^25.0.1",
|
|
40
|
+
"react": "^19.0.0",
|
|
41
|
+
"react-dom": "^19.0.0",
|
|
42
|
+
"rimraf": "^6.0.0",
|
|
43
|
+
"tailwindcss": "^4.0.0",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"typescript": "^5.6.0",
|
|
46
|
+
"vite": "^6.0.0",
|
|
47
|
+
"vitest": "^2.0.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "vite",
|
|
51
|
+
"build": "tsup",
|
|
52
|
+
"build:app": "vite build",
|
|
53
|
+
"preview": "vite preview",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:e2e": "playwright test",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"lint": "eslint src",
|
|
58
|
+
"clean": "rimraf dist dist-app"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import 'tailwindcss';
|