@maykonpaulo/maestro-admin 0.2.0 → 0.3.0-next.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 +28 -1
- package/dist/index.js +437 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/MaestroAdmin.tsx
|
|
2
|
-
import { useMemo as useMemo3, useState as
|
|
2
|
+
import { useMemo as useMemo3, useState as useState10 } from "react";
|
|
3
3
|
|
|
4
4
|
// src/client/MaestroClient.ts
|
|
5
5
|
var MaestroApiError = class extends Error {
|
|
@@ -294,6 +294,12 @@ var EN = {
|
|
|
294
294
|
entitiesFooter: (count) => `${count} entities \xB7 Maestro`,
|
|
295
295
|
yes: "true",
|
|
296
296
|
no: "false",
|
|
297
|
+
jsonFormMode: "Fields",
|
|
298
|
+
jsonRawMode: "JSON",
|
|
299
|
+
jsonInvalid: "Invalid JSON \u2014 fix it to apply this change.",
|
|
300
|
+
emptyObject: "(empty)",
|
|
301
|
+
addItem: "Add item",
|
|
302
|
+
removeItem: "Remove item",
|
|
297
303
|
searchMenu: "Search menu\u2026",
|
|
298
304
|
recents: "Recents",
|
|
299
305
|
menus: "Menus",
|
|
@@ -334,6 +340,12 @@ var PT_BR = {
|
|
|
334
340
|
entitiesFooter: (count) => `${count} entidades \xB7 Maestro`,
|
|
335
341
|
yes: "sim",
|
|
336
342
|
no: "n\xE3o",
|
|
343
|
+
jsonFormMode: "Campos",
|
|
344
|
+
jsonRawMode: "JSON",
|
|
345
|
+
jsonInvalid: "JSON inv\xE1lido \u2014 corrija para aplicar esta altera\xE7\xE3o.",
|
|
346
|
+
emptyObject: "(vazio)",
|
|
347
|
+
addItem: "Adicionar item",
|
|
348
|
+
removeItem: "Remover item",
|
|
337
349
|
searchMenu: "Buscar no menu\u2026",
|
|
338
350
|
recents: "Recentes",
|
|
339
351
|
menus: "Menus",
|
|
@@ -595,8 +607,58 @@ function RelationValue({ field, value }) {
|
|
|
595
607
|
);
|
|
596
608
|
}
|
|
597
609
|
|
|
610
|
+
// src/components/ObjectValue.tsx
|
|
611
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
612
|
+
var ISO_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
613
|
+
var MAX_DEPTH = 4;
|
|
614
|
+
function isPlainObject(value) {
|
|
615
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
616
|
+
}
|
|
617
|
+
function Primitive({ value }) {
|
|
618
|
+
if (value === null || value === void 0) {
|
|
619
|
+
return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "\u2014" });
|
|
620
|
+
}
|
|
621
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
622
|
+
return /* @__PURE__ */ jsx4("code", { className: "text-xs font-medium text-indigo-700", children: String(value) });
|
|
623
|
+
}
|
|
624
|
+
const text = String(value);
|
|
625
|
+
if (ISO_DATETIME.test(text)) {
|
|
626
|
+
const d = new Date(text);
|
|
627
|
+
if (!Number.isNaN(d.getTime())) {
|
|
628
|
+
return /* @__PURE__ */ jsx4("span", { className: "whitespace-nowrap text-slate-700", children: d.toLocaleString() });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return /* @__PURE__ */ jsx4("span", { className: "break-all text-slate-700", children: text });
|
|
632
|
+
}
|
|
633
|
+
function ObjectValue({ value, depth = 0 }) {
|
|
634
|
+
const t = useT();
|
|
635
|
+
if (!isPlainObject(value) && !Array.isArray(value)) {
|
|
636
|
+
return /* @__PURE__ */ jsx4(Primitive, { value });
|
|
637
|
+
}
|
|
638
|
+
if (depth >= MAX_DEPTH) {
|
|
639
|
+
return /* @__PURE__ */ jsx4("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) });
|
|
640
|
+
}
|
|
641
|
+
const entries = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : Object.entries(value);
|
|
642
|
+
if (entries.length === 0) {
|
|
643
|
+
return /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: t.emptyObject });
|
|
644
|
+
}
|
|
645
|
+
return /* @__PURE__ */ jsx4(
|
|
646
|
+
"dl",
|
|
647
|
+
{
|
|
648
|
+
className: depth === 0 ? "w-full divide-y divide-slate-100 rounded-md border border-slate-200 bg-slate-50/50" : "w-full divide-y divide-slate-100 border-l-2 border-slate-200 pl-2",
|
|
649
|
+
children: entries.map(([key, entry]) => (
|
|
650
|
+
// flex-wrap + basis: deeply nested values drop to their own full-width line instead of squeezing.
|
|
651
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-start gap-x-3 gap-y-1 px-3 py-1.5", children: [
|
|
652
|
+
/* @__PURE__ */ jsx4("dt", { className: "w-36 shrink-0 truncate pt-px text-xs font-medium text-slate-500", title: key, children: key }),
|
|
653
|
+
/* @__PURE__ */ jsx4("dd", { className: "min-w-0 flex-1 basis-56 text-sm", children: isPlainObject(entry) || Array.isArray(entry) ? /* @__PURE__ */ jsx4(ObjectValue, { value: entry, depth: depth + 1 }) : /* @__PURE__ */ jsx4(Primitive, { value: entry }) })
|
|
654
|
+
] }, key)
|
|
655
|
+
))
|
|
656
|
+
}
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
598
660
|
// src/components/FieldValue.tsx
|
|
599
|
-
import { jsx as
|
|
661
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
600
662
|
function enumLabel(field, value) {
|
|
601
663
|
const opt = field.enumOptions?.find((o) => o.value === value);
|
|
602
664
|
return opt ? opt.label : String(value);
|
|
@@ -604,34 +666,40 @@ function enumLabel(field, value) {
|
|
|
604
666
|
function FieldValue({ field, value, compact = false }) {
|
|
605
667
|
const t = useT();
|
|
606
668
|
if (value === null || value === void 0 || value === "") {
|
|
607
|
-
return /* @__PURE__ */
|
|
669
|
+
return /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "\u2014" });
|
|
608
670
|
}
|
|
609
671
|
if (field.sensitive) {
|
|
610
|
-
return /* @__PURE__ */
|
|
672
|
+
return /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "\u2022\u2022\u2022\u2022\u2022\u2022" });
|
|
611
673
|
}
|
|
612
674
|
switch (field.type) {
|
|
613
675
|
case "boolean":
|
|
614
|
-
return value ? /* @__PURE__ */
|
|
676
|
+
return value ? /* @__PURE__ */ jsx5(Badge, { tone: "green", children: t.yes }) : /* @__PURE__ */ jsx5(Badge, { tone: "slate", children: t.no });
|
|
615
677
|
case "enum":
|
|
616
|
-
return /* @__PURE__ */
|
|
678
|
+
return /* @__PURE__ */ jsx5(Badge, { tone: "indigo", children: enumLabel(field, value) });
|
|
617
679
|
case "relation":
|
|
618
|
-
return field.relationEntity ? /* @__PURE__ */
|
|
680
|
+
return field.relationEntity ? /* @__PURE__ */ jsx5(RelationValue, { field, value }) : /* @__PURE__ */ jsx5("span", { className: "text-slate-700", children: String(value) });
|
|
619
681
|
case "date":
|
|
620
682
|
case "datetime": {
|
|
621
683
|
const d = new Date(String(value));
|
|
622
|
-
return /* @__PURE__ */
|
|
684
|
+
return /* @__PURE__ */ jsx5("span", { className: "whitespace-nowrap", children: Number.isNaN(d.getTime()) ? String(value) : d.toLocaleString() });
|
|
623
685
|
}
|
|
624
686
|
case "json":
|
|
625
|
-
|
|
687
|
+
if (compact) {
|
|
688
|
+
return /* @__PURE__ */ jsx5("code", { className: "block max-w-xs truncate overflow-x-auto rounded bg-slate-50 px-1.5 py-0.5 text-xs text-slate-700", children: JSON.stringify(value) });
|
|
689
|
+
}
|
|
690
|
+
if (typeof value === "object") {
|
|
691
|
+
return /* @__PURE__ */ jsx5(ObjectValue, { value });
|
|
692
|
+
}
|
|
693
|
+
return /* @__PURE__ */ jsx5("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) });
|
|
626
694
|
default:
|
|
627
|
-
return /* @__PURE__ */
|
|
695
|
+
return /* @__PURE__ */ jsx5("span", { className: `text-slate-700 ${compact ? "block max-w-64 truncate" : ""}`, children: String(value) });
|
|
628
696
|
}
|
|
629
697
|
}
|
|
630
698
|
|
|
631
699
|
// src/components/menu.tsx
|
|
632
700
|
import { useEffect as useEffect4, useLayoutEffect, useRef as useRef2, useState as useState4 } from "react";
|
|
633
701
|
import { createPortal } from "react-dom";
|
|
634
|
-
import { Fragment as Fragment2, jsx as
|
|
702
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
635
703
|
function PopMenu({ anchor, items, onClose }) {
|
|
636
704
|
const ref = useRef2(null);
|
|
637
705
|
const [pos, setPos] = useState4(anchor);
|
|
@@ -663,14 +731,14 @@ function PopMenu({ anchor, items, onClose }) {
|
|
|
663
731
|
};
|
|
664
732
|
}, [onClose]);
|
|
665
733
|
return createPortal(
|
|
666
|
-
/* @__PURE__ */
|
|
734
|
+
/* @__PURE__ */ jsx6(
|
|
667
735
|
"div",
|
|
668
736
|
{
|
|
669
737
|
ref,
|
|
670
738
|
role: "menu",
|
|
671
739
|
style: { position: "fixed", left: pos.x, top: pos.y, zIndex: 50 },
|
|
672
740
|
className: "min-w-44 rounded-md border border-slate-200 bg-white py-1 shadow-lg",
|
|
673
|
-
children: items.map((item) => /* @__PURE__ */
|
|
741
|
+
children: items.map((item) => /* @__PURE__ */ jsx6(
|
|
674
742
|
"button",
|
|
675
743
|
{
|
|
676
744
|
role: "menuitem",
|
|
@@ -690,8 +758,8 @@ function PopMenu({ anchor, items, onClose }) {
|
|
|
690
758
|
}
|
|
691
759
|
function MenuButton({ items, label }) {
|
|
692
760
|
const [anchor, setAnchor] = useState4(void 0);
|
|
693
|
-
return /* @__PURE__ */
|
|
694
|
-
/* @__PURE__ */
|
|
761
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
762
|
+
/* @__PURE__ */ jsx6(
|
|
695
763
|
"button",
|
|
696
764
|
{
|
|
697
765
|
type: "button",
|
|
@@ -705,7 +773,7 @@ function MenuButton({ items, label }) {
|
|
|
705
773
|
children: "\u22EE"
|
|
706
774
|
}
|
|
707
775
|
),
|
|
708
|
-
anchor && /* @__PURE__ */
|
|
776
|
+
anchor && /* @__PURE__ */ jsx6(PopMenu, { anchor, items, onClose: () => setAnchor(void 0) })
|
|
709
777
|
] });
|
|
710
778
|
}
|
|
711
779
|
|
|
@@ -729,7 +797,7 @@ async function copyText(text) {
|
|
|
729
797
|
}
|
|
730
798
|
|
|
731
799
|
// src/components/EntityList.tsx
|
|
732
|
-
import { jsx as
|
|
800
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
733
801
|
var PAGE_SIZE = 20;
|
|
734
802
|
function filterFor(field, value) {
|
|
735
803
|
if (value === null || value === void 0) return { field: field.name, operator: "isNull" };
|
|
@@ -843,14 +911,14 @@ function EntityList({
|
|
|
843
911
|
return items;
|
|
844
912
|
};
|
|
845
913
|
const totalPages = data?.totalPages ?? (data ? Math.max(1, Math.ceil(data.total / PAGE_SIZE)) : 1);
|
|
846
|
-
return /* @__PURE__ */
|
|
847
|
-
/* @__PURE__ */
|
|
848
|
-
/* @__PURE__ */
|
|
849
|
-
/* @__PURE__ */
|
|
850
|
-
data && /* @__PURE__ */
|
|
914
|
+
return /* @__PURE__ */ jsxs6("section", { className: "space-y-4", children: [
|
|
915
|
+
/* @__PURE__ */ jsxs6("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
916
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
917
|
+
/* @__PURE__ */ jsx7("h1", { className: "text-lg font-semibold text-slate-900", children: entity.label.plural }),
|
|
918
|
+
data && /* @__PURE__ */ jsx7("p", { className: "text-sm text-slate-500", children: t.records(data.total) })
|
|
851
919
|
] }),
|
|
852
|
-
/* @__PURE__ */
|
|
853
|
-
searchFields.length > 0 && /* @__PURE__ */
|
|
920
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
|
|
921
|
+
searchFields.length > 0 && /* @__PURE__ */ jsx7(
|
|
854
922
|
"input",
|
|
855
923
|
{
|
|
856
924
|
type: "search",
|
|
@@ -863,15 +931,15 @@ function EntityList({
|
|
|
863
931
|
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"
|
|
864
932
|
}
|
|
865
933
|
),
|
|
866
|
-
entity.capabilities.create && /* @__PURE__ */
|
|
934
|
+
entity.capabilities.create && /* @__PURE__ */ jsx7(Button, { variant: "primary", onClick: onCreate, children: t.newRecord })
|
|
867
935
|
] })
|
|
868
936
|
] }),
|
|
869
|
-
filters.length > 0 && /* @__PURE__ */
|
|
870
|
-
/* @__PURE__ */
|
|
937
|
+
filters.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
|
|
938
|
+
/* @__PURE__ */ jsxs6("span", { className: "text-slate-500", children: [
|
|
871
939
|
t.filters,
|
|
872
940
|
":"
|
|
873
941
|
] }),
|
|
874
|
-
filters.map((f) => /* @__PURE__ */
|
|
942
|
+
filters.map((f) => /* @__PURE__ */ jsxs6(
|
|
875
943
|
"button",
|
|
876
944
|
{
|
|
877
945
|
onClick: () => {
|
|
@@ -882,12 +950,12 @@ function EntityList({
|
|
|
882
950
|
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
951
|
children: [
|
|
884
952
|
filterChipText(entity, f),
|
|
885
|
-
/* @__PURE__ */
|
|
953
|
+
/* @__PURE__ */ jsx7("span", { "aria-hidden": true, children: "\xD7" })
|
|
886
954
|
]
|
|
887
955
|
},
|
|
888
956
|
f.field
|
|
889
957
|
)),
|
|
890
|
-
/* @__PURE__ */
|
|
958
|
+
/* @__PURE__ */ jsx7(
|
|
891
959
|
"button",
|
|
892
960
|
{
|
|
893
961
|
onClick: () => {
|
|
@@ -899,10 +967,10 @@ function EntityList({
|
|
|
899
967
|
}
|
|
900
968
|
)
|
|
901
969
|
] }),
|
|
902
|
-
error && /* @__PURE__ */
|
|
903
|
-
loading && !data ? /* @__PURE__ */
|
|
904
|
-
/* @__PURE__ */
|
|
905
|
-
columns.map((f) => /* @__PURE__ */
|
|
970
|
+
error && /* @__PURE__ */ jsx7(ErrorBanner, { error }),
|
|
971
|
+
loading && !data ? /* @__PURE__ */ jsx7(Spinner, { label: t.loading }) : data && data.records.length === 0 ? /* @__PURE__ */ jsx7(EmptyState, { title: t.noRecords, hint: search || filters.length ? t.tryDifferentSearch : void 0 }) : /* @__PURE__ */ jsx7("div", { className: "overflow-x-auto rounded-lg border border-slate-200", children: /* @__PURE__ */ jsxs6("table", { className: "min-w-full divide-y divide-slate-200 text-sm", children: [
|
|
972
|
+
/* @__PURE__ */ jsx7("thead", { className: "bg-slate-50", children: /* @__PURE__ */ jsxs6("tr", { children: [
|
|
973
|
+
columns.map((f) => /* @__PURE__ */ jsxs6(
|
|
906
974
|
"th",
|
|
907
975
|
{
|
|
908
976
|
onClick: () => f.sortable && toggleSort(f.name),
|
|
@@ -914,38 +982,38 @@ function EntityList({
|
|
|
914
982
|
},
|
|
915
983
|
f.name
|
|
916
984
|
)),
|
|
917
|
-
/* @__PURE__ */
|
|
985
|
+
/* @__PURE__ */ jsx7("th", { className: "px-3 py-2 text-right font-medium text-slate-600", children: t.actions })
|
|
918
986
|
] }) }),
|
|
919
|
-
/* @__PURE__ */
|
|
987
|
+
/* @__PURE__ */ jsx7("tbody", { className: "divide-y divide-slate-100 bg-white", children: data?.records.map((record, i) => {
|
|
920
988
|
const id = String(record[entity.primaryKey]);
|
|
921
|
-
return /* @__PURE__ */
|
|
922
|
-
columns.map((f) => /* @__PURE__ */
|
|
989
|
+
return /* @__PURE__ */ jsxs6("tr", { className: "hover:bg-slate-50", children: [
|
|
990
|
+
columns.map((f) => /* @__PURE__ */ jsx7(
|
|
923
991
|
"td",
|
|
924
992
|
{
|
|
925
993
|
className: "cursor-pointer px-3 py-2 align-top",
|
|
926
994
|
onClick: (e) => setCellMenu({ anchor: { x: e.clientX, y: e.clientY }, field: f, record }),
|
|
927
995
|
onDoubleClick: () => onOpen(id),
|
|
928
|
-
children: /* @__PURE__ */
|
|
996
|
+
children: /* @__PURE__ */ jsx7(FieldValue, { field: f, value: record[f.name], compact: true })
|
|
929
997
|
},
|
|
930
998
|
f.name
|
|
931
999
|
)),
|
|
932
|
-
/* @__PURE__ */
|
|
1000
|
+
/* @__PURE__ */ jsx7("td", { className: "whitespace-nowrap px-3 py-2 text-right", children: /* @__PURE__ */ jsx7(MenuButton, { label: t.actions, items: rowMenuItems(record) }) })
|
|
933
1001
|
] }, id || i);
|
|
934
1002
|
}) })
|
|
935
1003
|
] }) }),
|
|
936
|
-
cellMenu && /* @__PURE__ */
|
|
937
|
-
copied && /* @__PURE__ */
|
|
938
|
-
data && totalPages > 1 && /* @__PURE__ */
|
|
939
|
-
/* @__PURE__ */
|
|
940
|
-
/* @__PURE__ */
|
|
941
|
-
/* @__PURE__ */
|
|
1004
|
+
cellMenu && /* @__PURE__ */ jsx7(PopMenu, { anchor: cellMenu.anchor, items: cellMenuItems(cellMenu), onClose: () => setCellMenu(void 0) }),
|
|
1005
|
+
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 }),
|
|
1006
|
+
data && totalPages > 1 && /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-end gap-2 text-sm", children: [
|
|
1007
|
+
/* @__PURE__ */ jsx7(Button, { variant: "secondary", disabled: page <= 1, onClick: () => setPage((p) => p - 1), children: t.previous }),
|
|
1008
|
+
/* @__PURE__ */ jsx7("span", { className: "text-slate-500", children: t.page(page, totalPages) }),
|
|
1009
|
+
/* @__PURE__ */ jsx7(Button, { variant: "secondary", disabled: page >= totalPages, onClick: () => setPage((p) => p + 1), children: t.next })
|
|
942
1010
|
] })
|
|
943
1011
|
] });
|
|
944
1012
|
}
|
|
945
1013
|
|
|
946
1014
|
// src/components/EntityDetail.tsx
|
|
947
1015
|
import { useState as useState6 } from "react";
|
|
948
|
-
import { jsx as
|
|
1016
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
949
1017
|
function EntityDetail({
|
|
950
1018
|
entity,
|
|
951
1019
|
id,
|
|
@@ -986,34 +1054,34 @@ function EntityDetail({
|
|
|
986
1054
|
if (entity.capabilities.delete) {
|
|
987
1055
|
menuItems.push({ id: "remove", label: `\u{1F5D1} ${t.remove}`, danger: true, onSelect: () => void onDelete() });
|
|
988
1056
|
}
|
|
989
|
-
return /* @__PURE__ */
|
|
990
|
-
/* @__PURE__ */
|
|
991
|
-
/* @__PURE__ */
|
|
992
|
-
/* @__PURE__ */
|
|
1057
|
+
return /* @__PURE__ */ jsxs7("section", { className: "space-y-4", children: [
|
|
1058
|
+
/* @__PURE__ */ jsxs7("header", { className: "flex items-center justify-between gap-3", children: [
|
|
1059
|
+
/* @__PURE__ */ jsxs7("div", { children: [
|
|
1060
|
+
/* @__PURE__ */ jsxs7("button", { onClick: onBack, className: "text-sm text-indigo-600 hover:underline", children: [
|
|
993
1061
|
"\u2190 ",
|
|
994
1062
|
entity.label.plural
|
|
995
1063
|
] }),
|
|
996
|
-
/* @__PURE__ */
|
|
1064
|
+
/* @__PURE__ */ jsxs7("h1", { className: "text-lg font-semibold text-slate-900", children: [
|
|
997
1065
|
entity.label.singular,
|
|
998
1066
|
" ",
|
|
999
1067
|
data ? String(data[entity.displayField] ?? id) : ""
|
|
1000
1068
|
] })
|
|
1001
1069
|
] }),
|
|
1002
|
-
/* @__PURE__ */
|
|
1003
|
-
entity.capabilities.update && /* @__PURE__ */
|
|
1004
|
-
/* @__PURE__ */
|
|
1070
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
|
|
1071
|
+
entity.capabilities.update && /* @__PURE__ */ jsx8(Button, { variant: "primary", onClick: onEdit, children: t.edit }),
|
|
1072
|
+
/* @__PURE__ */ jsx8(MenuButton, { label: t.actions, items: menuItems })
|
|
1005
1073
|
] })
|
|
1006
1074
|
] }),
|
|
1007
|
-
error && /* @__PURE__ */
|
|
1008
|
-
loading && !data ? /* @__PURE__ */
|
|
1075
|
+
error && /* @__PURE__ */ jsx8(ErrorBanner, { error }),
|
|
1076
|
+
loading && !data ? /* @__PURE__ */ jsx8(Spinner, {}) : data && /* @__PURE__ */ jsx8("dl", { className: "divide-y divide-slate-100 rounded-lg border border-slate-200 bg-white", children: fields.map((f) => {
|
|
1009
1077
|
const value = data[f.name];
|
|
1010
1078
|
const isRelation = f.type === "relation" && f.relationEntity && value !== null && value !== void 0;
|
|
1011
1079
|
const target = isRelation ? entities.find((e) => e.id === f.relationEntity) : void 0;
|
|
1012
|
-
return /* @__PURE__ */
|
|
1013
|
-
/* @__PURE__ */
|
|
1014
|
-
/* @__PURE__ */
|
|
1015
|
-
/* @__PURE__ */
|
|
1016
|
-
isRelation && /* @__PURE__ */
|
|
1080
|
+
return /* @__PURE__ */ jsxs7("div", { className: "grid grid-cols-3 gap-4 px-4 py-3", children: [
|
|
1081
|
+
/* @__PURE__ */ jsx8("dt", { className: "text-sm font-medium text-slate-500", children: f.label }),
|
|
1082
|
+
/* @__PURE__ */ jsxs7("dd", { className: "col-span-2 flex items-center gap-2 text-sm", children: [
|
|
1083
|
+
/* @__PURE__ */ jsx8(FieldValue, { field: f, value }),
|
|
1084
|
+
isRelation && /* @__PURE__ */ jsxs7(
|
|
1017
1085
|
"button",
|
|
1018
1086
|
{
|
|
1019
1087
|
onClick: () => onNavigate(f.relationEntity, String(value)),
|
|
@@ -1027,15 +1095,270 @@ function EntityDetail({
|
|
|
1027
1095
|
] })
|
|
1028
1096
|
] }, f.name);
|
|
1029
1097
|
}) }),
|
|
1030
|
-
copied && /* @__PURE__ */
|
|
1098
|
+
copied && /* @__PURE__ */ jsx8("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 })
|
|
1031
1099
|
] });
|
|
1032
1100
|
}
|
|
1033
1101
|
|
|
1034
1102
|
// src/components/EntityForm.tsx
|
|
1035
|
-
import { useEffect as
|
|
1103
|
+
import { useEffect as useEffect6, useState as useState8 } from "react";
|
|
1104
|
+
|
|
1105
|
+
// src/components/JsonInput.tsx
|
|
1106
|
+
import { useEffect as useEffect5, useRef as useRef3, useState as useState7 } from "react";
|
|
1107
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1108
|
+
var leafInputClass = "w-full rounded-md border border-slate-300 px-2.5 py-1 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";
|
|
1109
|
+
var MAX_DEPTH2 = 3;
|
|
1110
|
+
function RawJsonArea({
|
|
1111
|
+
id,
|
|
1112
|
+
value,
|
|
1113
|
+
onChange,
|
|
1114
|
+
disabled,
|
|
1115
|
+
rows = 8,
|
|
1116
|
+
placeholder
|
|
1117
|
+
}) {
|
|
1118
|
+
const t = useT();
|
|
1119
|
+
const [text, setText] = useState7(() => value === null || value === void 0 ? "" : JSON.stringify(value, null, 2));
|
|
1120
|
+
const [invalid, setInvalid] = useState7(false);
|
|
1121
|
+
const lastEmitted = useRef3(value);
|
|
1122
|
+
useEffect5(() => {
|
|
1123
|
+
if (value !== lastEmitted.current) {
|
|
1124
|
+
lastEmitted.current = value;
|
|
1125
|
+
setText(value === null || value === void 0 ? "" : JSON.stringify(value, null, 2));
|
|
1126
|
+
setInvalid(false);
|
|
1127
|
+
}
|
|
1128
|
+
}, [value]);
|
|
1129
|
+
const onText = (raw) => {
|
|
1130
|
+
setText(raw);
|
|
1131
|
+
if (raw.trim() === "") {
|
|
1132
|
+
lastEmitted.current = null;
|
|
1133
|
+
setInvalid(false);
|
|
1134
|
+
onChange(null);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
try {
|
|
1138
|
+
const parsed = JSON.parse(raw);
|
|
1139
|
+
lastEmitted.current = parsed;
|
|
1140
|
+
setInvalid(false);
|
|
1141
|
+
onChange(parsed);
|
|
1142
|
+
} catch {
|
|
1143
|
+
setInvalid(true);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
return /* @__PURE__ */ jsxs8("div", { className: "space-y-1", children: [
|
|
1147
|
+
/* @__PURE__ */ jsx9(
|
|
1148
|
+
"textarea",
|
|
1149
|
+
{
|
|
1150
|
+
id,
|
|
1151
|
+
rows,
|
|
1152
|
+
value: text,
|
|
1153
|
+
placeholder,
|
|
1154
|
+
disabled,
|
|
1155
|
+
onChange: (e) => onText(e.target.value),
|
|
1156
|
+
"aria-invalid": invalid || void 0,
|
|
1157
|
+
className: `${leafInputClass} font-mono ${invalid ? "border-amber-400 focus:border-amber-500 focus:ring-amber-500" : ""}`
|
|
1158
|
+
}
|
|
1159
|
+
),
|
|
1160
|
+
invalid && /* @__PURE__ */ jsx9("p", { role: "alert", className: "text-xs text-amber-600", children: t.jsonInvalid })
|
|
1161
|
+
] });
|
|
1162
|
+
}
|
|
1163
|
+
function LeafEditor({
|
|
1164
|
+
id,
|
|
1165
|
+
value,
|
|
1166
|
+
onChange,
|
|
1167
|
+
disabled,
|
|
1168
|
+
depth
|
|
1169
|
+
}) {
|
|
1170
|
+
if (typeof value === "boolean") {
|
|
1171
|
+
return /* @__PURE__ */ jsx9(
|
|
1172
|
+
"input",
|
|
1173
|
+
{
|
|
1174
|
+
id,
|
|
1175
|
+
type: "checkbox",
|
|
1176
|
+
checked: value,
|
|
1177
|
+
disabled,
|
|
1178
|
+
onChange: (e) => onChange(e.target.checked),
|
|
1179
|
+
className: "mt-1 h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500"
|
|
1180
|
+
}
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
if (typeof value === "number") {
|
|
1184
|
+
return /* @__PURE__ */ jsx9(
|
|
1185
|
+
"input",
|
|
1186
|
+
{
|
|
1187
|
+
id,
|
|
1188
|
+
type: "number",
|
|
1189
|
+
value: String(value),
|
|
1190
|
+
disabled,
|
|
1191
|
+
step: "any",
|
|
1192
|
+
onChange: (e) => onChange(e.target.value === "" ? null : Number(e.target.value)),
|
|
1193
|
+
className: leafInputClass
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
if (typeof value === "string") {
|
|
1198
|
+
return /* @__PURE__ */ jsx9(
|
|
1199
|
+
"input",
|
|
1200
|
+
{
|
|
1201
|
+
id,
|
|
1202
|
+
type: "text",
|
|
1203
|
+
value,
|
|
1204
|
+
disabled,
|
|
1205
|
+
onChange: (e) => onChange(e.target.value),
|
|
1206
|
+
className: leafInputClass
|
|
1207
|
+
}
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
if (value === null || value === void 0) {
|
|
1211
|
+
return /* @__PURE__ */ jsx9(
|
|
1212
|
+
"input",
|
|
1213
|
+
{
|
|
1214
|
+
id,
|
|
1215
|
+
type: "text",
|
|
1216
|
+
value: "",
|
|
1217
|
+
placeholder: "null",
|
|
1218
|
+
disabled,
|
|
1219
|
+
onChange: (e) => onChange(e.target.value === "" ? null : e.target.value),
|
|
1220
|
+
className: leafInputClass
|
|
1221
|
+
}
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
if (isPlainObject(value) && depth < MAX_DEPTH2) {
|
|
1225
|
+
return /* @__PURE__ */ jsx9("div", { className: "rounded-md border border-slate-200", children: /* @__PURE__ */ jsx9(ObjectEditor, { idPrefix: id, value, onChange, disabled, depth: depth + 1 }) });
|
|
1226
|
+
}
|
|
1227
|
+
if (Array.isArray(value) && depth < MAX_DEPTH2) {
|
|
1228
|
+
return /* @__PURE__ */ jsx9("div", { className: "rounded-md border border-slate-200", children: /* @__PURE__ */ jsx9(ArrayEditor, { idPrefix: id, value, onChange, disabled, depth: depth + 1 }) });
|
|
1229
|
+
}
|
|
1230
|
+
return /* @__PURE__ */ jsx9(RawJsonArea, { id, value, onChange, disabled, rows: 3 });
|
|
1231
|
+
}
|
|
1232
|
+
function ArrayEditor({
|
|
1233
|
+
idPrefix,
|
|
1234
|
+
value,
|
|
1235
|
+
onChange,
|
|
1236
|
+
disabled,
|
|
1237
|
+
depth = 0
|
|
1238
|
+
}) {
|
|
1239
|
+
const t = useT();
|
|
1240
|
+
const setAt = (i, next) => onChange(value.map((cur, idx) => idx === i ? next : cur));
|
|
1241
|
+
const removeAt = (i) => onChange(value.filter((_, idx) => idx !== i));
|
|
1242
|
+
const append = () => {
|
|
1243
|
+
const last = value[value.length - 1];
|
|
1244
|
+
const template = last === void 0 || last === null ? null : JSON.parse(JSON.stringify(last));
|
|
1245
|
+
onChange([...value, template]);
|
|
1246
|
+
};
|
|
1247
|
+
return /* @__PURE__ */ jsxs8("div", { className: "divide-y divide-slate-100", children: [
|
|
1248
|
+
value.map((item, i) => {
|
|
1249
|
+
const id = `${idPrefix}-${i}`;
|
|
1250
|
+
return /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-start gap-x-3 gap-y-1.5 px-3 py-2", children: [
|
|
1251
|
+
/* @__PURE__ */ jsx9("label", { htmlFor: id, className: "w-8 shrink-0 pt-1.5 text-right text-xs font-medium text-slate-400", children: i }),
|
|
1252
|
+
!disabled && /* @__PURE__ */ jsx9(
|
|
1253
|
+
"button",
|
|
1254
|
+
{
|
|
1255
|
+
type: "button",
|
|
1256
|
+
title: t.removeItem,
|
|
1257
|
+
"aria-label": `${t.removeItem} ${i}`,
|
|
1258
|
+
onClick: () => removeAt(i),
|
|
1259
|
+
className: "shrink-0 pt-1.5 text-xs text-slate-400 transition-colors hover:text-red-600",
|
|
1260
|
+
children: "\u2715"
|
|
1261
|
+
}
|
|
1262
|
+
),
|
|
1263
|
+
/* @__PURE__ */ jsx9("div", { className: "min-w-0 flex-1 basis-64", children: /* @__PURE__ */ jsx9(LeafEditor, { id, value: item, onChange: (next) => setAt(i, next), disabled, depth }) })
|
|
1264
|
+
] }, i);
|
|
1265
|
+
}),
|
|
1266
|
+
value.length === 0 && /* @__PURE__ */ jsx9("p", { className: "px-3 py-2 text-xs text-slate-400", children: t.emptyObject }),
|
|
1267
|
+
!disabled && /* @__PURE__ */ jsx9("div", { className: "px-3 py-1.5", children: /* @__PURE__ */ jsxs8("button", { type: "button", onClick: append, className: "text-xs font-medium text-indigo-600 hover:underline", children: [
|
|
1268
|
+
"+ ",
|
|
1269
|
+
t.addItem
|
|
1270
|
+
] }) })
|
|
1271
|
+
] });
|
|
1272
|
+
}
|
|
1273
|
+
function ObjectEditor({
|
|
1274
|
+
idPrefix,
|
|
1275
|
+
value,
|
|
1276
|
+
onChange,
|
|
1277
|
+
disabled,
|
|
1278
|
+
depth = 0
|
|
1279
|
+
}) {
|
|
1280
|
+
const t = useT();
|
|
1281
|
+
const entries = Object.entries(value);
|
|
1282
|
+
if (entries.length === 0) {
|
|
1283
|
+
return /* @__PURE__ */ jsx9("p", { className: "px-3 py-2 text-xs text-slate-400", children: t.emptyObject });
|
|
1284
|
+
}
|
|
1285
|
+
return /* @__PURE__ */ jsx9("div", { className: "divide-y divide-slate-100", children: entries.map(([key, entry]) => {
|
|
1286
|
+
const id = `${idPrefix}-${key}`;
|
|
1287
|
+
return (
|
|
1288
|
+
// flex-wrap + basis: when the label column leaves less than ~16rem for the control (deeply
|
|
1289
|
+
// nested groups), the control wraps to its own full-width line instead of squeezing.
|
|
1290
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap items-start gap-x-3 gap-y-1.5 px-3 py-2", children: [
|
|
1291
|
+
/* @__PURE__ */ jsx9("label", { htmlFor: id, className: "w-36 shrink-0 truncate pt-1.5 text-xs font-medium text-slate-500", title: key, children: key }),
|
|
1292
|
+
/* @__PURE__ */ jsx9("div", { className: "min-w-0 flex-1 basis-64", children: /* @__PURE__ */ jsx9(
|
|
1293
|
+
LeafEditor,
|
|
1294
|
+
{
|
|
1295
|
+
id,
|
|
1296
|
+
value: entry,
|
|
1297
|
+
onChange: (next) => onChange({ ...value, [key]: next }),
|
|
1298
|
+
disabled,
|
|
1299
|
+
depth
|
|
1300
|
+
}
|
|
1301
|
+
) })
|
|
1302
|
+
] }, key)
|
|
1303
|
+
);
|
|
1304
|
+
}) });
|
|
1305
|
+
}
|
|
1306
|
+
function JsonInput({
|
|
1307
|
+
field,
|
|
1308
|
+
value,
|
|
1309
|
+
onChange,
|
|
1310
|
+
disabled
|
|
1311
|
+
}) {
|
|
1312
|
+
const t = useT();
|
|
1313
|
+
const [mode, setMode] = useState7(() => isPlainObject(value) ? "form" : "raw");
|
|
1314
|
+
const [modeTouched, setModeTouched] = useState7(false);
|
|
1315
|
+
const structured = isPlainObject(value) || Array.isArray(value);
|
|
1316
|
+
useEffect5(() => {
|
|
1317
|
+
if (!modeTouched && structured) setMode("form");
|
|
1318
|
+
}, [structured, modeTouched]);
|
|
1319
|
+
const pick = (next) => {
|
|
1320
|
+
setModeTouched(true);
|
|
1321
|
+
setMode(next);
|
|
1322
|
+
};
|
|
1323
|
+
const showForm = mode === "form" && structured;
|
|
1324
|
+
const toggleClass = (active) => `rounded px-2 py-0.5 text-xs font-medium transition-colors ${active ? "bg-white text-slate-800 shadow-sm ring-1 ring-slate-300" : "text-slate-500 hover:text-slate-700"}`;
|
|
1325
|
+
return /* @__PURE__ */ jsxs8("div", { className: "rounded-md border border-slate-300 shadow-sm", children: [
|
|
1326
|
+
/* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-end gap-1 rounded-t-md border-b border-slate-200 bg-slate-50 px-2 py-1", children: [
|
|
1327
|
+
/* @__PURE__ */ jsx9(
|
|
1328
|
+
"button",
|
|
1329
|
+
{
|
|
1330
|
+
type: "button",
|
|
1331
|
+
className: toggleClass(showForm),
|
|
1332
|
+
disabled: !structured,
|
|
1333
|
+
onClick: () => pick("form"),
|
|
1334
|
+
children: t.jsonFormMode
|
|
1335
|
+
}
|
|
1336
|
+
),
|
|
1337
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: toggleClass(!showForm), onClick: () => pick("raw"), children: t.jsonRawMode })
|
|
1338
|
+
] }),
|
|
1339
|
+
/* @__PURE__ */ jsx9("div", { className: "p-1", children: showForm ? Array.isArray(value) ? /* @__PURE__ */ jsx9(ArrayEditor, { idPrefix: `field-${field.name}`, value, onChange, disabled }) : /* @__PURE__ */ jsx9(
|
|
1340
|
+
ObjectEditor,
|
|
1341
|
+
{
|
|
1342
|
+
idPrefix: `field-${field.name}`,
|
|
1343
|
+
value,
|
|
1344
|
+
onChange,
|
|
1345
|
+
disabled
|
|
1346
|
+
}
|
|
1347
|
+
) : /* @__PURE__ */ jsx9(
|
|
1348
|
+
RawJsonArea,
|
|
1349
|
+
{
|
|
1350
|
+
id: `field-${field.name}`,
|
|
1351
|
+
value,
|
|
1352
|
+
onChange,
|
|
1353
|
+
disabled,
|
|
1354
|
+
placeholder: field.form.placeholder
|
|
1355
|
+
}
|
|
1356
|
+
) })
|
|
1357
|
+
] });
|
|
1358
|
+
}
|
|
1036
1359
|
|
|
1037
1360
|
// src/components/FieldInput.tsx
|
|
1038
|
-
import { jsx as
|
|
1361
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1039
1362
|
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";
|
|
1040
1363
|
function FieldInput({
|
|
1041
1364
|
field,
|
|
@@ -1046,7 +1369,7 @@ function FieldInput({
|
|
|
1046
1369
|
const id = `field-${field.name}`;
|
|
1047
1370
|
switch (field.type) {
|
|
1048
1371
|
case "boolean":
|
|
1049
|
-
return /* @__PURE__ */
|
|
1372
|
+
return /* @__PURE__ */ jsx10(
|
|
1050
1373
|
"input",
|
|
1051
1374
|
{
|
|
1052
1375
|
id,
|
|
@@ -1062,7 +1385,7 @@ function FieldInput({
|
|
|
1062
1385
|
case "float":
|
|
1063
1386
|
case "decimal":
|
|
1064
1387
|
case "currency":
|
|
1065
|
-
return /* @__PURE__ */
|
|
1388
|
+
return /* @__PURE__ */ jsx10(
|
|
1066
1389
|
"input",
|
|
1067
1390
|
{
|
|
1068
1391
|
id,
|
|
@@ -1076,7 +1399,7 @@ function FieldInput({
|
|
|
1076
1399
|
}
|
|
1077
1400
|
);
|
|
1078
1401
|
case "enum":
|
|
1079
|
-
return /* @__PURE__ */
|
|
1402
|
+
return /* @__PURE__ */ jsxs9(
|
|
1080
1403
|
"select",
|
|
1081
1404
|
{
|
|
1082
1405
|
id,
|
|
@@ -1085,13 +1408,13 @@ function FieldInput({
|
|
|
1085
1408
|
onChange: (e) => onChange(e.target.value),
|
|
1086
1409
|
className: inputClass,
|
|
1087
1410
|
children: [
|
|
1088
|
-
/* @__PURE__ */
|
|
1089
|
-
field.enumOptions?.map((o) => /* @__PURE__ */
|
|
1411
|
+
/* @__PURE__ */ jsx10("option", { value: "", children: "\u2014" }),
|
|
1412
|
+
field.enumOptions?.map((o) => /* @__PURE__ */ jsx10("option", { value: String(o.value), children: o.label }, String(o.value)))
|
|
1090
1413
|
]
|
|
1091
1414
|
}
|
|
1092
1415
|
);
|
|
1093
1416
|
case "date":
|
|
1094
|
-
return /* @__PURE__ */
|
|
1417
|
+
return /* @__PURE__ */ jsx10(
|
|
1095
1418
|
"input",
|
|
1096
1419
|
{
|
|
1097
1420
|
id,
|
|
@@ -1102,32 +1425,23 @@ function FieldInput({
|
|
|
1102
1425
|
className: inputClass
|
|
1103
1426
|
}
|
|
1104
1427
|
);
|
|
1105
|
-
case "text":
|
|
1106
1428
|
case "json":
|
|
1107
|
-
return /* @__PURE__ */
|
|
1429
|
+
return /* @__PURE__ */ jsx10(JsonInput, { field, value, onChange, disabled });
|
|
1430
|
+
case "text":
|
|
1431
|
+
return /* @__PURE__ */ jsx10(
|
|
1108
1432
|
"textarea",
|
|
1109
1433
|
{
|
|
1110
1434
|
id,
|
|
1111
|
-
rows:
|
|
1112
|
-
value:
|
|
1435
|
+
rows: 3,
|
|
1436
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
1113
1437
|
placeholder: field.form.placeholder,
|
|
1114
1438
|
disabled,
|
|
1115
|
-
onChange: (e) =>
|
|
1116
|
-
|
|
1117
|
-
try {
|
|
1118
|
-
onChange(e.target.value ? JSON.parse(e.target.value) : null);
|
|
1119
|
-
} catch {
|
|
1120
|
-
onChange(e.target.value);
|
|
1121
|
-
}
|
|
1122
|
-
} else {
|
|
1123
|
-
onChange(e.target.value);
|
|
1124
|
-
}
|
|
1125
|
-
},
|
|
1126
|
-
className: `${inputClass} font-mono`
|
|
1439
|
+
onChange: (e) => onChange(e.target.value),
|
|
1440
|
+
className: inputClass
|
|
1127
1441
|
}
|
|
1128
1442
|
);
|
|
1129
1443
|
default:
|
|
1130
|
-
return /* @__PURE__ */
|
|
1444
|
+
return /* @__PURE__ */ jsx10(
|
|
1131
1445
|
"input",
|
|
1132
1446
|
{
|
|
1133
1447
|
id,
|
|
@@ -1143,7 +1457,7 @@ function FieldInput({
|
|
|
1143
1457
|
}
|
|
1144
1458
|
|
|
1145
1459
|
// src/components/EntityForm.tsx
|
|
1146
|
-
import { jsx as
|
|
1460
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1147
1461
|
function EntityForm({
|
|
1148
1462
|
entity,
|
|
1149
1463
|
mode,
|
|
@@ -1157,10 +1471,10 @@ function EntityForm({
|
|
|
1157
1471
|
const fields = formFields(entity, mode);
|
|
1158
1472
|
const sourceId = mode === "edit" ? id : cloneFromId;
|
|
1159
1473
|
const existing = useEntityRecord(entity.id, sourceId);
|
|
1160
|
-
const [values, setValues] =
|
|
1161
|
-
const [saving, setSaving] =
|
|
1162
|
-
const [error, setError] =
|
|
1163
|
-
|
|
1474
|
+
const [values, setValues] = useState8({});
|
|
1475
|
+
const [saving, setSaving] = useState8(false);
|
|
1476
|
+
const [error, setError] = useState8(void 0);
|
|
1477
|
+
useEffect6(() => {
|
|
1164
1478
|
if (!existing.data || !sourceId) return;
|
|
1165
1479
|
if (mode === "edit") {
|
|
1166
1480
|
setValues(existing.data);
|
|
@@ -1192,25 +1506,25 @@ function EntityForm({
|
|
|
1192
1506
|
}
|
|
1193
1507
|
};
|
|
1194
1508
|
if (sourceId && existing.loading && !existing.data) {
|
|
1195
|
-
return /* @__PURE__ */
|
|
1509
|
+
return /* @__PURE__ */ jsx11(Spinner, {});
|
|
1196
1510
|
}
|
|
1197
1511
|
const title = mode === "edit" ? t.editTitle(entity.label.singular) : cloneFromId ? t.cloneTitle(entity.label.singular) : t.newTitle(entity.label.singular);
|
|
1198
|
-
return /* @__PURE__ */
|
|
1199
|
-
/* @__PURE__ */
|
|
1200
|
-
/* @__PURE__ */
|
|
1512
|
+
return /* @__PURE__ */ jsxs10("section", { className: "space-y-4", children: [
|
|
1513
|
+
/* @__PURE__ */ jsxs10("header", { children: [
|
|
1514
|
+
/* @__PURE__ */ jsxs10("button", { onClick: onCancel, className: "text-sm text-indigo-600 hover:underline", children: [
|
|
1201
1515
|
"\u2190 ",
|
|
1202
1516
|
entity.label.plural
|
|
1203
1517
|
] }),
|
|
1204
|
-
/* @__PURE__ */
|
|
1518
|
+
/* @__PURE__ */ jsx11("h1", { className: "text-lg font-semibold text-slate-900", children: title })
|
|
1205
1519
|
] }),
|
|
1206
|
-
error && /* @__PURE__ */
|
|
1207
|
-
/* @__PURE__ */
|
|
1208
|
-
fields.map((f) => /* @__PURE__ */
|
|
1209
|
-
/* @__PURE__ */
|
|
1520
|
+
error && /* @__PURE__ */ jsx11(ErrorBanner, { error }),
|
|
1521
|
+
/* @__PURE__ */ jsxs10("form", { onSubmit: submit, className: "space-y-4 rounded-lg border border-slate-200 bg-white p-5", children: [
|
|
1522
|
+
fields.map((f) => /* @__PURE__ */ jsxs10("div", { className: "space-y-1", children: [
|
|
1523
|
+
/* @__PURE__ */ jsxs10("label", { htmlFor: `field-${f.name}`, className: "block text-sm font-medium text-slate-700", children: [
|
|
1210
1524
|
f.label,
|
|
1211
|
-
f.required && /* @__PURE__ */
|
|
1525
|
+
f.required && /* @__PURE__ */ jsx11("span", { className: "ml-0.5 text-red-500", children: "*" })
|
|
1212
1526
|
] }),
|
|
1213
|
-
/* @__PURE__ */
|
|
1527
|
+
/* @__PURE__ */ jsx11(
|
|
1214
1528
|
FieldInput,
|
|
1215
1529
|
{
|
|
1216
1530
|
field: f,
|
|
@@ -1219,11 +1533,11 @@ function EntityForm({
|
|
|
1219
1533
|
onChange: (next) => setValues((v) => ({ ...v, [f.name]: next }))
|
|
1220
1534
|
}
|
|
1221
1535
|
),
|
|
1222
|
-
f.form.helpText && /* @__PURE__ */
|
|
1536
|
+
f.form.helpText && /* @__PURE__ */ jsx11("p", { className: "text-xs text-slate-500", children: f.form.helpText })
|
|
1223
1537
|
] }, f.name)),
|
|
1224
|
-
/* @__PURE__ */
|
|
1225
|
-
/* @__PURE__ */
|
|
1226
|
-
/* @__PURE__ */
|
|
1538
|
+
/* @__PURE__ */ jsxs10("div", { className: "flex justify-end gap-2 border-t border-slate-100 pt-4", children: [
|
|
1539
|
+
/* @__PURE__ */ jsx11(Button, { type: "button", variant: "secondary", onClick: onCancel, disabled: saving, children: t.cancel }),
|
|
1540
|
+
/* @__PURE__ */ jsx11(Button, { type: "submit", variant: "primary", disabled: saving, children: saving ? t.saving : mode === "create" ? t.create : t.save })
|
|
1227
1541
|
] })
|
|
1228
1542
|
] })
|
|
1229
1543
|
] });
|
|
@@ -1252,7 +1566,7 @@ function applyLabels(metadata, labels) {
|
|
|
1252
1566
|
}
|
|
1253
1567
|
|
|
1254
1568
|
// src/recents.ts
|
|
1255
|
-
import { useCallback as useCallback2, useState as
|
|
1569
|
+
import { useCallback as useCallback2, useState as useState9 } from "react";
|
|
1256
1570
|
var STORAGE_KEY = "maestro-admin:recents";
|
|
1257
1571
|
var MAX_RECENTS = 8;
|
|
1258
1572
|
function read(key) {
|
|
@@ -1272,7 +1586,7 @@ function write(key, ids) {
|
|
|
1272
1586
|
}
|
|
1273
1587
|
function useRecents(scope) {
|
|
1274
1588
|
const key = scope ? `${STORAGE_KEY}:${scope}` : STORAGE_KEY;
|
|
1275
|
-
const [recents, setRecents] =
|
|
1589
|
+
const [recents, setRecents] = useState9(() => read(key));
|
|
1276
1590
|
const push = useCallback2(
|
|
1277
1591
|
(entityId) => {
|
|
1278
1592
|
setRecents((prev) => {
|
|
@@ -1287,11 +1601,11 @@ function useRecents(scope) {
|
|
|
1287
1601
|
}
|
|
1288
1602
|
|
|
1289
1603
|
// src/components/MaestroAdmin.tsx
|
|
1290
|
-
import { jsx as
|
|
1604
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1291
1605
|
function AdminShell({ title, labels }) {
|
|
1292
1606
|
const { data, error, loading } = useMetadata();
|
|
1293
1607
|
const t = useT();
|
|
1294
|
-
const [view, setView] =
|
|
1608
|
+
const [view, setView] = useState10(void 0);
|
|
1295
1609
|
const { recents, push: pushRecent } = useRecents();
|
|
1296
1610
|
const metadata = useMemo3(() => data ? applyLabels(data, labels) : void 0, [data, labels]);
|
|
1297
1611
|
const entities = metadata?.entities ?? [];
|
|
@@ -1307,10 +1621,10 @@ function AdminShell({ title, labels }) {
|
|
|
1307
1621
|
setView({ kind: "detail", entityId, id });
|
|
1308
1622
|
};
|
|
1309
1623
|
if (loading && !data) {
|
|
1310
|
-
return /* @__PURE__ */
|
|
1624
|
+
return /* @__PURE__ */ jsx12("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx12(Spinner, { label: t.loadingMetadata }) });
|
|
1311
1625
|
}
|
|
1312
|
-
return /* @__PURE__ */
|
|
1313
|
-
/* @__PURE__ */
|
|
1626
|
+
return /* @__PURE__ */ jsx12(AdminEntitiesProvider, { value: entities, children: /* @__PURE__ */ jsxs11("div", { className: "flex h-screen overflow-hidden bg-white text-slate-800", children: [
|
|
1627
|
+
/* @__PURE__ */ jsx12(
|
|
1314
1628
|
Sidebar,
|
|
1315
1629
|
{
|
|
1316
1630
|
entities,
|
|
@@ -1321,9 +1635,9 @@ function AdminShell({ title, labels }) {
|
|
|
1321
1635
|
onSelect: openEntity
|
|
1322
1636
|
}
|
|
1323
1637
|
),
|
|
1324
|
-
/* @__PURE__ */
|
|
1325
|
-
error && /* @__PURE__ */
|
|
1326
|
-
activeEntity && effectiveView && effectiveView.kind === "list" && /* @__PURE__ */
|
|
1638
|
+
/* @__PURE__ */ jsxs11("main", { className: "flex-1 overflow-y-auto p-6", children: [
|
|
1639
|
+
error && /* @__PURE__ */ jsx12(ErrorBanner, { error }),
|
|
1640
|
+
activeEntity && effectiveView && effectiveView.kind === "list" && /* @__PURE__ */ jsx12(
|
|
1327
1641
|
EntityList,
|
|
1328
1642
|
{
|
|
1329
1643
|
entity: activeEntity,
|
|
@@ -1334,7 +1648,7 @@ function AdminShell({ title, labels }) {
|
|
|
1334
1648
|
onNavigate: navigateToRecord
|
|
1335
1649
|
}
|
|
1336
1650
|
),
|
|
1337
|
-
activeEntity && effectiveView?.kind === "detail" && /* @__PURE__ */
|
|
1651
|
+
activeEntity && effectiveView?.kind === "detail" && /* @__PURE__ */ jsx12(
|
|
1338
1652
|
EntityDetail,
|
|
1339
1653
|
{
|
|
1340
1654
|
entity: activeEntity,
|
|
@@ -1346,7 +1660,7 @@ function AdminShell({ title, labels }) {
|
|
|
1346
1660
|
onNavigate: navigateToRecord
|
|
1347
1661
|
}
|
|
1348
1662
|
),
|
|
1349
|
-
activeEntity && (effectiveView?.kind === "create" || effectiveView?.kind === "edit") && /* @__PURE__ */
|
|
1663
|
+
activeEntity && (effectiveView?.kind === "create" || effectiveView?.kind === "edit") && /* @__PURE__ */ jsx12(
|
|
1350
1664
|
EntityForm,
|
|
1351
1665
|
{
|
|
1352
1666
|
entity: activeEntity,
|
|
@@ -1366,7 +1680,7 @@ function MaestroAdmin(props) {
|
|
|
1366
1680
|
[props.client, props.apiUrl, props.headers]
|
|
1367
1681
|
);
|
|
1368
1682
|
const strings = useMemo3(() => stringsFor(props.locale ?? "en"), [props.locale]);
|
|
1369
|
-
return /* @__PURE__ */
|
|
1683
|
+
return /* @__PURE__ */ jsx12(AdminClientProvider, { value: client, children: /* @__PURE__ */ jsx12(I18nProvider, { value: strings, children: /* @__PURE__ */ jsx12(AdminShell, { title: props.title ?? "Maestro Admin", labels: props.labels }) }) });
|
|
1370
1684
|
}
|
|
1371
1685
|
export {
|
|
1372
1686
|
AdminClientProvider,
|
|
@@ -1376,9 +1690,11 @@ export {
|
|
|
1376
1690
|
FieldInput,
|
|
1377
1691
|
FieldValue,
|
|
1378
1692
|
I18nProvider,
|
|
1693
|
+
JsonInput,
|
|
1379
1694
|
MaestroAdmin,
|
|
1380
1695
|
MaestroApiError,
|
|
1381
1696
|
MaestroClient,
|
|
1697
|
+
ObjectValue,
|
|
1382
1698
|
Sidebar,
|
|
1383
1699
|
applyLabels,
|
|
1384
1700
|
buildMenu,
|