@maykonpaulo/maestro-admin 0.1.2 → 0.2.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +149 -7
- package/dist/index.js +869 -134
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/MaestroAdmin.tsx
|
|
2
|
-
import { useMemo as
|
|
2
|
+
import { useMemo as useMemo3, useState as useState9 } from "react";
|
|
3
3
|
|
|
4
4
|
// src/client/MaestroClient.ts
|
|
5
5
|
var MaestroApiError = class extends Error {
|
|
@@ -21,6 +21,9 @@ function buildListParams(query = {}) {
|
|
|
21
21
|
if (query.searchFields?.length) params.set("searchFields", query.searchFields.join(","));
|
|
22
22
|
}
|
|
23
23
|
for (const s of query.sort ?? []) params.append("sort", `${s.field}:${s.direction}`);
|
|
24
|
+
for (const f of query.filters ?? []) {
|
|
25
|
+
params.append("filter", f.value === void 0 ? `${f.field}:${f.operator}` : `${f.field}:${f.operator}:${f.value}`);
|
|
26
|
+
}
|
|
24
27
|
const qs = params.toString();
|
|
25
28
|
return qs ? `?${qs}` : "";
|
|
26
29
|
}
|
|
@@ -98,6 +101,15 @@ function useClient() {
|
|
|
98
101
|
}
|
|
99
102
|
return client;
|
|
100
103
|
}
|
|
104
|
+
var EntitiesContext = createContext([]);
|
|
105
|
+
var AdminEntitiesProvider = EntitiesContext.Provider;
|
|
106
|
+
function useEntities() {
|
|
107
|
+
return useContext(EntitiesContext);
|
|
108
|
+
}
|
|
109
|
+
function useEntity(entityId) {
|
|
110
|
+
const entities = useEntities();
|
|
111
|
+
return entityId ? entities.find((e) => e.id === entityId) : void 0;
|
|
112
|
+
}
|
|
101
113
|
|
|
102
114
|
// src/hooks.ts
|
|
103
115
|
import { useCallback, useEffect, useState } from "react";
|
|
@@ -140,6 +152,45 @@ function useEntityRecord(entityId, id) {
|
|
|
140
152
|
function listFields(entity) {
|
|
141
153
|
return entity.fields.filter((f) => f.list.visible && !f.hidden).sort((a, b) => (a.list.order ?? 0) - (b.list.order ?? 0));
|
|
142
154
|
}
|
|
155
|
+
var LIST_EXCLUDED_TYPES = /* @__PURE__ */ new Set(["json", "array", "text"]);
|
|
156
|
+
function listColumnRank(field) {
|
|
157
|
+
switch (field.type) {
|
|
158
|
+
case "string":
|
|
159
|
+
case "email":
|
|
160
|
+
case "document":
|
|
161
|
+
case "phone":
|
|
162
|
+
return 1;
|
|
163
|
+
case "enum":
|
|
164
|
+
return 2;
|
|
165
|
+
case "relation":
|
|
166
|
+
return 3;
|
|
167
|
+
case "boolean":
|
|
168
|
+
return 4;
|
|
169
|
+
case "date":
|
|
170
|
+
case "datetime":
|
|
171
|
+
case "time":
|
|
172
|
+
return 5;
|
|
173
|
+
case "uuid":
|
|
174
|
+
return 7;
|
|
175
|
+
case "url":
|
|
176
|
+
return 8;
|
|
177
|
+
default:
|
|
178
|
+
return 6;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function primaryListFields(entity, max = 8) {
|
|
182
|
+
const all = listFields(entity);
|
|
183
|
+
const curated = all.some((f) => f.list.order !== void 0 || f.list.width !== void 0);
|
|
184
|
+
if (curated || all.length <= max) return all;
|
|
185
|
+
const scored = all.filter((f) => !f.sensitive && f.name !== entity.primaryKey && !LIST_EXCLUDED_TYPES.has(f.type)).map((field, index) => {
|
|
186
|
+
let score = listColumnRank(field) * 10 + index * 0.01;
|
|
187
|
+
if (field.searchable) score -= 5;
|
|
188
|
+
if (field.required) score -= 2;
|
|
189
|
+
if (field.name === entity.displayField) score = -100;
|
|
190
|
+
return { field, score };
|
|
191
|
+
}).sort((a, b) => a.score - b.score);
|
|
192
|
+
return scored.slice(0, max).map((s) => s.field);
|
|
193
|
+
}
|
|
143
194
|
function detailFields(entity) {
|
|
144
195
|
return entity.fields.filter((f) => f.detail.visible).sort((a, b) => (a.detail.order ?? 0) - (b.detail.order ?? 0));
|
|
145
196
|
}
|
|
@@ -148,33 +199,303 @@ function formFields(entity, mode) {
|
|
|
148
199
|
}
|
|
149
200
|
|
|
150
201
|
// src/components/Sidebar.tsx
|
|
151
|
-
import {
|
|
202
|
+
import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
203
|
+
|
|
204
|
+
// src/grouping.ts
|
|
205
|
+
var MIN_PREFIX = 4;
|
|
206
|
+
function commonPrefix(a, b) {
|
|
207
|
+
let i = 0;
|
|
208
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i += 1;
|
|
209
|
+
return a.slice(0, i);
|
|
210
|
+
}
|
|
211
|
+
function humanize(prefix) {
|
|
212
|
+
return prefix.charAt(0).toUpperCase() + prefix.slice(1);
|
|
213
|
+
}
|
|
214
|
+
function buildMenu(entities, labels) {
|
|
215
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
216
|
+
const rest = [];
|
|
217
|
+
for (const entity of entities) {
|
|
218
|
+
const override = labels?.entities?.[entity.id] ?? labels?.entities?.[entity.table];
|
|
219
|
+
if (override?.group) {
|
|
220
|
+
const existing = byGroup.get(override.group);
|
|
221
|
+
if (existing) existing.entities.push(entity);
|
|
222
|
+
else byGroup.set(override.group, { id: override.group, label: override.group, entities: [entity] });
|
|
223
|
+
} else {
|
|
224
|
+
rest.push(entity);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const sorted = [...rest].sort((a, b) => a.table < b.table ? -1 : a.table > b.table ? 1 : 0);
|
|
228
|
+
const ungrouped = [];
|
|
229
|
+
let i = 0;
|
|
230
|
+
while (i < sorted.length) {
|
|
231
|
+
let prefix = i + 1 < sorted.length ? commonPrefix(sorted[i].table, sorted[i + 1].table) : "";
|
|
232
|
+
if (prefix.length < MIN_PREFIX) {
|
|
233
|
+
ungrouped.push(sorted[i]);
|
|
234
|
+
i += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
let end = i + 1;
|
|
238
|
+
while (end + 1 < sorted.length) {
|
|
239
|
+
const next = commonPrefix(prefix, sorted[end + 1].table);
|
|
240
|
+
if (next.length < MIN_PREFIX) break;
|
|
241
|
+
prefix = next;
|
|
242
|
+
end += 1;
|
|
243
|
+
}
|
|
244
|
+
const members = sorted.slice(i, end + 1);
|
|
245
|
+
const label = labels?.groups?.[prefix] ?? humanize(prefix);
|
|
246
|
+
const existing = byGroup.get(prefix);
|
|
247
|
+
if (existing) existing.entities.push(...members);
|
|
248
|
+
else byGroup.set(prefix, { id: prefix, label, entities: members });
|
|
249
|
+
i = end + 1;
|
|
250
|
+
}
|
|
251
|
+
const collator = (a, b) => a.localeCompare(b);
|
|
252
|
+
const groups = [...byGroup.values()].sort((a, b) => collator(a.label, b.label));
|
|
253
|
+
for (const group of groups) {
|
|
254
|
+
group.entities.sort((a, b) => collator(a.label.plural, b.label.plural));
|
|
255
|
+
}
|
|
256
|
+
ungrouped.sort((a, b) => collator(a.label.plural, b.label.plural));
|
|
257
|
+
return { groups, ungrouped };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/i18n.ts
|
|
261
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
262
|
+
var EN = {
|
|
263
|
+
loading: "Loading\u2026",
|
|
264
|
+
loadingMetadata: "Loading metadata\u2026",
|
|
265
|
+
records: (total) => `${total} record${total === 1 ? "" : "s"}`,
|
|
266
|
+
searchPlaceholder: (plural) => `Search ${plural.toLowerCase()}\u2026`,
|
|
267
|
+
newRecord: "+ New",
|
|
268
|
+
view: "View",
|
|
269
|
+
edit: "Edit",
|
|
270
|
+
clone: "Clone",
|
|
271
|
+
remove: "Remove",
|
|
272
|
+
copyId: "Copy ID",
|
|
273
|
+
copyObject: "Copy object",
|
|
274
|
+
copyValue: "Copy value",
|
|
275
|
+
copied: "Copied!",
|
|
276
|
+
addToFilter: "Add to filter",
|
|
277
|
+
openRelated: (label) => `Open ${label}`,
|
|
278
|
+
filters: "Filters",
|
|
279
|
+
clearFilters: "Clear filters",
|
|
280
|
+
actions: "Actions",
|
|
281
|
+
noRecords: "No records",
|
|
282
|
+
tryDifferentSearch: "Try a different search.",
|
|
283
|
+
deleteConfirm: (singular) => `Delete this ${singular.toLowerCase()}? This cannot be undone.`,
|
|
284
|
+
page: (page, total) => `Page ${page} of ${total}`,
|
|
285
|
+
previous: "Previous",
|
|
286
|
+
next: "Next",
|
|
287
|
+
cancel: "Cancel",
|
|
288
|
+
save: "Save",
|
|
289
|
+
saving: "Saving\u2026",
|
|
290
|
+
create: "Create",
|
|
291
|
+
newTitle: (singular) => `New ${singular}`,
|
|
292
|
+
editTitle: (singular) => `Edit ${singular}`,
|
|
293
|
+
cloneTitle: (singular) => `Clone ${singular}`,
|
|
294
|
+
entitiesFooter: (count) => `${count} entities \xB7 Maestro`,
|
|
295
|
+
yes: "true",
|
|
296
|
+
no: "false",
|
|
297
|
+
searchMenu: "Search menu\u2026",
|
|
298
|
+
recents: "Recents",
|
|
299
|
+
menus: "Menus",
|
|
300
|
+
noMenuResults: "No matches"
|
|
301
|
+
};
|
|
302
|
+
var PT_BR = {
|
|
303
|
+
loading: "Carregando\u2026",
|
|
304
|
+
loadingMetadata: "Carregando metadados\u2026",
|
|
305
|
+
records: (total) => `${total} registro${total === 1 ? "" : "s"}`,
|
|
306
|
+
searchPlaceholder: (plural) => `Buscar ${plural.toLowerCase()}\u2026`,
|
|
307
|
+
newRecord: "+ Novo",
|
|
308
|
+
view: "Visualizar",
|
|
309
|
+
edit: "Editar",
|
|
310
|
+
clone: "Clonar",
|
|
311
|
+
remove: "Remover",
|
|
312
|
+
copyId: "Copiar ID",
|
|
313
|
+
copyObject: "Copiar objeto",
|
|
314
|
+
copyValue: "Copiar valor",
|
|
315
|
+
copied: "Copiado!",
|
|
316
|
+
addToFilter: "Adicionar ao filtro",
|
|
317
|
+
openRelated: (label) => `Abrir ${label}`,
|
|
318
|
+
filters: "Filtros",
|
|
319
|
+
clearFilters: "Limpar filtros",
|
|
320
|
+
actions: "A\xE7\xF5es",
|
|
321
|
+
noRecords: "Nenhum registro",
|
|
322
|
+
tryDifferentSearch: "Tente uma busca diferente.",
|
|
323
|
+
deleteConfirm: (singular) => `Remover este registro de ${singular}? Essa a\xE7\xE3o n\xE3o pode ser desfeita.`,
|
|
324
|
+
page: (page, total) => `P\xE1gina ${page} de ${total}`,
|
|
325
|
+
previous: "Anterior",
|
|
326
|
+
next: "Pr\xF3xima",
|
|
327
|
+
cancel: "Cancelar",
|
|
328
|
+
save: "Salvar",
|
|
329
|
+
saving: "Salvando\u2026",
|
|
330
|
+
create: "Criar",
|
|
331
|
+
newTitle: (singular) => `Novo(a) ${singular}`,
|
|
332
|
+
editTitle: (singular) => `Editar ${singular}`,
|
|
333
|
+
cloneTitle: (singular) => `Clonar ${singular}`,
|
|
334
|
+
entitiesFooter: (count) => `${count} entidades \xB7 Maestro`,
|
|
335
|
+
yes: "sim",
|
|
336
|
+
no: "n\xE3o",
|
|
337
|
+
searchMenu: "Buscar no menu\u2026",
|
|
338
|
+
recents: "Recentes",
|
|
339
|
+
menus: "Menus",
|
|
340
|
+
noMenuResults: "Nada encontrado"
|
|
341
|
+
};
|
|
342
|
+
var LOCALES = { en: EN, "pt-BR": PT_BR };
|
|
343
|
+
function stringsFor(locale) {
|
|
344
|
+
return LOCALES[locale] ?? EN;
|
|
345
|
+
}
|
|
346
|
+
var I18nContext = createContext2(EN);
|
|
347
|
+
var I18nProvider = I18nContext.Provider;
|
|
348
|
+
function useT() {
|
|
349
|
+
return useContext2(I18nContext);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/components/Sidebar.tsx
|
|
353
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
354
|
+
function normalize(text) {
|
|
355
|
+
return text.toLowerCase().normalize("NFD").replace(/\p{M}/gu, "");
|
|
356
|
+
}
|
|
357
|
+
function Highlight({ label, query }) {
|
|
358
|
+
const foldedQuery = normalize(query);
|
|
359
|
+
if (!foldedQuery) return label;
|
|
360
|
+
const folded = [];
|
|
361
|
+
const originalIndex = [];
|
|
362
|
+
for (let i = 0; i < label.length; i += 1) {
|
|
363
|
+
for (const ch of normalize(label[i])) {
|
|
364
|
+
folded.push(ch);
|
|
365
|
+
originalIndex.push(i);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const idx = folded.join("").indexOf(foldedQuery);
|
|
369
|
+
if (idx === -1) return label;
|
|
370
|
+
const start = originalIndex[idx];
|
|
371
|
+
const end = (originalIndex[idx + foldedQuery.length - 1] ?? label.length - 1) + 1;
|
|
372
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
373
|
+
label.slice(0, start),
|
|
374
|
+
/* @__PURE__ */ jsx("mark", { className: "rounded-sm bg-amber-200 text-inherit", children: label.slice(start, end) }),
|
|
375
|
+
label.slice(end)
|
|
376
|
+
] });
|
|
377
|
+
}
|
|
378
|
+
function EntityButton({
|
|
379
|
+
entity,
|
|
380
|
+
active,
|
|
381
|
+
onSelect,
|
|
382
|
+
subtitle,
|
|
383
|
+
query
|
|
384
|
+
}) {
|
|
385
|
+
return /* @__PURE__ */ jsxs(
|
|
386
|
+
"button",
|
|
387
|
+
{
|
|
388
|
+
onClick: () => onSelect(entity.id),
|
|
389
|
+
className: `flex w-full flex-col rounded-md px-3 py-1.5 text-left text-sm transition-colors ${active ? "bg-indigo-100 font-medium text-indigo-700" : "text-slate-700 hover:bg-slate-100"}`,
|
|
390
|
+
children: [
|
|
391
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: query ? /* @__PURE__ */ jsx(Highlight, { label: entity.label.plural, query }) : entity.label.plural }),
|
|
392
|
+
subtitle && /* @__PURE__ */ jsx("span", { className: "truncate text-xs font-normal text-slate-400", children: subtitle })
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
function SectionTitle({ children }) {
|
|
398
|
+
return /* @__PURE__ */ jsx("div", { className: "px-3 pb-1 pt-3 text-xs font-semibold uppercase tracking-wide text-slate-400", children });
|
|
399
|
+
}
|
|
152
400
|
function Sidebar({
|
|
153
401
|
entities,
|
|
154
402
|
activeId,
|
|
155
403
|
onSelect,
|
|
156
|
-
title
|
|
404
|
+
title,
|
|
405
|
+
labels,
|
|
406
|
+
recents = []
|
|
157
407
|
}) {
|
|
158
|
-
|
|
408
|
+
const t = useT();
|
|
409
|
+
const [query, setQuery] = useState2("");
|
|
410
|
+
const [expanded, setExpanded] = useState2({});
|
|
411
|
+
const searchRef = useRef(null);
|
|
412
|
+
const menu = useMemo(() => buildMenu(entities, labels), [entities, labels]);
|
|
413
|
+
const groupOf = useMemo(() => {
|
|
414
|
+
const map = /* @__PURE__ */ new Map();
|
|
415
|
+
for (const group of menu.groups) for (const e of group.entities) map.set(e.id, group.label);
|
|
416
|
+
return map;
|
|
417
|
+
}, [menu]);
|
|
418
|
+
const recentEntities = recents.map((id) => entities.find((e) => e.id === id)).filter((e) => Boolean(e)).slice(0, 6);
|
|
419
|
+
const results = useMemo(() => {
|
|
420
|
+
if (!query) return [];
|
|
421
|
+
const q = normalize(query);
|
|
422
|
+
return entities.filter((e) => normalize(`${e.label.plural} ${e.label.singular} ${e.table}`).includes(q)).sort((a, b) => a.label.plural.localeCompare(b.label.plural));
|
|
423
|
+
}, [entities, query]);
|
|
424
|
+
useEffect2(() => {
|
|
425
|
+
const onKey = (e) => {
|
|
426
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
searchRef.current?.focus();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
document.addEventListener("keydown", onKey);
|
|
432
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
433
|
+
}, []);
|
|
434
|
+
const select = (entityId) => {
|
|
435
|
+
setQuery("");
|
|
436
|
+
onSelect(entityId);
|
|
437
|
+
};
|
|
438
|
+
const isExpanded = (groupId, hasActive) => expanded[groupId] ?? hasActive;
|
|
439
|
+
return /* @__PURE__ */ jsxs("aside", { className: "flex w-64 shrink-0 flex-col border-r border-slate-200 bg-slate-50", children: [
|
|
159
440
|
/* @__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("
|
|
161
|
-
"
|
|
441
|
+
/* @__PURE__ */ jsx("div", { className: "px-2 pt-2", children: /* @__PURE__ */ jsx(
|
|
442
|
+
"input",
|
|
162
443
|
{
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
444
|
+
ref: searchRef,
|
|
445
|
+
type: "search",
|
|
446
|
+
value: query,
|
|
447
|
+
onChange: (e) => setQuery(e.target.value),
|
|
448
|
+
onKeyDown: (e) => {
|
|
449
|
+
if (e.key === "Escape") setQuery("");
|
|
450
|
+
if (e.key === "Enter" && results[0]) select(results[0].id);
|
|
451
|
+
},
|
|
452
|
+
placeholder: `${t.searchMenu} (Ctrl+K)`,
|
|
453
|
+
className: "w-full rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
454
|
+
}
|
|
455
|
+
) }),
|
|
456
|
+
/* @__PURE__ */ jsx("nav", { className: "flex-1 overflow-y-auto p-2", children: query ? results.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-3 py-2 text-sm text-slate-400", children: t.noMenuResults }) : results.map((e) => /* @__PURE__ */ jsx(
|
|
457
|
+
EntityButton,
|
|
458
|
+
{
|
|
459
|
+
entity: e,
|
|
460
|
+
active: e.id === activeId,
|
|
461
|
+
onSelect: select,
|
|
462
|
+
subtitle: groupOf.get(e.id),
|
|
463
|
+
query
|
|
166
464
|
},
|
|
167
465
|
e.id
|
|
168
|
-
))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
466
|
+
)) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
467
|
+
recentEntities.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
468
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: t.recents }),
|
|
469
|
+
recentEntities.map((e) => /* @__PURE__ */ jsx(EntityButton, { entity: e, active: e.id === activeId, onSelect: select }, e.id))
|
|
470
|
+
] }),
|
|
471
|
+
/* @__PURE__ */ jsx(SectionTitle, { children: t.menus }),
|
|
472
|
+
menu.groups.map((group) => {
|
|
473
|
+
const hasActive = group.entities.some((e) => e.id === activeId);
|
|
474
|
+
const open = isExpanded(group.id, hasActive);
|
|
475
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
476
|
+
/* @__PURE__ */ jsxs(
|
|
477
|
+
"button",
|
|
478
|
+
{
|
|
479
|
+
onClick: () => setExpanded((prev) => ({ ...prev, [group.id]: !open })),
|
|
480
|
+
className: "flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-sm font-medium text-slate-600 hover:bg-slate-100",
|
|
481
|
+
children: [
|
|
482
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-slate-400", children: open ? "\u25BE" : "\u25B8" }),
|
|
483
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: group.label }),
|
|
484
|
+
/* @__PURE__ */ jsx("span", { className: "ml-auto text-xs text-slate-400", children: group.entities.length })
|
|
485
|
+
]
|
|
486
|
+
}
|
|
487
|
+
),
|
|
488
|
+
open && /* @__PURE__ */ jsx("div", { className: "ml-3 border-l border-slate-200 pl-1", children: group.entities.map((e) => /* @__PURE__ */ jsx(EntityButton, { entity: e, active: e.id === activeId, onSelect: select }, e.id)) })
|
|
489
|
+
] }, group.id);
|
|
490
|
+
}),
|
|
491
|
+
menu.ungrouped.map((e) => /* @__PURE__ */ jsx(EntityButton, { entity: e, active: e.id === activeId, onSelect: select }, e.id))
|
|
492
|
+
] }) }),
|
|
493
|
+
/* @__PURE__ */ jsx("div", { className: "border-t border-slate-200 px-4 py-3 text-xs text-slate-400", children: t.entitiesFooter(entities.length) })
|
|
173
494
|
] });
|
|
174
495
|
}
|
|
175
496
|
|
|
176
497
|
// src/components/EntityList.tsx
|
|
177
|
-
import { useMemo, useState as
|
|
498
|
+
import { useMemo as useMemo2, useState as useState5 } from "react";
|
|
178
499
|
|
|
179
500
|
// src/components/ui.tsx
|
|
180
501
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -223,57 +544,235 @@ function ErrorBanner({ error }) {
|
|
|
223
544
|
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
545
|
}
|
|
225
546
|
|
|
547
|
+
// src/components/RelationValue.tsx
|
|
548
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
549
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
550
|
+
var displayCache = /* @__PURE__ */ new Map();
|
|
551
|
+
function resolveDisplay(client, entityId, displayField, id) {
|
|
552
|
+
const key = `${entityId}/${id}`;
|
|
553
|
+
let cached = displayCache.get(key);
|
|
554
|
+
if (!cached) {
|
|
555
|
+
cached = client.findById(entityId, id).then((record) => {
|
|
556
|
+
const value = record[displayField];
|
|
557
|
+
return value === null || value === void 0 || value === "" ? void 0 : String(value);
|
|
558
|
+
}).catch(() => void 0);
|
|
559
|
+
displayCache.set(key, cached);
|
|
560
|
+
}
|
|
561
|
+
return cached;
|
|
562
|
+
}
|
|
563
|
+
function shortId(id) {
|
|
564
|
+
return id.length > 12 ? `${id.slice(0, 6)}\u2026${id.slice(-4)}` : id;
|
|
565
|
+
}
|
|
566
|
+
function RelationValue({ field, value }) {
|
|
567
|
+
const client = useClient();
|
|
568
|
+
const target = useEntity(field.relationEntity);
|
|
569
|
+
const id = String(value);
|
|
570
|
+
const [display, setDisplay] = useState3(void 0);
|
|
571
|
+
useEffect3(() => {
|
|
572
|
+
let cancelled = false;
|
|
573
|
+
setDisplay(void 0);
|
|
574
|
+
if (!target || target.displayField === target.primaryKey) return;
|
|
575
|
+
resolveDisplay(client, target.id, target.displayField, id).then((resolved) => {
|
|
576
|
+
if (!cancelled) setDisplay(resolved);
|
|
577
|
+
});
|
|
578
|
+
return () => {
|
|
579
|
+
cancelled = true;
|
|
580
|
+
};
|
|
581
|
+
}, [client, target, id]);
|
|
582
|
+
return /* @__PURE__ */ jsxs3(
|
|
583
|
+
"span",
|
|
584
|
+
{
|
|
585
|
+
title: id,
|
|
586
|
+
className: "inline-flex max-w-56 items-center gap-1 truncate rounded border border-slate-200 bg-slate-50 px-1.5 py-0.5 text-xs font-medium text-slate-700",
|
|
587
|
+
children: [
|
|
588
|
+
target && /* @__PURE__ */ jsxs3("span", { className: "text-slate-400", children: [
|
|
589
|
+
target.label.singular,
|
|
590
|
+
":"
|
|
591
|
+
] }),
|
|
592
|
+
/* @__PURE__ */ jsx3("span", { className: "truncate", children: display ?? shortId(id) })
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
226
598
|
// src/components/FieldValue.tsx
|
|
227
|
-
import { jsx as
|
|
599
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
228
600
|
function enumLabel(field, value) {
|
|
229
601
|
const opt = field.enumOptions?.find((o) => o.value === value);
|
|
230
602
|
return opt ? opt.label : String(value);
|
|
231
603
|
}
|
|
232
|
-
function FieldValue({ field, value }) {
|
|
604
|
+
function FieldValue({ field, value, compact = false }) {
|
|
605
|
+
const t = useT();
|
|
233
606
|
if (value === null || value === void 0 || value === "") {
|
|
234
|
-
return /* @__PURE__ */
|
|
607
|
+
return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "\u2014" });
|
|
235
608
|
}
|
|
236
609
|
if (field.sensitive) {
|
|
237
|
-
return /* @__PURE__ */
|
|
610
|
+
return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "\u2022\u2022\u2022\u2022\u2022\u2022" });
|
|
238
611
|
}
|
|
239
612
|
switch (field.type) {
|
|
240
613
|
case "boolean":
|
|
241
|
-
return value ? /* @__PURE__ */
|
|
614
|
+
return value ? /* @__PURE__ */ jsx4(Badge, { tone: "green", children: t.yes }) : /* @__PURE__ */ jsx4(Badge, { tone: "slate", children: t.no });
|
|
242
615
|
case "enum":
|
|
243
|
-
return /* @__PURE__ */
|
|
616
|
+
return /* @__PURE__ */ jsx4(Badge, { tone: "indigo", children: enumLabel(field, value) });
|
|
617
|
+
case "relation":
|
|
618
|
+
return field.relationEntity ? /* @__PURE__ */ jsx4(RelationValue, { field, value }) : /* @__PURE__ */ jsx4("span", { className: "text-slate-700", children: String(value) });
|
|
244
619
|
case "date":
|
|
245
620
|
case "datetime": {
|
|
246
621
|
const d = new Date(String(value));
|
|
247
|
-
return /* @__PURE__ */
|
|
622
|
+
return /* @__PURE__ */ jsx4("span", { className: "whitespace-nowrap", children: Number.isNaN(d.getTime()) ? String(value) : d.toLocaleString() });
|
|
248
623
|
}
|
|
249
624
|
case "json":
|
|
250
|
-
return /* @__PURE__ */
|
|
625
|
+
return /* @__PURE__ */ jsx4("code", { className: `block overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700 ${compact ? "max-w-xs truncate" : "max-w-md"}`, children: JSON.stringify(value) });
|
|
251
626
|
default:
|
|
252
|
-
return /* @__PURE__ */
|
|
627
|
+
return /* @__PURE__ */ jsx4("span", { className: `text-slate-700 ${compact ? "block max-w-64 truncate" : ""}`, children: String(value) });
|
|
253
628
|
}
|
|
254
629
|
}
|
|
255
630
|
|
|
631
|
+
// src/components/menu.tsx
|
|
632
|
+
import { useEffect as useEffect4, useLayoutEffect, useRef as useRef2, useState as useState4 } from "react";
|
|
633
|
+
import { createPortal } from "react-dom";
|
|
634
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
635
|
+
function PopMenu({ anchor, items, onClose }) {
|
|
636
|
+
const ref = useRef2(null);
|
|
637
|
+
const [pos, setPos] = useState4(anchor);
|
|
638
|
+
useLayoutEffect(() => {
|
|
639
|
+
const el = ref.current;
|
|
640
|
+
if (!el) return;
|
|
641
|
+
const rect = el.getBoundingClientRect();
|
|
642
|
+
setPos({
|
|
643
|
+
x: Math.max(4, Math.min(anchor.x, window.innerWidth - rect.width - 8)),
|
|
644
|
+
y: Math.max(4, Math.min(anchor.y, window.innerHeight - rect.height - 8))
|
|
645
|
+
});
|
|
646
|
+
}, [anchor]);
|
|
647
|
+
useEffect4(() => {
|
|
648
|
+
const onDown = (e) => {
|
|
649
|
+
if (ref.current && !ref.current.contains(e.target)) onClose();
|
|
650
|
+
};
|
|
651
|
+
const onKey = (e) => {
|
|
652
|
+
if (e.key === "Escape") onClose();
|
|
653
|
+
};
|
|
654
|
+
document.addEventListener("mousedown", onDown);
|
|
655
|
+
document.addEventListener("keydown", onKey);
|
|
656
|
+
window.addEventListener("scroll", onClose, true);
|
|
657
|
+
window.addEventListener("resize", onClose);
|
|
658
|
+
return () => {
|
|
659
|
+
document.removeEventListener("mousedown", onDown);
|
|
660
|
+
document.removeEventListener("keydown", onKey);
|
|
661
|
+
window.removeEventListener("scroll", onClose, true);
|
|
662
|
+
window.removeEventListener("resize", onClose);
|
|
663
|
+
};
|
|
664
|
+
}, [onClose]);
|
|
665
|
+
return createPortal(
|
|
666
|
+
/* @__PURE__ */ jsx5(
|
|
667
|
+
"div",
|
|
668
|
+
{
|
|
669
|
+
ref,
|
|
670
|
+
role: "menu",
|
|
671
|
+
style: { position: "fixed", left: pos.x, top: pos.y, zIndex: 50 },
|
|
672
|
+
className: "min-w-44 rounded-md border border-slate-200 bg-white py-1 shadow-lg",
|
|
673
|
+
children: items.map((item) => /* @__PURE__ */ jsx5(
|
|
674
|
+
"button",
|
|
675
|
+
{
|
|
676
|
+
role: "menuitem",
|
|
677
|
+
onClick: () => {
|
|
678
|
+
onClose();
|
|
679
|
+
item.onSelect();
|
|
680
|
+
},
|
|
681
|
+
className: `block w-full px-3 py-1.5 text-left text-sm ${item.danger ? "text-red-600 hover:bg-red-50" : "text-slate-700 hover:bg-slate-100"}`,
|
|
682
|
+
children: item.label
|
|
683
|
+
},
|
|
684
|
+
item.id
|
|
685
|
+
))
|
|
686
|
+
}
|
|
687
|
+
),
|
|
688
|
+
document.body
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
function MenuButton({ items, label }) {
|
|
692
|
+
const [anchor, setAnchor] = useState4(void 0);
|
|
693
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
694
|
+
/* @__PURE__ */ jsx5(
|
|
695
|
+
"button",
|
|
696
|
+
{
|
|
697
|
+
type: "button",
|
|
698
|
+
"aria-label": label,
|
|
699
|
+
title: label,
|
|
700
|
+
onClick: (e) => {
|
|
701
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
702
|
+
setAnchor({ x: rect.right - 176, y: rect.bottom + 4 });
|
|
703
|
+
},
|
|
704
|
+
className: "rounded-md px-2 py-1 text-slate-500 hover:bg-slate-100 hover:text-slate-900",
|
|
705
|
+
children: "\u22EE"
|
|
706
|
+
}
|
|
707
|
+
),
|
|
708
|
+
anchor && /* @__PURE__ */ jsx5(PopMenu, { anchor, items, onClose: () => setAnchor(void 0) })
|
|
709
|
+
] });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/clipboard.ts
|
|
713
|
+
async function copyText(text) {
|
|
714
|
+
if (navigator.clipboard?.writeText) {
|
|
715
|
+
try {
|
|
716
|
+
await navigator.clipboard.writeText(text);
|
|
717
|
+
return;
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const textarea = document.createElement("textarea");
|
|
722
|
+
textarea.value = text;
|
|
723
|
+
textarea.style.position = "fixed";
|
|
724
|
+
textarea.style.opacity = "0";
|
|
725
|
+
document.body.appendChild(textarea);
|
|
726
|
+
textarea.select();
|
|
727
|
+
document.execCommand("copy");
|
|
728
|
+
textarea.remove();
|
|
729
|
+
}
|
|
730
|
+
|
|
256
731
|
// src/components/EntityList.tsx
|
|
257
|
-
import { jsx as
|
|
732
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
258
733
|
var PAGE_SIZE = 20;
|
|
734
|
+
function filterFor(field, value) {
|
|
735
|
+
if (value === null || value === void 0) return { field: field.name, operator: "isNull" };
|
|
736
|
+
if (field.type === "boolean") return { field: field.name, operator: value ? "isTrue" : "isFalse" };
|
|
737
|
+
if (field.type === "json" || field.type === "array") return void 0;
|
|
738
|
+
return { field: field.name, operator: "equals", value: String(value) };
|
|
739
|
+
}
|
|
740
|
+
function filterChipText(entity, filter) {
|
|
741
|
+
const label = entity.fields.find((f) => f.name === filter.field)?.label ?? filter.field;
|
|
742
|
+
if (filter.operator === "isNull") return `${label} = \u2205`;
|
|
743
|
+
if (filter.operator === "isTrue") return `${label} = \u2713`;
|
|
744
|
+
if (filter.operator === "isFalse") return `${label} = \u2717`;
|
|
745
|
+
const value = filter.value ?? "";
|
|
746
|
+
return `${label} = ${value.length > 24 ? `${value.slice(0, 24)}\u2026` : value}`;
|
|
747
|
+
}
|
|
259
748
|
function EntityList({
|
|
260
749
|
entity,
|
|
261
750
|
onOpen,
|
|
262
751
|
onCreate,
|
|
263
|
-
onEdit
|
|
752
|
+
onEdit,
|
|
753
|
+
onClone,
|
|
754
|
+
onNavigate,
|
|
755
|
+
initialFilters
|
|
264
756
|
}) {
|
|
265
757
|
const client = useClient();
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
const
|
|
758
|
+
const t = useT();
|
|
759
|
+
const entities = useEntities();
|
|
760
|
+
const relatedLabel = (entityId) => entities.find((e) => e.id === entityId)?.label.singular ?? entityId;
|
|
761
|
+
const columns = useMemo2(() => primaryListFields(entity), [entity]);
|
|
762
|
+
const searchFields = useMemo2(() => entity.fields.filter((f) => f.searchable).map((f) => f.name), [entity]);
|
|
763
|
+
const [page, setPage] = useState5(1);
|
|
764
|
+
const [search, setSearch] = useState5("");
|
|
765
|
+
const [sort, setSort] = useState5(void 0);
|
|
766
|
+
const [filters, setFilters] = useState5(initialFilters ?? []);
|
|
767
|
+
const [cellMenu, setCellMenu] = useState5(void 0);
|
|
768
|
+
const [copied, setCopied] = useState5(false);
|
|
271
769
|
const query = {
|
|
272
770
|
page,
|
|
273
771
|
pageSize: PAGE_SIZE,
|
|
274
772
|
search: search || void 0,
|
|
275
773
|
searchFields,
|
|
276
|
-
sort: sort ? [sort] : void 0
|
|
774
|
+
sort: sort ? [sort] : void 0,
|
|
775
|
+
filters: filters.length ? filters : void 0
|
|
277
776
|
};
|
|
278
777
|
const { data, error, loading, reload } = useEntityList(entity.id, query);
|
|
279
778
|
const toggleSort = (fieldName) => {
|
|
@@ -281,27 +780,81 @@ function EntityList({
|
|
|
281
780
|
(prev) => prev?.field === fieldName ? { field: fieldName, direction: prev.direction === "asc" ? "desc" : "asc" } : { field: fieldName, direction: "asc" }
|
|
282
781
|
);
|
|
283
782
|
};
|
|
783
|
+
const flashCopied = () => {
|
|
784
|
+
setCopied(true);
|
|
785
|
+
setTimeout(() => setCopied(false), 1500);
|
|
786
|
+
};
|
|
787
|
+
const copy = async (text) => {
|
|
788
|
+
await copyText(text);
|
|
789
|
+
flashCopied();
|
|
790
|
+
};
|
|
791
|
+
const addFilter = (filter) => {
|
|
792
|
+
setFilters((prev) => {
|
|
793
|
+
const others = prev.filter((f) => f.field !== filter.field);
|
|
794
|
+
return [...others, filter];
|
|
795
|
+
});
|
|
796
|
+
setPage(1);
|
|
797
|
+
};
|
|
284
798
|
const onDelete = async (id) => {
|
|
285
|
-
if (!globalThis.confirm(
|
|
799
|
+
if (!globalThis.confirm(t.deleteConfirm(entity.label.singular))) return;
|
|
286
800
|
await client.remove(entity.id, id);
|
|
287
801
|
reload();
|
|
288
802
|
};
|
|
803
|
+
const rowMenuItems = (record) => {
|
|
804
|
+
const id = String(record[entity.primaryKey]);
|
|
805
|
+
const items = [{ id: "view", label: `\u{1F441} ${t.view}`, onSelect: () => onOpen(id) }];
|
|
806
|
+
if (entity.capabilities.update) items.push({ id: "edit", label: `\u270F\uFE0F ${t.edit}`, onSelect: () => onEdit(id) });
|
|
807
|
+
if (entity.capabilities.clone && entity.capabilities.create) {
|
|
808
|
+
items.push({ id: "clone", label: `\u29C9 ${t.clone}`, onSelect: () => onClone(id) });
|
|
809
|
+
}
|
|
810
|
+
items.push(
|
|
811
|
+
{ id: "copy-id", label: `\u2398 ${t.copyId}`, onSelect: () => void copy(id) },
|
|
812
|
+
{ id: "copy-object", label: `{} ${t.copyObject}`, onSelect: () => void copy(JSON.stringify(record, null, 2)) }
|
|
813
|
+
);
|
|
814
|
+
if (entity.capabilities.delete) {
|
|
815
|
+
items.push({ id: "remove", label: `\u{1F5D1} ${t.remove}`, danger: true, onSelect: () => void onDelete(id) });
|
|
816
|
+
}
|
|
817
|
+
return items;
|
|
818
|
+
};
|
|
819
|
+
const cellMenuItems = (state) => {
|
|
820
|
+
const { field, record } = state;
|
|
821
|
+
const id = String(record[entity.primaryKey]);
|
|
822
|
+
const value = record[field.name];
|
|
823
|
+
const items = [];
|
|
824
|
+
const filter = filterFor(field, value);
|
|
825
|
+
if (filter) items.push({ id: "filter", label: `\u25BC ${t.addToFilter}`, onSelect: () => addFilter(filter) });
|
|
826
|
+
items.push({ id: "view", label: `\u{1F441} ${t.view}`, onSelect: () => onOpen(id) });
|
|
827
|
+
if (entity.capabilities.update) items.push({ id: "edit", label: `\u270F\uFE0F ${t.edit}`, onSelect: () => onEdit(id) });
|
|
828
|
+
if (field.type === "relation" && field.relationEntity && value !== null && value !== void 0) {
|
|
829
|
+
const targetId = field.relationEntity;
|
|
830
|
+
items.push({
|
|
831
|
+
id: "open-related",
|
|
832
|
+
label: `\u2197 ${t.openRelated(relatedLabel(targetId))}`,
|
|
833
|
+
onSelect: () => onNavigate(targetId, String(value))
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (value !== null && value !== void 0) {
|
|
837
|
+
items.push({
|
|
838
|
+
id: "copy-value",
|
|
839
|
+
label: `\u2398 ${t.copyValue}`,
|
|
840
|
+
onSelect: () => void copy(typeof value === "object" ? JSON.stringify(value) : String(value))
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return items;
|
|
844
|
+
};
|
|
289
845
|
const totalPages = data?.totalPages ?? (data ? Math.max(1, Math.ceil(data.total / PAGE_SIZE)) : 1);
|
|
290
|
-
return /* @__PURE__ */
|
|
291
|
-
/* @__PURE__ */
|
|
292
|
-
/* @__PURE__ */
|
|
293
|
-
/* @__PURE__ */
|
|
294
|
-
data && /* @__PURE__ */
|
|
295
|
-
data.total,
|
|
296
|
-
" records"
|
|
297
|
-
] })
|
|
846
|
+
return /* @__PURE__ */ jsxs5("section", { className: "space-y-4", children: [
|
|
847
|
+
/* @__PURE__ */ jsxs5("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
848
|
+
/* @__PURE__ */ jsxs5("div", { children: [
|
|
849
|
+
/* @__PURE__ */ jsx6("h1", { className: "text-lg font-semibold text-slate-900", children: entity.label.plural }),
|
|
850
|
+
data && /* @__PURE__ */ jsx6("p", { className: "text-sm text-slate-500", children: t.records(data.total) })
|
|
298
851
|
] }),
|
|
299
|
-
/* @__PURE__ */
|
|
300
|
-
searchFields.length > 0 && /* @__PURE__ */
|
|
852
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
|
|
853
|
+
searchFields.length > 0 && /* @__PURE__ */ jsx6(
|
|
301
854
|
"input",
|
|
302
855
|
{
|
|
303
856
|
type: "search",
|
|
304
|
-
placeholder:
|
|
857
|
+
placeholder: t.searchPlaceholder(entity.label.plural),
|
|
305
858
|
value: search,
|
|
306
859
|
onChange: (e) => {
|
|
307
860
|
setSearch(e.target.value);
|
|
@@ -310,13 +863,46 @@ function EntityList({
|
|
|
310
863
|
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
864
|
}
|
|
312
865
|
),
|
|
313
|
-
entity.capabilities.create && /* @__PURE__ */
|
|
866
|
+
entity.capabilities.create && /* @__PURE__ */ jsx6(Button, { variant: "primary", onClick: onCreate, children: t.newRecord })
|
|
314
867
|
] })
|
|
315
868
|
] }),
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
869
|
+
filters.length > 0 && /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
|
|
870
|
+
/* @__PURE__ */ jsxs5("span", { className: "text-slate-500", children: [
|
|
871
|
+
t.filters,
|
|
872
|
+
":"
|
|
873
|
+
] }),
|
|
874
|
+
filters.map((f) => /* @__PURE__ */ jsxs5(
|
|
875
|
+
"button",
|
|
876
|
+
{
|
|
877
|
+
onClick: () => {
|
|
878
|
+
setFilters((prev) => prev.filter((p) => p.field !== f.field));
|
|
879
|
+
setPage(1);
|
|
880
|
+
},
|
|
881
|
+
title: f.value,
|
|
882
|
+
className: "inline-flex items-center gap-1 rounded-full bg-indigo-50 px-2.5 py-0.5 text-xs font-medium text-indigo-700 hover:bg-indigo-100",
|
|
883
|
+
children: [
|
|
884
|
+
filterChipText(entity, f),
|
|
885
|
+
/* @__PURE__ */ jsx6("span", { "aria-hidden": true, children: "\xD7" })
|
|
886
|
+
]
|
|
887
|
+
},
|
|
888
|
+
f.field
|
|
889
|
+
)),
|
|
890
|
+
/* @__PURE__ */ jsx6(
|
|
891
|
+
"button",
|
|
892
|
+
{
|
|
893
|
+
onClick: () => {
|
|
894
|
+
setFilters([]);
|
|
895
|
+
setPage(1);
|
|
896
|
+
},
|
|
897
|
+
className: "text-xs text-slate-500 underline hover:text-slate-700",
|
|
898
|
+
children: t.clearFilters
|
|
899
|
+
}
|
|
900
|
+
)
|
|
901
|
+
] }),
|
|
902
|
+
error && /* @__PURE__ */ jsx6(ErrorBanner, { error }),
|
|
903
|
+
loading && !data ? /* @__PURE__ */ jsx6(Spinner, { label: t.loading }) : data && data.records.length === 0 ? /* @__PURE__ */ jsx6(EmptyState, { title: t.noRecords, hint: search || filters.length ? t.tryDifferentSearch : void 0 }) : /* @__PURE__ */ jsx6("div", { className: "overflow-x-auto rounded-lg border border-slate-200", children: /* @__PURE__ */ jsxs5("table", { className: "min-w-full divide-y divide-slate-200 text-sm", children: [
|
|
904
|
+
/* @__PURE__ */ jsx6("thead", { className: "bg-slate-50", children: /* @__PURE__ */ jsxs5("tr", { children: [
|
|
905
|
+
columns.map((f) => /* @__PURE__ */ jsxs5(
|
|
320
906
|
"th",
|
|
321
907
|
{
|
|
322
908
|
onClick: () => f.sortable && toggleSort(f.name),
|
|
@@ -328,78 +914,128 @@ function EntityList({
|
|
|
328
914
|
},
|
|
329
915
|
f.name
|
|
330
916
|
)),
|
|
331
|
-
/* @__PURE__ */
|
|
917
|
+
/* @__PURE__ */ jsx6("th", { className: "px-3 py-2 text-right font-medium text-slate-600", children: t.actions })
|
|
332
918
|
] }) }),
|
|
333
|
-
/* @__PURE__ */
|
|
919
|
+
/* @__PURE__ */ jsx6("tbody", { className: "divide-y divide-slate-100 bg-white", children: data?.records.map((record, i) => {
|
|
334
920
|
const id = String(record[entity.primaryKey]);
|
|
335
|
-
return /* @__PURE__ */
|
|
336
|
-
columns.map((f) => /* @__PURE__ */
|
|
921
|
+
return /* @__PURE__ */ jsxs5("tr", { className: "hover:bg-slate-50", children: [
|
|
922
|
+
columns.map((f) => /* @__PURE__ */ jsx6(
|
|
337
923
|
"td",
|
|
338
924
|
{
|
|
339
925
|
className: "cursor-pointer px-3 py-2 align-top",
|
|
340
|
-
onClick: () =>
|
|
341
|
-
|
|
926
|
+
onClick: (e) => setCellMenu({ anchor: { x: e.clientX, y: e.clientY }, field: f, record }),
|
|
927
|
+
onDoubleClick: () => onOpen(id),
|
|
928
|
+
children: /* @__PURE__ */ jsx6(FieldValue, { field: f, value: record[f.name], compact: true })
|
|
342
929
|
},
|
|
343
930
|
f.name
|
|
344
931
|
)),
|
|
345
|
-
/* @__PURE__ */
|
|
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
|
-
] })
|
|
932
|
+
/* @__PURE__ */ jsx6("td", { className: "whitespace-nowrap px-3 py-2 text-right", children: /* @__PURE__ */ jsx6(MenuButton, { label: t.actions, items: rowMenuItems(record) }) })
|
|
349
933
|
] }, id || i);
|
|
350
934
|
}) })
|
|
351
935
|
] }) }),
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
totalPages
|
|
359
|
-
] }),
|
|
360
|
-
/* @__PURE__ */ jsx4(Button, { variant: "secondary", disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), children: "Next" })
|
|
936
|
+
cellMenu && /* @__PURE__ */ jsx6(PopMenu, { anchor: cellMenu.anchor, items: cellMenuItems(cellMenu), onClose: () => setCellMenu(void 0) }),
|
|
937
|
+
copied && /* @__PURE__ */ jsx6("div", { className: "fixed bottom-4 right-4 z-50 rounded-md bg-slate-900 px-3 py-1.5 text-sm text-white shadow-lg", children: t.copied }),
|
|
938
|
+
data && totalPages > 1 && /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-end gap-2 text-sm", children: [
|
|
939
|
+
/* @__PURE__ */ jsx6(Button, { variant: "secondary", disabled: page <= 1, onClick: () => setPage((p) => p - 1), children: t.previous }),
|
|
940
|
+
/* @__PURE__ */ jsx6("span", { className: "text-slate-500", children: t.page(page, totalPages) }),
|
|
941
|
+
/* @__PURE__ */ jsx6(Button, { variant: "secondary", disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), children: t.next })
|
|
361
942
|
] })
|
|
362
943
|
] });
|
|
363
944
|
}
|
|
364
945
|
|
|
365
946
|
// src/components/EntityDetail.tsx
|
|
366
|
-
import {
|
|
947
|
+
import { useState as useState6 } from "react";
|
|
948
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
367
949
|
function EntityDetail({
|
|
368
950
|
entity,
|
|
369
951
|
id,
|
|
370
952
|
onBack,
|
|
371
|
-
onEdit
|
|
953
|
+
onEdit,
|
|
954
|
+
onClone,
|
|
955
|
+
onDeleted,
|
|
956
|
+
onNavigate
|
|
372
957
|
}) {
|
|
958
|
+
const client = useClient();
|
|
959
|
+
const t = useT();
|
|
960
|
+
const entities = useEntities();
|
|
373
961
|
const { data, error, loading } = useEntityRecord(entity.id, id);
|
|
374
962
|
const fields = detailFields(entity);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
963
|
+
const [copied, setCopied] = useState6(false);
|
|
964
|
+
const copy = async (text) => {
|
|
965
|
+
await copyText(text);
|
|
966
|
+
setCopied(true);
|
|
967
|
+
setTimeout(() => setCopied(false), 1500);
|
|
968
|
+
};
|
|
969
|
+
const onDelete = async () => {
|
|
970
|
+
if (!globalThis.confirm(t.deleteConfirm(entity.label.singular))) return;
|
|
971
|
+
await client.remove(entity.id, id);
|
|
972
|
+
onDeleted();
|
|
973
|
+
};
|
|
974
|
+
const menuItems = [];
|
|
975
|
+
if (entity.capabilities.clone && entity.capabilities.create) {
|
|
976
|
+
menuItems.push({ id: "clone", label: `\u29C9 ${t.clone}`, onSelect: onClone });
|
|
977
|
+
}
|
|
978
|
+
menuItems.push({ id: "copy-id", label: `\u2398 ${t.copyId}`, onSelect: () => void copy(id) });
|
|
979
|
+
if (data) {
|
|
980
|
+
menuItems.push({
|
|
981
|
+
id: "copy-object",
|
|
982
|
+
label: `{} ${t.copyObject}`,
|
|
983
|
+
onSelect: () => void copy(JSON.stringify(data, null, 2))
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
if (entity.capabilities.delete) {
|
|
987
|
+
menuItems.push({ id: "remove", label: `\u{1F5D1} ${t.remove}`, danger: true, onSelect: () => void onDelete() });
|
|
988
|
+
}
|
|
989
|
+
return /* @__PURE__ */ jsxs6("section", { className: "space-y-4", children: [
|
|
990
|
+
/* @__PURE__ */ jsxs6("header", { className: "flex items-center justify-between gap-3", children: [
|
|
991
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
992
|
+
/* @__PURE__ */ jsxs6("button", { onClick: onBack, className: "text-sm text-indigo-600 hover:underline", children: [
|
|
379
993
|
"\u2190 ",
|
|
380
994
|
entity.label.plural
|
|
381
995
|
] }),
|
|
382
|
-
/* @__PURE__ */
|
|
996
|
+
/* @__PURE__ */ jsxs6("h1", { className: "text-lg font-semibold text-slate-900", children: [
|
|
383
997
|
entity.label.singular,
|
|
384
998
|
" ",
|
|
385
999
|
data ? String(data[entity.displayField] ?? id) : ""
|
|
386
1000
|
] })
|
|
387
1001
|
] }),
|
|
388
|
-
|
|
1002
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
|
|
1003
|
+
entity.capabilities.update && /* @__PURE__ */ jsx7(Button, { variant: "primary", onClick: onEdit, children: t.edit }),
|
|
1004
|
+
/* @__PURE__ */ jsx7(MenuButton, { label: t.actions, items: menuItems })
|
|
1005
|
+
] })
|
|
389
1006
|
] }),
|
|
390
|
-
error && /* @__PURE__ */
|
|
391
|
-
loading && !data ? /* @__PURE__ */
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1007
|
+
error && /* @__PURE__ */ jsx7(ErrorBanner, { error }),
|
|
1008
|
+
loading && !data ? /* @__PURE__ */ jsx7(Spinner, {}) : data && /* @__PURE__ */ jsx7("dl", { className: "divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white", children: fields.map((f) => {
|
|
1009
|
+
const value = data[f.name];
|
|
1010
|
+
const isRelation = f.type === "relation" && f.relationEntity && value !== null && value !== void 0;
|
|
1011
|
+
const target = isRelation ? entities.find((e) => e.id === f.relationEntity) : void 0;
|
|
1012
|
+
return /* @__PURE__ */ jsxs6("div", { className: "grid grid-cols-3 gap-4 px-4 py-3", children: [
|
|
1013
|
+
/* @__PURE__ */ jsx7("dt", { className: "text-sm font-medium text-slate-500", children: f.label }),
|
|
1014
|
+
/* @__PURE__ */ jsxs6("dd", { className: "col-span-2 flex items-center gap-2 text-sm", children: [
|
|
1015
|
+
/* @__PURE__ */ jsx7(FieldValue, { field: f, value }),
|
|
1016
|
+
isRelation && /* @__PURE__ */ jsxs6(
|
|
1017
|
+
"button",
|
|
1018
|
+
{
|
|
1019
|
+
onClick: () => onNavigate(f.relationEntity, String(value)),
|
|
1020
|
+
className: "text-xs text-indigo-600 hover:underline",
|
|
1021
|
+
children: [
|
|
1022
|
+
t.openRelated(target?.label.singular ?? f.relationEntity),
|
|
1023
|
+
" \u2197"
|
|
1024
|
+
]
|
|
1025
|
+
}
|
|
1026
|
+
)
|
|
1027
|
+
] })
|
|
1028
|
+
] }, f.name);
|
|
1029
|
+
}) }),
|
|
1030
|
+
copied && /* @__PURE__ */ jsx7("div", { className: "fixed bottom-4 right-4 z-50 rounded-md bg-slate-900 px-3 py-1.5 text-sm text-white shadow-lg", children: t.copied })
|
|
395
1031
|
] });
|
|
396
1032
|
}
|
|
397
1033
|
|
|
398
1034
|
// src/components/EntityForm.tsx
|
|
399
|
-
import { useEffect as
|
|
1035
|
+
import { useEffect as useEffect5, useState as useState7 } from "react";
|
|
400
1036
|
|
|
401
1037
|
// src/components/FieldInput.tsx
|
|
402
|
-
import { jsx as
|
|
1038
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
403
1039
|
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
1040
|
function FieldInput({
|
|
405
1041
|
field,
|
|
@@ -410,7 +1046,7 @@ function FieldInput({
|
|
|
410
1046
|
const id = `field-${field.name}`;
|
|
411
1047
|
switch (field.type) {
|
|
412
1048
|
case "boolean":
|
|
413
|
-
return /* @__PURE__ */
|
|
1049
|
+
return /* @__PURE__ */ jsx8(
|
|
414
1050
|
"input",
|
|
415
1051
|
{
|
|
416
1052
|
id,
|
|
@@ -426,7 +1062,7 @@ function FieldInput({
|
|
|
426
1062
|
case "float":
|
|
427
1063
|
case "decimal":
|
|
428
1064
|
case "currency":
|
|
429
|
-
return /* @__PURE__ */
|
|
1065
|
+
return /* @__PURE__ */ jsx8(
|
|
430
1066
|
"input",
|
|
431
1067
|
{
|
|
432
1068
|
id,
|
|
@@ -440,7 +1076,7 @@ function FieldInput({
|
|
|
440
1076
|
}
|
|
441
1077
|
);
|
|
442
1078
|
case "enum":
|
|
443
|
-
return /* @__PURE__ */
|
|
1079
|
+
return /* @__PURE__ */ jsxs7(
|
|
444
1080
|
"select",
|
|
445
1081
|
{
|
|
446
1082
|
id,
|
|
@@ -449,13 +1085,13 @@ function FieldInput({
|
|
|
449
1085
|
onChange: (e) => onChange(e.target.value),
|
|
450
1086
|
className: inputClass,
|
|
451
1087
|
children: [
|
|
452
|
-
/* @__PURE__ */
|
|
453
|
-
field.enumOptions?.map((o) => /* @__PURE__ */
|
|
1088
|
+
/* @__PURE__ */ jsx8("option", { value: "", children: "\u2014" }),
|
|
1089
|
+
field.enumOptions?.map((o) => /* @__PURE__ */ jsx8("option", { value: String(o.value), children: o.label }, String(o.value)))
|
|
454
1090
|
]
|
|
455
1091
|
}
|
|
456
1092
|
);
|
|
457
1093
|
case "date":
|
|
458
|
-
return /* @__PURE__ */
|
|
1094
|
+
return /* @__PURE__ */ jsx8(
|
|
459
1095
|
"input",
|
|
460
1096
|
{
|
|
461
1097
|
id,
|
|
@@ -468,7 +1104,7 @@ function FieldInput({
|
|
|
468
1104
|
);
|
|
469
1105
|
case "text":
|
|
470
1106
|
case "json":
|
|
471
|
-
return /* @__PURE__ */
|
|
1107
|
+
return /* @__PURE__ */ jsx8(
|
|
472
1108
|
"textarea",
|
|
473
1109
|
{
|
|
474
1110
|
id,
|
|
@@ -491,7 +1127,7 @@ function FieldInput({
|
|
|
491
1127
|
}
|
|
492
1128
|
);
|
|
493
1129
|
default:
|
|
494
|
-
return /* @__PURE__ */
|
|
1130
|
+
return /* @__PURE__ */ jsx8(
|
|
495
1131
|
"input",
|
|
496
1132
|
{
|
|
497
1133
|
id,
|
|
@@ -507,23 +1143,37 @@ function FieldInput({
|
|
|
507
1143
|
}
|
|
508
1144
|
|
|
509
1145
|
// src/components/EntityForm.tsx
|
|
510
|
-
import { jsx as
|
|
1146
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
511
1147
|
function EntityForm({
|
|
512
1148
|
entity,
|
|
513
1149
|
mode,
|
|
514
1150
|
id,
|
|
1151
|
+
cloneFromId,
|
|
515
1152
|
onCancel,
|
|
516
1153
|
onSaved
|
|
517
1154
|
}) {
|
|
518
1155
|
const client = useClient();
|
|
1156
|
+
const t = useT();
|
|
519
1157
|
const fields = formFields(entity, mode);
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
const [
|
|
523
|
-
const [
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1158
|
+
const sourceId = mode === "edit" ? id : cloneFromId;
|
|
1159
|
+
const existing = useEntityRecord(entity.id, sourceId);
|
|
1160
|
+
const [values, setValues] = useState7({});
|
|
1161
|
+
const [saving, setSaving] = useState7(false);
|
|
1162
|
+
const [error, setError] = useState7(void 0);
|
|
1163
|
+
useEffect5(() => {
|
|
1164
|
+
if (!existing.data || !sourceId) return;
|
|
1165
|
+
if (mode === "edit") {
|
|
1166
|
+
setValues(existing.data);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const seed = {};
|
|
1170
|
+
for (const f of entity.fields) {
|
|
1171
|
+
if (f.name === entity.primaryKey) continue;
|
|
1172
|
+
if (!f.form.cloneable || f.readonly) continue;
|
|
1173
|
+
if (existing.data[f.name] !== void 0) seed[f.name] = existing.data[f.name];
|
|
1174
|
+
}
|
|
1175
|
+
setValues(seed);
|
|
1176
|
+
}, [mode, sourceId, existing.data, entity]);
|
|
527
1177
|
const submit = async (e) => {
|
|
528
1178
|
e.preventDefault();
|
|
529
1179
|
setSaving(true);
|
|
@@ -541,25 +1191,26 @@ function EntityForm({
|
|
|
541
1191
|
setSaving(false);
|
|
542
1192
|
}
|
|
543
1193
|
};
|
|
544
|
-
if (
|
|
545
|
-
return /* @__PURE__ */
|
|
1194
|
+
if (sourceId && existing.loading && !existing.data) {
|
|
1195
|
+
return /* @__PURE__ */ jsx9(Spinner, {});
|
|
546
1196
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1197
|
+
const title = mode === "edit" ? t.editTitle(entity.label.singular) : cloneFromId ? t.cloneTitle(entity.label.singular) : t.newTitle(entity.label.singular);
|
|
1198
|
+
return /* @__PURE__ */ jsxs8("section", { className: "max-w-2xl space-y-4", children: [
|
|
1199
|
+
/* @__PURE__ */ jsxs8("header", { children: [
|
|
1200
|
+
/* @__PURE__ */ jsxs8("button", { onClick: onCancel, className: "text-sm text-indigo-600 hover:underline", children: [
|
|
550
1201
|
"\u2190 ",
|
|
551
1202
|
entity.label.plural
|
|
552
1203
|
] }),
|
|
553
|
-
/* @__PURE__ */
|
|
1204
|
+
/* @__PURE__ */ jsx9("h1", { className: "text-lg font-semibold text-slate-900", children: title })
|
|
554
1205
|
] }),
|
|
555
|
-
error && /* @__PURE__ */
|
|
556
|
-
/* @__PURE__ */
|
|
557
|
-
fields.map((f) => /* @__PURE__ */
|
|
558
|
-
/* @__PURE__ */
|
|
1206
|
+
error && /* @__PURE__ */ jsx9(ErrorBanner, { error }),
|
|
1207
|
+
/* @__PURE__ */ jsxs8("form", { onSubmit: submit, className: "space-y-4 rounded-lg border border-slate-200 bg-white p-5", children: [
|
|
1208
|
+
fields.map((f) => /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [
|
|
1209
|
+
/* @__PURE__ */ jsxs8("label", { htmlFor: `field-${f.name}`, className: "block text-sm font-medium text-slate-700", children: [
|
|
559
1210
|
f.label,
|
|
560
|
-
f.required && /* @__PURE__ */
|
|
1211
|
+
f.required && /* @__PURE__ */ jsx9("span", { className: "ml-0.5 text-red-500", children: "*" })
|
|
561
1212
|
] }),
|
|
562
|
-
/* @__PURE__ */
|
|
1213
|
+
/* @__PURE__ */ jsx9(
|
|
563
1214
|
FieldInput,
|
|
564
1215
|
{
|
|
565
1216
|
field: f,
|
|
@@ -568,77 +1219,154 @@ function EntityForm({
|
|
|
568
1219
|
onChange: (next) => setValues((v) => ({ ...v, [f.name]: next }))
|
|
569
1220
|
}
|
|
570
1221
|
),
|
|
571
|
-
f.form.helpText && /* @__PURE__ */
|
|
1222
|
+
f.form.helpText && /* @__PURE__ */ jsx9("p", { className: "text-xs text-slate-500", children: f.form.helpText })
|
|
572
1223
|
] }, f.name)),
|
|
573
|
-
/* @__PURE__ */
|
|
574
|
-
/* @__PURE__ */
|
|
575
|
-
/* @__PURE__ */
|
|
1224
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex justify-end gap-2 border-t border-slate-100 pt-4", children: [
|
|
1225
|
+
/* @__PURE__ */ jsx9(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: saving, children: t.cancel }),
|
|
1226
|
+
/* @__PURE__ */ jsx9(Button, { type: "submit", variant: "primary", disabled: saving, children: saving ? t.saving : mode === "create" ? t.create : t.save })
|
|
576
1227
|
] })
|
|
577
1228
|
] })
|
|
578
1229
|
] });
|
|
579
1230
|
}
|
|
580
1231
|
|
|
1232
|
+
// src/labels.ts
|
|
1233
|
+
function relabelField(field, entityOverride, globalFields) {
|
|
1234
|
+
const label = entityOverride?.fields?.[field.name] ?? globalFields[field.name];
|
|
1235
|
+
return label ? { ...field, label } : field;
|
|
1236
|
+
}
|
|
1237
|
+
function relabelEntity(entity, labels) {
|
|
1238
|
+
const override = labels.entities?.[entity.id] ?? labels.entities?.[entity.table];
|
|
1239
|
+
const globalFields = labels.fields ?? {};
|
|
1240
|
+
return {
|
|
1241
|
+
...entity,
|
|
1242
|
+
label: {
|
|
1243
|
+
singular: override?.singular ?? entity.label.singular,
|
|
1244
|
+
plural: override?.plural ?? override?.singular ?? entity.label.plural
|
|
1245
|
+
},
|
|
1246
|
+
fields: entity.fields.map((f) => relabelField(f, override, globalFields))
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
function applyLabels(metadata, labels) {
|
|
1250
|
+
if (!labels || !labels.entities && !labels.fields) return metadata;
|
|
1251
|
+
return { ...metadata, entities: metadata.entities.map((e) => relabelEntity(e, labels)) };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/recents.ts
|
|
1255
|
+
import { useCallback as useCallback2, useState as useState8 } from "react";
|
|
1256
|
+
var STORAGE_KEY = "maestro-admin:recents";
|
|
1257
|
+
var MAX_RECENTS = 8;
|
|
1258
|
+
function read(key) {
|
|
1259
|
+
try {
|
|
1260
|
+
const raw = globalThis.localStorage?.getItem(key);
|
|
1261
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
1262
|
+
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
|
|
1263
|
+
} catch {
|
|
1264
|
+
return [];
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
function write(key, ids) {
|
|
1268
|
+
try {
|
|
1269
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(ids));
|
|
1270
|
+
} catch {
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function useRecents(scope) {
|
|
1274
|
+
const key = scope ? `${STORAGE_KEY}:${scope}` : STORAGE_KEY;
|
|
1275
|
+
const [recents, setRecents] = useState8(() => read(key));
|
|
1276
|
+
const push = useCallback2(
|
|
1277
|
+
(entityId) => {
|
|
1278
|
+
setRecents((prev) => {
|
|
1279
|
+
const next = [entityId, ...prev.filter((id) => id !== entityId)].slice(0, MAX_RECENTS);
|
|
1280
|
+
write(key, next);
|
|
1281
|
+
return next;
|
|
1282
|
+
});
|
|
1283
|
+
},
|
|
1284
|
+
[key]
|
|
1285
|
+
);
|
|
1286
|
+
return { recents, push };
|
|
1287
|
+
}
|
|
1288
|
+
|
|
581
1289
|
// src/components/MaestroAdmin.tsx
|
|
582
|
-
import { jsx as
|
|
583
|
-
function AdminShell({ title }) {
|
|
1290
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1291
|
+
function AdminShell({ title, labels }) {
|
|
584
1292
|
const { data, error, loading } = useMetadata();
|
|
585
|
-
const
|
|
586
|
-
const
|
|
1293
|
+
const t = useT();
|
|
1294
|
+
const [view, setView] = useState9(void 0);
|
|
1295
|
+
const { recents, push: pushRecent } = useRecents();
|
|
1296
|
+
const metadata = useMemo3(() => data ? applyLabels(data, labels) : void 0, [data, labels]);
|
|
1297
|
+
const entities = metadata?.entities ?? [];
|
|
587
1298
|
const activeId = view?.entityId ?? entities[0]?.id;
|
|
588
1299
|
const activeEntity = entities.find((e) => e.id === activeId);
|
|
589
1300
|
const effectiveView = view ?? (activeId ? { kind: "list", entityId: activeId } : void 0);
|
|
1301
|
+
const openEntity = (entityId) => {
|
|
1302
|
+
pushRecent(entityId);
|
|
1303
|
+
setView({ kind: "list", entityId });
|
|
1304
|
+
};
|
|
1305
|
+
const navigateToRecord = (entityId, id) => {
|
|
1306
|
+
pushRecent(entityId);
|
|
1307
|
+
setView({ kind: "detail", entityId, id });
|
|
1308
|
+
};
|
|
590
1309
|
if (loading && !data) {
|
|
591
|
-
return /* @__PURE__ */
|
|
1310
|
+
return /* @__PURE__ */ jsx10("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx10(Spinner, { label: t.loadingMetadata }) });
|
|
592
1311
|
}
|
|
593
|
-
return /* @__PURE__ */
|
|
594
|
-
/* @__PURE__ */
|
|
1312
|
+
return /* @__PURE__ */ jsx10(AdminEntitiesProvider, { value: entities, children: /* @__PURE__ */ jsxs9("div", { className: "flex h-screen overflow-hidden bg-white text-slate-800", children: [
|
|
1313
|
+
/* @__PURE__ */ jsx10(
|
|
595
1314
|
Sidebar,
|
|
596
1315
|
{
|
|
597
1316
|
entities,
|
|
598
1317
|
activeId,
|
|
599
1318
|
title,
|
|
600
|
-
|
|
1319
|
+
labels,
|
|
1320
|
+
recents,
|
|
1321
|
+
onSelect: openEntity
|
|
601
1322
|
}
|
|
602
1323
|
),
|
|
603
|
-
/* @__PURE__ */
|
|
604
|
-
error && /* @__PURE__ */
|
|
605
|
-
activeEntity && effectiveView && effectiveView.kind === "list" && /* @__PURE__ */
|
|
1324
|
+
/* @__PURE__ */ jsxs9("main", { className: "flex-1 overflow-y-auto p-6", children: [
|
|
1325
|
+
error && /* @__PURE__ */ jsx10(ErrorBanner, { error }),
|
|
1326
|
+
activeEntity && effectiveView && effectiveView.kind === "list" && /* @__PURE__ */ jsx10(
|
|
606
1327
|
EntityList,
|
|
607
1328
|
{
|
|
608
1329
|
entity: activeEntity,
|
|
609
1330
|
onOpen: (id) => setView({ kind: "detail", entityId: activeEntity.id, id }),
|
|
610
1331
|
onCreate: () => setView({ kind: "create", entityId: activeEntity.id }),
|
|
611
|
-
onEdit: (id) => setView({ kind: "edit", entityId: activeEntity.id, id })
|
|
1332
|
+
onEdit: (id) => setView({ kind: "edit", entityId: activeEntity.id, id }),
|
|
1333
|
+
onClone: (id) => setView({ kind: "create", entityId: activeEntity.id, cloneFromId: id }),
|
|
1334
|
+
onNavigate: navigateToRecord
|
|
612
1335
|
}
|
|
613
1336
|
),
|
|
614
|
-
activeEntity && effectiveView?.kind === "detail" && /* @__PURE__ */
|
|
1337
|
+
activeEntity && effectiveView?.kind === "detail" && /* @__PURE__ */ jsx10(
|
|
615
1338
|
EntityDetail,
|
|
616
1339
|
{
|
|
617
1340
|
entity: activeEntity,
|
|
618
1341
|
id: effectiveView.id,
|
|
619
1342
|
onBack: () => setView({ kind: "list", entityId: activeEntity.id }),
|
|
620
|
-
onEdit: () => setView({ kind: "edit", entityId: activeEntity.id, id: effectiveView.id })
|
|
1343
|
+
onEdit: () => setView({ kind: "edit", entityId: activeEntity.id, id: effectiveView.id }),
|
|
1344
|
+
onClone: () => setView({ kind: "create", entityId: activeEntity.id, cloneFromId: effectiveView.id }),
|
|
1345
|
+
onDeleted: () => setView({ kind: "list", entityId: activeEntity.id }),
|
|
1346
|
+
onNavigate: navigateToRecord
|
|
621
1347
|
}
|
|
622
1348
|
),
|
|
623
|
-
activeEntity && (effectiveView?.kind === "create" || effectiveView?.kind === "edit") && /* @__PURE__ */
|
|
1349
|
+
activeEntity && (effectiveView?.kind === "create" || effectiveView?.kind === "edit") && /* @__PURE__ */ jsx10(
|
|
624
1350
|
EntityForm,
|
|
625
1351
|
{
|
|
626
1352
|
entity: activeEntity,
|
|
627
1353
|
mode: effectiveView.kind,
|
|
628
1354
|
id: effectiveView.kind === "edit" ? effectiveView.id : void 0,
|
|
1355
|
+
cloneFromId: effectiveView.kind === "create" ? effectiveView.cloneFromId : void 0,
|
|
629
1356
|
onCancel: () => setView({ kind: "list", entityId: activeEntity.id }),
|
|
630
1357
|
onSaved: () => setView({ kind: "list", entityId: activeEntity.id })
|
|
631
1358
|
}
|
|
632
1359
|
)
|
|
633
1360
|
] })
|
|
634
|
-
] });
|
|
1361
|
+
] }) });
|
|
635
1362
|
}
|
|
636
1363
|
function MaestroAdmin(props) {
|
|
637
|
-
const client =
|
|
1364
|
+
const client = useMemo3(
|
|
638
1365
|
() => props.client ?? new MaestroClient({ apiUrl: props.apiUrl ?? "", headers: props.headers }),
|
|
639
1366
|
[props.client, props.apiUrl, props.headers]
|
|
640
1367
|
);
|
|
641
|
-
|
|
1368
|
+
const strings = useMemo3(() => stringsFor(props.locale ?? "en"), [props.locale]);
|
|
1369
|
+
return /* @__PURE__ */ jsx10(AdminClientProvider, { value: client, children: /* @__PURE__ */ jsx10(I18nProvider, { value: strings, children: /* @__PURE__ */ jsx10(AdminShell, { title: props.title ?? "Maestro Admin", labels: props.labels }) }) });
|
|
642
1370
|
}
|
|
643
1371
|
export {
|
|
644
1372
|
AdminClientProvider,
|
|
@@ -647,17 +1375,24 @@ export {
|
|
|
647
1375
|
EntityList,
|
|
648
1376
|
FieldInput,
|
|
649
1377
|
FieldValue,
|
|
1378
|
+
I18nProvider,
|
|
650
1379
|
MaestroAdmin,
|
|
651
1380
|
MaestroApiError,
|
|
652
1381
|
MaestroClient,
|
|
653
1382
|
Sidebar,
|
|
1383
|
+
applyLabels,
|
|
1384
|
+
buildMenu,
|
|
654
1385
|
detailFields,
|
|
655
1386
|
formFields,
|
|
656
1387
|
listFields,
|
|
1388
|
+
primaryListFields,
|
|
1389
|
+
stringsFor,
|
|
657
1390
|
useAsync,
|
|
658
1391
|
useClient,
|
|
659
1392
|
useEntityList,
|
|
660
1393
|
useEntityRecord,
|
|
661
|
-
useMetadata
|
|
1394
|
+
useMetadata,
|
|
1395
|
+
useRecents,
|
|
1396
|
+
useT
|
|
662
1397
|
};
|
|
663
1398
|
//# sourceMappingURL=index.js.map
|