@adminforge/core 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +56 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/bin/adminforge.js +317 -0
- package/dist/auth-client.cjs +45 -0
- package/dist/auth-client.cjs.map +1 -0
- package/dist/auth-client.d.cts +17 -0
- package/dist/auth-client.d.ts +17 -0
- package/dist/auth-client.js +20 -0
- package/dist/auth-client.js.map +1 -0
- package/dist/auth.cjs +65 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +21 -0
- package/dist/auth.d.ts +21 -0
- package/dist/auth.js +36 -0
- package/dist/auth.js.map +1 -0
- package/dist/client-D0cjJVsn.d.ts +20 -0
- package/dist/client-sRnmZ-Y9.d.cts +20 -0
- package/dist/index-CyzxaE7n.d.cts +124 -0
- package/dist/index-CyzxaE7n.d.ts +124 -0
- package/dist/index.cjs +453 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +410 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +839 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +84 -0
- package/dist/next.d.ts +84 -0
- package/dist/next.js +800 -0
- package/dist/next.js.map +1 -0
- package/dist/styles.css +763 -0
- package/dist/styles.css.map +1 -0
- package/dist/styles.d.cts +2 -0
- package/dist/styles.d.ts +2 -0
- package/dist/ui.cjs +2500 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.cts +119 -0
- package/dist/ui.d.ts +119 -0
- package/dist/ui.js +2448 -0
- package/dist/ui.js.map +1 -0
- package/eslint.config.js +35 -0
- package/package.json +99 -0
- package/src/api/controller.ts +234 -0
- package/src/api/index.ts +4 -0
- package/src/api/next.ts +281 -0
- package/src/api/security/agent-auth.ts +134 -0
- package/src/auth/config.ts +20 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/middleware.ts +15 -0
- package/src/auth/provider.tsx +28 -0
- package/src/core/fields/index.ts +119 -0
- package/src/core/hooks/index.ts +60 -0
- package/src/core/index.ts +43 -0
- package/src/core/registry/index.ts +22 -0
- package/src/core/schema/collection.ts +12 -0
- package/src/core/schema/config.ts +11 -0
- package/src/core/schema/normalize.ts +32 -0
- package/src/core/types/index.ts +114 -0
- package/src/db/client.ts +146 -0
- package/src/db/index.ts +3 -0
- package/src/db/schema-generator.ts +104 -0
- package/src/fields/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/next.ts +3 -0
- package/src/styles/adminforge.css +840 -0
- package/src/ui/AdminDashboard.tsx +176 -0
- package/src/ui/AdminForgeContext.tsx +64 -0
- package/src/ui/components/AdminLayout.tsx +107 -0
- package/src/ui/form-engine/FormEngine.tsx +250 -0
- package/src/ui/form-engine/ImageUpload.tsx +68 -0
- package/src/ui/form-engine/RelationInput.tsx +215 -0
- package/src/ui/form-engine/RichTextEditor.tsx +708 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/screens/AdminPage.tsx +162 -0
- package/src/ui/screens/AgentTokenPage.tsx +232 -0
- package/src/ui/screens/CollectionFormPage.tsx +135 -0
- package/src/ui/screens/CollectionListPage.tsx +170 -0
- package/src/ui/screens/CollectionSchemaPage.tsx +180 -0
- package/src/ui/screens/RoleDetailPage.tsx +147 -0
- package/src/ui/screens/RolesListPage.tsx +57 -0
- package/src/ui/table-engine/TableEngine.tsx +157 -0
- package/src/ui.ts +3 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +54 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { useEffect, useState, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
interface RelationInputProps {
|
|
4
|
+
name: string;
|
|
5
|
+
to: string;
|
|
6
|
+
relationType?: string;
|
|
7
|
+
value?: string | string[];
|
|
8
|
+
onChange?: (val: string | string[]) => void;
|
|
9
|
+
error?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function RelationInput({ name, to, relationType, value, onChange, error, disabled }: RelationInputProps) {
|
|
14
|
+
const [options, setOptions] = useState<any[]>([]);
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [search, setSearch] = useState("");
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
function handleClickOutside(event: MouseEvent) {
|
|
22
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
23
|
+
setOpen(false);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
27
|
+
return () => {
|
|
28
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
29
|
+
};
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const isMulti = relationType === "many-to-many" || relationType === "one-to-many";
|
|
33
|
+
|
|
34
|
+
// Normalize value to array of strings for easier handling
|
|
35
|
+
const rawValueArray = Array.isArray(value) ? value : value ? [value] : [];
|
|
36
|
+
const selectedIds = rawValueArray.map((v: any) => typeof v === "object" && v !== null ? v.id : v).filter(Boolean);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
let mounted = true;
|
|
40
|
+
async function fetchOptions() {
|
|
41
|
+
try {
|
|
42
|
+
setLoading(true);
|
|
43
|
+
const res = await fetch(`/api/${to}?pageSize=100`);
|
|
44
|
+
const json = await res.json();
|
|
45
|
+
if (mounted && json.data) {
|
|
46
|
+
const uniqueOptions = [];
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
const getLbl = (opt: any) => opt.name || opt.title || opt.label || opt.id;
|
|
49
|
+
for (const opt of json.data) {
|
|
50
|
+
const lbl = getLbl(opt);
|
|
51
|
+
if (!seen.has(lbl)) {
|
|
52
|
+
seen.add(lbl);
|
|
53
|
+
uniqueOptions.push(opt);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
setOptions(uniqueOptions);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error("Failed to fetch relations", err);
|
|
60
|
+
} finally {
|
|
61
|
+
if (mounted) setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
fetchOptions();
|
|
65
|
+
return () => {
|
|
66
|
+
mounted = false;
|
|
67
|
+
};
|
|
68
|
+
}, [to]);
|
|
69
|
+
|
|
70
|
+
const getLabel = (opt: any) => opt.name || opt.title || opt.label || opt.id;
|
|
71
|
+
|
|
72
|
+
const filteredOptions = options.filter((o) =>
|
|
73
|
+
getLabel(o).toLowerCase().includes(search.toLowerCase())
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const toggleOption = (id: string) => {
|
|
77
|
+
if (isMulti) {
|
|
78
|
+
const newIds = selectedIds.includes(id)
|
|
79
|
+
? selectedIds.filter(v => v !== id)
|
|
80
|
+
: [...selectedIds, id];
|
|
81
|
+
onChange?.(newIds);
|
|
82
|
+
} else {
|
|
83
|
+
onChange?.(id);
|
|
84
|
+
setOpen(false);
|
|
85
|
+
setSearch("");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const removeOption = (e: React.MouseEvent, id: string) => {
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
if (disabled) return;
|
|
92
|
+
onChange?.(selectedIds.filter(v => v !== id));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="adminforge-relation" ref={containerRef} style={{ position: "relative" }}>
|
|
97
|
+
{/* Hidden inputs for native form submission */}
|
|
98
|
+
{selectedIds.length === 0 && <input type="hidden" name={name} value="" />}
|
|
99
|
+
{selectedIds.map(id => (
|
|
100
|
+
<input key={id} type="hidden" name={name} value={id} />
|
|
101
|
+
))}
|
|
102
|
+
|
|
103
|
+
<div
|
|
104
|
+
className={`adminforge-input ${error ? "adminforge-input-error" : ""}`}
|
|
105
|
+
style={{
|
|
106
|
+
position: "relative",
|
|
107
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
108
|
+
minHeight: "38px",
|
|
109
|
+
display: "flex",
|
|
110
|
+
alignItems: "center",
|
|
111
|
+
flexWrap: "wrap",
|
|
112
|
+
gap: "4px",
|
|
113
|
+
padding: "4px 8px",
|
|
114
|
+
opacity: disabled ? 0.6 : 1,
|
|
115
|
+
}}
|
|
116
|
+
onClick={() => !disabled && setOpen(!open)}
|
|
117
|
+
>
|
|
118
|
+
{selectedIds.length > 0 ? (
|
|
119
|
+
selectedIds.map(id => {
|
|
120
|
+
const opt = options.find(o => o.id === id);
|
|
121
|
+
const label = opt ? getLabel(opt) : id;
|
|
122
|
+
if (isMulti) {
|
|
123
|
+
return (
|
|
124
|
+
<span key={id} style={{
|
|
125
|
+
background: "#e0e0e0",
|
|
126
|
+
padding: "2px 6px",
|
|
127
|
+
borderRadius: "4px",
|
|
128
|
+
fontSize: "14px",
|
|
129
|
+
display: "flex",
|
|
130
|
+
alignItems: "center",
|
|
131
|
+
gap: "4px"
|
|
132
|
+
}}>
|
|
133
|
+
{label}
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={(e) => removeOption(e, id)}
|
|
137
|
+
style={{ background: "none", border: "none", cursor: "pointer", padding: "0 2px", fontWeight: "bold" }}
|
|
138
|
+
>×</button>
|
|
139
|
+
</span>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return <span key={id}>{label}</span>;
|
|
143
|
+
})
|
|
144
|
+
) : (
|
|
145
|
+
<span style={{ color: "#888" }}>Select...</span>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{open && (
|
|
150
|
+
<div
|
|
151
|
+
style={{
|
|
152
|
+
position: "absolute",
|
|
153
|
+
zIndex: 10,
|
|
154
|
+
background: "white",
|
|
155
|
+
border: "1px solid #ccc",
|
|
156
|
+
borderRadius: "4px",
|
|
157
|
+
marginTop: "4px",
|
|
158
|
+
width: "100%",
|
|
159
|
+
maxHeight: "250px",
|
|
160
|
+
overflowY: "auto",
|
|
161
|
+
boxShadow: "0 4px 6px rgba(0,0,0,0.1)"
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<div style={{ padding: "8px", borderBottom: "1px solid #eee" }}>
|
|
165
|
+
<input
|
|
166
|
+
type="text"
|
|
167
|
+
placeholder="Search..."
|
|
168
|
+
value={search}
|
|
169
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
170
|
+
onClick={(e) => e.stopPropagation()}
|
|
171
|
+
style={{
|
|
172
|
+
width: "100%",
|
|
173
|
+
padding: "4px 8px",
|
|
174
|
+
border: "1px solid #ccc",
|
|
175
|
+
borderRadius: "4px"
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
{loading ? (
|
|
180
|
+
<div style={{ padding: "8px", color: "#888" }}>Loading...</div>
|
|
181
|
+
) : filteredOptions.length === 0 ? (
|
|
182
|
+
<div style={{ padding: "8px", color: "#888" }}>No options found</div>
|
|
183
|
+
) : (
|
|
184
|
+
<ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
|
185
|
+
{filteredOptions.map((opt) => {
|
|
186
|
+
const isSelected = selectedIds.includes(opt.id);
|
|
187
|
+
return (
|
|
188
|
+
<li
|
|
189
|
+
key={opt.id}
|
|
190
|
+
style={{
|
|
191
|
+
padding: "8px 12px",
|
|
192
|
+
cursor: "pointer",
|
|
193
|
+
background: isSelected ? "#f0f0f0" : "transparent",
|
|
194
|
+
display: "flex",
|
|
195
|
+
justifyContent: "space-between"
|
|
196
|
+
}}
|
|
197
|
+
onMouseEnter={(e) => (e.currentTarget.style.background = "#f9f9f9")}
|
|
198
|
+
onMouseLeave={(e) => (e.currentTarget.style.background = isSelected ? "#f0f0f0" : "transparent")}
|
|
199
|
+
onClick={(e) => {
|
|
200
|
+
e.stopPropagation();
|
|
201
|
+
toggleOption(opt.id);
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
{getLabel(opt)}
|
|
205
|
+
{isSelected && isMulti && <span style={{ color: "#417690" }}>✓</span>}
|
|
206
|
+
</li>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</ul>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|