@cfast/admin 0.0.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/index.d.ts +735 -0
- package/dist/index.js +1898 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1898 @@
|
|
|
1
|
+
// src/introspect.ts
|
|
2
|
+
import { getTableConfig } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
4
|
+
var AUTO_EXCLUDED_TABLES = /* @__PURE__ */ new Set([
|
|
5
|
+
"session",
|
|
6
|
+
"account",
|
|
7
|
+
"verification",
|
|
8
|
+
"passkey"
|
|
9
|
+
]);
|
|
10
|
+
function tableNameToLabel(name) {
|
|
11
|
+
const words = name.replace(/([a-z])([A-Z])/g, "$1_$2").split("_").filter(Boolean);
|
|
12
|
+
if (words.length === 0) return name;
|
|
13
|
+
const titled = words.map(
|
|
14
|
+
(w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
|
|
15
|
+
);
|
|
16
|
+
const lastIndex = titled.length - 1;
|
|
17
|
+
titled[lastIndex] = pluralize(titled[lastIndex]);
|
|
18
|
+
return titled.join(" ");
|
|
19
|
+
}
|
|
20
|
+
function pluralize(word) {
|
|
21
|
+
const lower = word.toLowerCase();
|
|
22
|
+
if (lower.endsWith("es") || lower.endsWith("ies") || lower.endsWith("s") && !lower.endsWith("ss") && !lower.endsWith("us")) {
|
|
23
|
+
return word;
|
|
24
|
+
}
|
|
25
|
+
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("z")) {
|
|
26
|
+
return word + "es";
|
|
27
|
+
}
|
|
28
|
+
if (word.endsWith("sh") || word.endsWith("ch")) {
|
|
29
|
+
return word + "es";
|
|
30
|
+
}
|
|
31
|
+
if (word.endsWith("y") && word.length > 1 && !isVowel(word.charAt(word.length - 2))) {
|
|
32
|
+
return word.slice(0, -1) + "ies";
|
|
33
|
+
}
|
|
34
|
+
return word + "s";
|
|
35
|
+
}
|
|
36
|
+
function isVowel(ch) {
|
|
37
|
+
return "aeiouAEIOU".includes(ch);
|
|
38
|
+
}
|
|
39
|
+
function columnNameToLabel(name) {
|
|
40
|
+
const words = name.replace(/([a-z])([A-Z])/g, "$1_$2").split("_").filter(Boolean);
|
|
41
|
+
return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(" ");
|
|
42
|
+
}
|
|
43
|
+
function introspectSchema(schema, tableOverrides) {
|
|
44
|
+
const result = [];
|
|
45
|
+
for (const [_key, table] of Object.entries(schema)) {
|
|
46
|
+
const tableName = getTableName(table);
|
|
47
|
+
const overrides = tableOverrides?.[tableName] ?? {};
|
|
48
|
+
if (overrides.exclude === true) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (AUTO_EXCLUDED_TABLES.has(tableName) && !tableOverrides?.[tableName]) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const columns = introspectColumns(table);
|
|
55
|
+
if (columns.length === 0) continue;
|
|
56
|
+
const primaryKey = columns.find((c) => c.isPrimaryKey)?.name ?? columns[0].name;
|
|
57
|
+
const searchableColumns = overrides.searchable ?? defaultSearchableColumns(columns);
|
|
58
|
+
const listColumns = overrides.listColumns ?? columns.filter((c) => !c.isPrimaryKey).map((c) => c.name);
|
|
59
|
+
const defaultSort = overrides.defaultSort ?? {
|
|
60
|
+
column: primaryKey,
|
|
61
|
+
direction: "desc"
|
|
62
|
+
};
|
|
63
|
+
const label = overrides.label ?? tableNameToLabel(tableName);
|
|
64
|
+
result.push({
|
|
65
|
+
name: tableName,
|
|
66
|
+
label,
|
|
67
|
+
drizzleTable: table,
|
|
68
|
+
columns,
|
|
69
|
+
primaryKey,
|
|
70
|
+
searchableColumns,
|
|
71
|
+
listColumns,
|
|
72
|
+
defaultSort,
|
|
73
|
+
overrides
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
result.sort((a, b) => a.name.localeCompare(b.name));
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function introspectColumns(table) {
|
|
80
|
+
const drizzleColumns = getTableColumns(table);
|
|
81
|
+
const tableConfig = getTableConfig(table);
|
|
82
|
+
const foreignKeys = tableConfig.foreignKeys;
|
|
83
|
+
const refMap = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const fk of foreignKeys) {
|
|
85
|
+
const ref = fk.reference();
|
|
86
|
+
const localColumns = ref.columns;
|
|
87
|
+
const foreignColumns = ref.foreignColumns;
|
|
88
|
+
const foreignTable = ref.foreignTable;
|
|
89
|
+
const foreignTableName = getTableName(foreignTable);
|
|
90
|
+
for (let i = 0; i < localColumns.length; i++) {
|
|
91
|
+
const localCol = localColumns[i];
|
|
92
|
+
const foreignCol = foreignColumns[i];
|
|
93
|
+
if (localCol && foreignCol) {
|
|
94
|
+
refMap.set(localCol.name, {
|
|
95
|
+
table: foreignTableName,
|
|
96
|
+
column: foreignCol.name
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const result = [];
|
|
102
|
+
for (const [_fieldName, col] of Object.entries(drizzleColumns)) {
|
|
103
|
+
const ref = refMap.get(col.name);
|
|
104
|
+
const columnConfig = {
|
|
105
|
+
name: col.name,
|
|
106
|
+
label: columnNameToLabel(col.name),
|
|
107
|
+
dataType: col.dataType,
|
|
108
|
+
columnType: col.columnType,
|
|
109
|
+
required: col.notNull && !col.hasDefault,
|
|
110
|
+
hasDefault: col.hasDefault,
|
|
111
|
+
isPrimaryKey: col.primary
|
|
112
|
+
};
|
|
113
|
+
if (col.enumValues && col.enumValues.length > 0) {
|
|
114
|
+
columnConfig.enumValues = [...col.enumValues];
|
|
115
|
+
}
|
|
116
|
+
if (ref) {
|
|
117
|
+
columnConfig.referencesTable = ref.table;
|
|
118
|
+
columnConfig.referencesColumn = ref.column;
|
|
119
|
+
}
|
|
120
|
+
result.push(columnConfig);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function defaultSearchableColumns(columns) {
|
|
125
|
+
const textCol = columns.find((c) => c.dataType === "string");
|
|
126
|
+
return textCol ? [textCol.name] : [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/loader.ts
|
|
130
|
+
import { getTableColumns as getTableColumns2, getTableName as getTableName2, eq, like, or, asc, desc } from "drizzle-orm";
|
|
131
|
+
|
|
132
|
+
// src/utils.ts
|
|
133
|
+
var USERS_VIEW = "_users";
|
|
134
|
+
function parseAdminParams(url) {
|
|
135
|
+
const view = url.searchParams.get("view");
|
|
136
|
+
if (!view || view === "dashboard") {
|
|
137
|
+
return { kind: "dashboard" };
|
|
138
|
+
}
|
|
139
|
+
if (view === USERS_VIEW) {
|
|
140
|
+
const id2 = url.searchParams.get("id");
|
|
141
|
+
if (id2) {
|
|
142
|
+
return { kind: "user-detail", id: id2 };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
kind: "user-list",
|
|
146
|
+
page: parsePageParam(url.searchParams.get("page")),
|
|
147
|
+
search: url.searchParams.get("search") ?? ""
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const table = view;
|
|
151
|
+
const id = url.searchParams.get("id");
|
|
152
|
+
const mode = url.searchParams.get("mode");
|
|
153
|
+
if (mode === "create") {
|
|
154
|
+
return { kind: "create", table };
|
|
155
|
+
}
|
|
156
|
+
if (id && mode === "edit") {
|
|
157
|
+
return { kind: "edit", table, id };
|
|
158
|
+
}
|
|
159
|
+
if (id) {
|
|
160
|
+
return { kind: "detail", table, id };
|
|
161
|
+
}
|
|
162
|
+
const dirParam = url.searchParams.get("dir");
|
|
163
|
+
const direction = dirParam === "asc" ? "asc" : "desc";
|
|
164
|
+
return {
|
|
165
|
+
kind: "list",
|
|
166
|
+
table,
|
|
167
|
+
page: parsePageParam(url.searchParams.get("page")),
|
|
168
|
+
sort: url.searchParams.get("sort"),
|
|
169
|
+
direction,
|
|
170
|
+
search: url.searchParams.get("search") ?? ""
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function buildAdminUrl(params) {
|
|
174
|
+
const sp = new URLSearchParams();
|
|
175
|
+
switch (params.kind) {
|
|
176
|
+
case "dashboard":
|
|
177
|
+
return "";
|
|
178
|
+
case "list":
|
|
179
|
+
sp.set("view", params.table);
|
|
180
|
+
if (params.page > 1) {
|
|
181
|
+
sp.set("page", String(params.page));
|
|
182
|
+
}
|
|
183
|
+
if (params.sort) {
|
|
184
|
+
sp.set("sort", params.sort);
|
|
185
|
+
}
|
|
186
|
+
if (params.direction !== "desc") {
|
|
187
|
+
sp.set("dir", params.direction);
|
|
188
|
+
}
|
|
189
|
+
if (params.search) {
|
|
190
|
+
sp.set("search", params.search);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case "detail":
|
|
194
|
+
sp.set("view", params.table);
|
|
195
|
+
sp.set("id", params.id);
|
|
196
|
+
break;
|
|
197
|
+
case "create":
|
|
198
|
+
sp.set("view", params.table);
|
|
199
|
+
sp.set("mode", "create");
|
|
200
|
+
break;
|
|
201
|
+
case "edit":
|
|
202
|
+
sp.set("view", params.table);
|
|
203
|
+
sp.set("id", params.id);
|
|
204
|
+
sp.set("mode", "edit");
|
|
205
|
+
break;
|
|
206
|
+
case "user-list":
|
|
207
|
+
sp.set("view", USERS_VIEW);
|
|
208
|
+
if (params.page > 1) {
|
|
209
|
+
sp.set("page", String(params.page));
|
|
210
|
+
}
|
|
211
|
+
if (params.search) {
|
|
212
|
+
sp.set("search", params.search);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case "user-detail":
|
|
216
|
+
sp.set("view", USERS_VIEW);
|
|
217
|
+
sp.set("id", params.id);
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
const str = sp.toString();
|
|
221
|
+
return str ? `?${str}` : "";
|
|
222
|
+
}
|
|
223
|
+
function parsePageParam(value) {
|
|
224
|
+
if (!value) return 1;
|
|
225
|
+
const parsed = parseInt(value, 10);
|
|
226
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/loader.ts
|
|
230
|
+
var PAGE_SIZE = 20;
|
|
231
|
+
function asRecords(value) {
|
|
232
|
+
if (!Array.isArray(value)) {
|
|
233
|
+
throw new TypeError(
|
|
234
|
+
`Expected an array of records, got ${typeof value}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
for (let i = 0; i < value.length; i++) {
|
|
238
|
+
if (typeof value[i] !== "object" || value[i] === null || Array.isArray(value[i])) {
|
|
239
|
+
throw new TypeError(
|
|
240
|
+
`Expected record at index ${i}, got ${Array.isArray(value[i]) ? "array" : typeof value[i]}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
function asRecord(value) {
|
|
247
|
+
if (value === void 0 || value === null) {
|
|
248
|
+
return void 0;
|
|
249
|
+
}
|
|
250
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
251
|
+
throw new TypeError(
|
|
252
|
+
`Expected a record, got ${Array.isArray(value) ? "array" : typeof value}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
function tableList(tableMetas) {
|
|
258
|
+
return tableMetas.filter((t) => t.name !== "users" && t.name !== "user").map((t) => ({ name: t.name, label: t.label }));
|
|
259
|
+
}
|
|
260
|
+
function findTableMeta(tableMetas, name) {
|
|
261
|
+
return tableMetas.find((t) => t.name === name);
|
|
262
|
+
}
|
|
263
|
+
function getColumn(drizzleTable, columnName) {
|
|
264
|
+
const columns = getTableColumns2(drizzleTable);
|
|
265
|
+
return columns[columnName];
|
|
266
|
+
}
|
|
267
|
+
function findUsersTable(schema) {
|
|
268
|
+
for (const table of Object.values(schema)) {
|
|
269
|
+
const name = getTableName2(table);
|
|
270
|
+
if (name === "user" || name === "users") {
|
|
271
|
+
return table;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
function buildSearchWhere(drizzleTable, searchableColumns, search) {
|
|
277
|
+
if (!search || searchableColumns.length === 0) {
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
const pattern = `%${search}%`;
|
|
281
|
+
const conditions = searchableColumns.map((colName) => {
|
|
282
|
+
const col = getColumn(drizzleTable, colName);
|
|
283
|
+
if (!col) return void 0;
|
|
284
|
+
return like(col, pattern);
|
|
285
|
+
}).filter((c) => c != null);
|
|
286
|
+
if (conditions.length === 0) return void 0;
|
|
287
|
+
if (conditions.length === 1) return conditions[0];
|
|
288
|
+
return or(...conditions);
|
|
289
|
+
}
|
|
290
|
+
function buildOrderBy(drizzleTable, sortColumn, direction) {
|
|
291
|
+
const col = getColumn(drizzleTable, sortColumn);
|
|
292
|
+
if (!col) return void 0;
|
|
293
|
+
return direction === "asc" ? asc(col) : desc(col);
|
|
294
|
+
}
|
|
295
|
+
async function loadDashboard(config, db, tableMetas, user) {
|
|
296
|
+
const tables = tableList(tableMetas);
|
|
297
|
+
const stats = [];
|
|
298
|
+
const recentItems = [];
|
|
299
|
+
if (config.dashboard?.widgets) {
|
|
300
|
+
for (const widget of config.dashboard.widgets) {
|
|
301
|
+
const meta = findTableMeta(tableMetas, widget.table);
|
|
302
|
+
if (!meta) continue;
|
|
303
|
+
if (widget.type === "count") {
|
|
304
|
+
const rows = await db.query(meta.drizzleTable).findMany().run({});
|
|
305
|
+
stats.push({ label: widget.label, value: rows.length });
|
|
306
|
+
} else if (widget.type === "recent") {
|
|
307
|
+
const limit = widget.limit ?? 5;
|
|
308
|
+
const items = asRecords(await db.query(meta.drizzleTable).findMany({
|
|
309
|
+
limit,
|
|
310
|
+
orderBy: buildOrderBy(
|
|
311
|
+
meta.drizzleTable,
|
|
312
|
+
meta.primaryKey,
|
|
313
|
+
"desc"
|
|
314
|
+
)
|
|
315
|
+
}).run({}));
|
|
316
|
+
recentItems.push({
|
|
317
|
+
table: meta.name,
|
|
318
|
+
label: widget.label,
|
|
319
|
+
items,
|
|
320
|
+
columns: meta.listColumns
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
for (const meta of tableMetas) {
|
|
326
|
+
const rows = await db.query(meta.drizzleTable).findMany().run({});
|
|
327
|
+
stats.push({ label: meta.label, value: rows.length });
|
|
328
|
+
}
|
|
329
|
+
const firstMeta = tableMetas[0];
|
|
330
|
+
if (firstMeta) {
|
|
331
|
+
const items = asRecords(await db.query(firstMeta.drizzleTable).findMany({
|
|
332
|
+
limit: 5,
|
|
333
|
+
orderBy: buildOrderBy(
|
|
334
|
+
firstMeta.drizzleTable,
|
|
335
|
+
firstMeta.primaryKey,
|
|
336
|
+
"desc"
|
|
337
|
+
)
|
|
338
|
+
}).run({}));
|
|
339
|
+
recentItems.push({
|
|
340
|
+
table: firstMeta.name,
|
|
341
|
+
label: firstMeta.label,
|
|
342
|
+
items,
|
|
343
|
+
columns: firstMeta.listColumns
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
view: "dashboard",
|
|
349
|
+
user,
|
|
350
|
+
tables,
|
|
351
|
+
stats,
|
|
352
|
+
recentItems
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async function loadList(db, tableMetas, user, tableName, page, sort, direction, search) {
|
|
356
|
+
const tables = tableList(tableMetas);
|
|
357
|
+
const meta = findTableMeta(tableMetas, tableName);
|
|
358
|
+
if (!meta) {
|
|
359
|
+
return {
|
|
360
|
+
view: "error",
|
|
361
|
+
user,
|
|
362
|
+
tables,
|
|
363
|
+
message: `Table "${tableName}" not found.`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const sortColumn = sort ?? meta.defaultSort.column;
|
|
367
|
+
const sortDirection = sort ? direction : meta.defaultSort.direction;
|
|
368
|
+
const where = buildSearchWhere(
|
|
369
|
+
meta.drizzleTable,
|
|
370
|
+
meta.searchableColumns,
|
|
371
|
+
search
|
|
372
|
+
);
|
|
373
|
+
const orderBy = buildOrderBy(meta.drizzleTable, sortColumn, sortDirection);
|
|
374
|
+
const allRows = await db.query(meta.drizzleTable).findMany({ where }).run({});
|
|
375
|
+
const total = allRows.length;
|
|
376
|
+
const offset = (page - 1) * PAGE_SIZE;
|
|
377
|
+
const items = asRecords(await db.query(meta.drizzleTable).findMany({
|
|
378
|
+
where,
|
|
379
|
+
orderBy,
|
|
380
|
+
limit: PAGE_SIZE,
|
|
381
|
+
offset
|
|
382
|
+
}).run({}));
|
|
383
|
+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
384
|
+
return {
|
|
385
|
+
view: "list",
|
|
386
|
+
user,
|
|
387
|
+
tables,
|
|
388
|
+
tableName: meta.name,
|
|
389
|
+
tableLabel: meta.label,
|
|
390
|
+
items,
|
|
391
|
+
total,
|
|
392
|
+
page,
|
|
393
|
+
totalPages,
|
|
394
|
+
columns: meta.columns,
|
|
395
|
+
searchable: meta.searchableColumns,
|
|
396
|
+
sort: { column: sortColumn, direction: sortDirection },
|
|
397
|
+
search
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async function loadDetail(db, tableMetas, user, tableName, id) {
|
|
401
|
+
const tables = tableList(tableMetas);
|
|
402
|
+
const meta = findTableMeta(tableMetas, tableName);
|
|
403
|
+
if (!meta) {
|
|
404
|
+
return {
|
|
405
|
+
view: "error",
|
|
406
|
+
user,
|
|
407
|
+
tables,
|
|
408
|
+
message: `Table "${tableName}" not found.`
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const pkCol = getColumn(meta.drizzleTable, meta.primaryKey);
|
|
412
|
+
if (!pkCol) {
|
|
413
|
+
return {
|
|
414
|
+
view: "error",
|
|
415
|
+
user,
|
|
416
|
+
tables,
|
|
417
|
+
message: `Primary key column "${meta.primaryKey}" not found in table "${tableName}".`
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const item = asRecord(await db.query(meta.drizzleTable).findFirst({ where: eq(pkCol, id) }).run({}));
|
|
421
|
+
if (!item) {
|
|
422
|
+
return {
|
|
423
|
+
view: "error",
|
|
424
|
+
user,
|
|
425
|
+
tables,
|
|
426
|
+
message: `Record with id "${id}" not found in table "${tableName}".`
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
view: "detail",
|
|
431
|
+
user,
|
|
432
|
+
tables,
|
|
433
|
+
tableName: meta.name,
|
|
434
|
+
tableLabel: meta.label,
|
|
435
|
+
item,
|
|
436
|
+
columns: meta.columns
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function loadCreate(tableMetas, user, tableName) {
|
|
440
|
+
const tables = tableList(tableMetas);
|
|
441
|
+
const meta = findTableMeta(tableMetas, tableName);
|
|
442
|
+
if (!meta) {
|
|
443
|
+
return {
|
|
444
|
+
view: "error",
|
|
445
|
+
user,
|
|
446
|
+
tables,
|
|
447
|
+
message: `Table "${tableName}" not found.`
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
view: "create",
|
|
452
|
+
user,
|
|
453
|
+
tables,
|
|
454
|
+
tableName: meta.name,
|
|
455
|
+
tableLabel: meta.label,
|
|
456
|
+
columns: meta.columns
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function loadEdit(db, tableMetas, user, tableName, id) {
|
|
460
|
+
const tables = tableList(tableMetas);
|
|
461
|
+
const meta = findTableMeta(tableMetas, tableName);
|
|
462
|
+
if (!meta) {
|
|
463
|
+
return {
|
|
464
|
+
view: "error",
|
|
465
|
+
user,
|
|
466
|
+
tables,
|
|
467
|
+
message: `Table "${tableName}" not found.`
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const pkCol = getColumn(meta.drizzleTable, meta.primaryKey);
|
|
471
|
+
if (!pkCol) {
|
|
472
|
+
return {
|
|
473
|
+
view: "error",
|
|
474
|
+
user,
|
|
475
|
+
tables,
|
|
476
|
+
message: `Primary key column "${meta.primaryKey}" not found in table "${tableName}".`
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const item = asRecord(await db.query(meta.drizzleTable).findFirst({ where: eq(pkCol, id) }).run({}));
|
|
480
|
+
if (!item) {
|
|
481
|
+
return {
|
|
482
|
+
view: "error",
|
|
483
|
+
user,
|
|
484
|
+
tables,
|
|
485
|
+
message: `Record with id "${id}" not found in table "${tableName}".`
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
view: "edit",
|
|
490
|
+
user,
|
|
491
|
+
tables,
|
|
492
|
+
tableName: meta.name,
|
|
493
|
+
tableLabel: meta.label,
|
|
494
|
+
item,
|
|
495
|
+
columns: meta.columns
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async function loadUserList(config, db, tableMetas, user, page, search) {
|
|
499
|
+
const tables = tableList(tableMetas);
|
|
500
|
+
const usersTable = findUsersTable(config.schema);
|
|
501
|
+
if (!usersTable) {
|
|
502
|
+
return {
|
|
503
|
+
view: "error",
|
|
504
|
+
user,
|
|
505
|
+
tables,
|
|
506
|
+
message: "Users table not found in schema."
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const unsafeDb = db.unsafe();
|
|
510
|
+
const columns = getTableColumns2(usersTable);
|
|
511
|
+
const searchableUserCols = ["name", "email"].filter((c) => columns[c] != null);
|
|
512
|
+
const where = buildSearchWhere(usersTable, searchableUserCols, search);
|
|
513
|
+
const allRows = await unsafeDb.query(usersTable).findMany({ where }).run({});
|
|
514
|
+
const total = allRows.length;
|
|
515
|
+
const offset = (page - 1) * PAGE_SIZE;
|
|
516
|
+
const rawUsers = asRecords(await unsafeDb.query(usersTable).findMany({
|
|
517
|
+
where,
|
|
518
|
+
limit: PAGE_SIZE,
|
|
519
|
+
offset
|
|
520
|
+
}).run({}));
|
|
521
|
+
const items = [];
|
|
522
|
+
for (const raw of rawUsers) {
|
|
523
|
+
const userId = String(raw["id"] ?? "");
|
|
524
|
+
const roles = await config.auth.getRoles(userId);
|
|
525
|
+
items.push({
|
|
526
|
+
id: userId,
|
|
527
|
+
email: String(raw["email"] ?? ""),
|
|
528
|
+
name: String(raw["name"] ?? ""),
|
|
529
|
+
avatarUrl: raw["avatarUrl"] != null ? String(raw["avatarUrl"]) : raw["avatar_url"] != null ? String(raw["avatar_url"]) : null,
|
|
530
|
+
roles,
|
|
531
|
+
createdAt: raw["createdAt"] != null ? String(raw["createdAt"]) : raw["created_at"] != null ? String(raw["created_at"]) : void 0
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
535
|
+
const assignableRoles = config.users?.assignableRoles ?? [];
|
|
536
|
+
return {
|
|
537
|
+
view: "users",
|
|
538
|
+
user,
|
|
539
|
+
tables,
|
|
540
|
+
items,
|
|
541
|
+
total,
|
|
542
|
+
page,
|
|
543
|
+
totalPages,
|
|
544
|
+
search,
|
|
545
|
+
assignableRoles
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
async function loadUserDetail(config, db, tableMetas, user, targetId) {
|
|
549
|
+
const tables = tableList(tableMetas);
|
|
550
|
+
const usersTable = findUsersTable(config.schema);
|
|
551
|
+
if (!usersTable) {
|
|
552
|
+
return {
|
|
553
|
+
view: "error",
|
|
554
|
+
user,
|
|
555
|
+
tables,
|
|
556
|
+
message: "Users table not found in schema."
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const unsafeDb = db.unsafe();
|
|
560
|
+
const pkCol = getColumn(usersTable, "id");
|
|
561
|
+
if (!pkCol) {
|
|
562
|
+
return {
|
|
563
|
+
view: "error",
|
|
564
|
+
user,
|
|
565
|
+
tables,
|
|
566
|
+
message: 'Users table has no "id" column.'
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const raw = asRecord(await unsafeDb.query(usersTable).findFirst({ where: eq(pkCol, targetId) }).run({}));
|
|
570
|
+
if (!raw) {
|
|
571
|
+
return {
|
|
572
|
+
view: "error",
|
|
573
|
+
user,
|
|
574
|
+
tables,
|
|
575
|
+
message: `User with id "${targetId}" not found.`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
const roles = await config.auth.getRoles(targetId);
|
|
579
|
+
const assignableRoles = config.users?.assignableRoles ?? [];
|
|
580
|
+
const targetUser = {
|
|
581
|
+
id: String(raw["id"] ?? ""),
|
|
582
|
+
email: String(raw["email"] ?? ""),
|
|
583
|
+
name: String(raw["name"] ?? ""),
|
|
584
|
+
avatarUrl: raw["avatarUrl"] != null ? String(raw["avatarUrl"]) : raw["avatar_url"] != null ? String(raw["avatar_url"]) : null,
|
|
585
|
+
roles,
|
|
586
|
+
createdAt: raw["createdAt"] != null ? String(raw["createdAt"]) : raw["created_at"] != null ? String(raw["created_at"]) : void 0
|
|
587
|
+
};
|
|
588
|
+
return {
|
|
589
|
+
view: "user-detail",
|
|
590
|
+
user,
|
|
591
|
+
tables,
|
|
592
|
+
targetUser,
|
|
593
|
+
assignableRoles
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
function createAdminLoader(config, tableMetas) {
|
|
597
|
+
const requiredRole = config.requiredRole ?? "admin";
|
|
598
|
+
return async function adminLoader(request) {
|
|
599
|
+
const { user, grants } = await config.auth.requireUser(request);
|
|
600
|
+
if (!config.auth.hasRole(user, requiredRole)) {
|
|
601
|
+
throw new Response(null, {
|
|
602
|
+
status: 302,
|
|
603
|
+
headers: { Location: "/" }
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const db = config.db(grants, user);
|
|
607
|
+
const url = new URL(request.url);
|
|
608
|
+
const params = parseAdminParams(url);
|
|
609
|
+
switch (params.kind) {
|
|
610
|
+
case "dashboard":
|
|
611
|
+
return loadDashboard(config, db, tableMetas, user);
|
|
612
|
+
case "list":
|
|
613
|
+
return loadList(
|
|
614
|
+
db,
|
|
615
|
+
tableMetas,
|
|
616
|
+
user,
|
|
617
|
+
params.table,
|
|
618
|
+
params.page,
|
|
619
|
+
params.sort,
|
|
620
|
+
params.direction,
|
|
621
|
+
params.search
|
|
622
|
+
);
|
|
623
|
+
case "detail":
|
|
624
|
+
return loadDetail(db, tableMetas, user, params.table, params.id);
|
|
625
|
+
case "create":
|
|
626
|
+
return loadCreate(tableMetas, user, params.table);
|
|
627
|
+
case "edit":
|
|
628
|
+
return loadEdit(db, tableMetas, user, params.table, params.id);
|
|
629
|
+
case "user-list":
|
|
630
|
+
return loadUserList(
|
|
631
|
+
config,
|
|
632
|
+
db,
|
|
633
|
+
tableMetas,
|
|
634
|
+
user,
|
|
635
|
+
params.page,
|
|
636
|
+
params.search
|
|
637
|
+
);
|
|
638
|
+
case "user-detail":
|
|
639
|
+
return loadUserDetail(
|
|
640
|
+
config,
|
|
641
|
+
db,
|
|
642
|
+
tableMetas,
|
|
643
|
+
user,
|
|
644
|
+
params.id
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/action.ts
|
|
651
|
+
import { getTableColumns as getTableColumns3, eq as eq2 } from "drizzle-orm";
|
|
652
|
+
var INTERNAL_FIELDS = /* @__PURE__ */ new Set([
|
|
653
|
+
"_action",
|
|
654
|
+
"_table",
|
|
655
|
+
"_id",
|
|
656
|
+
"_actionName"
|
|
657
|
+
]);
|
|
658
|
+
function getColumn2(drizzleTable, columnName) {
|
|
659
|
+
const columns = getTableColumns3(drizzleTable);
|
|
660
|
+
return columns[columnName];
|
|
661
|
+
}
|
|
662
|
+
function findTableMeta2(tableMetas, name) {
|
|
663
|
+
return tableMetas.find((t) => t.name === name);
|
|
664
|
+
}
|
|
665
|
+
function extractFormValues(formData, columns) {
|
|
666
|
+
const values = {};
|
|
667
|
+
for (const col of columns) {
|
|
668
|
+
if (col.isPrimaryKey) continue;
|
|
669
|
+
if (!formData.has(col.name)) continue;
|
|
670
|
+
const rawValue = formData.get(col.name);
|
|
671
|
+
if (INTERNAL_FIELDS.has(col.name)) continue;
|
|
672
|
+
if (col.columnType === "SQLiteBoolean") {
|
|
673
|
+
values[col.name] = rawValue === "true" || rawValue === "on" || rawValue === "1";
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const strValue = rawValue != null ? String(rawValue) : "";
|
|
677
|
+
if (strValue === "" && !col.required) {
|
|
678
|
+
values[col.name] = null;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (col.dataType === "number") {
|
|
682
|
+
const num = Number(strValue);
|
|
683
|
+
values[col.name] = Number.isFinite(num) ? num : strValue;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
values[col.name] = strValue;
|
|
687
|
+
}
|
|
688
|
+
return values;
|
|
689
|
+
}
|
|
690
|
+
function createAdminAction(config, tableMetas) {
|
|
691
|
+
const requiredRole = config.requiredRole ?? "admin";
|
|
692
|
+
return async function adminAction(request) {
|
|
693
|
+
const { user, grants } = await config.auth.requireUser(request);
|
|
694
|
+
if (!config.auth.hasRole(user, requiredRole)) {
|
|
695
|
+
throw new Response("Forbidden", { status: 403 });
|
|
696
|
+
}
|
|
697
|
+
const db = config.db(grants, user);
|
|
698
|
+
const formData = await request.formData();
|
|
699
|
+
const action = formData.get("_action");
|
|
700
|
+
if (typeof action !== "string" || !action) {
|
|
701
|
+
return { error: "Missing _action field." };
|
|
702
|
+
}
|
|
703
|
+
switch (action) {
|
|
704
|
+
case "create":
|
|
705
|
+
return handleCreate(db, tableMetas, formData);
|
|
706
|
+
case "update":
|
|
707
|
+
return handleUpdate(db, tableMetas, formData);
|
|
708
|
+
case "delete":
|
|
709
|
+
return handleDelete(db, tableMetas, formData);
|
|
710
|
+
case "setRole":
|
|
711
|
+
return handleSetRole(config, formData);
|
|
712
|
+
case "removeRole":
|
|
713
|
+
return handleRemoveRole(config, formData);
|
|
714
|
+
case "impersonate":
|
|
715
|
+
if (!config.auth.impersonate) {
|
|
716
|
+
return { error: "Impersonation is not configured." };
|
|
717
|
+
}
|
|
718
|
+
return handleImpersonate(config, user.id, formData, request);
|
|
719
|
+
case "stopImpersonation":
|
|
720
|
+
if (!config.auth.stopImpersonation) {
|
|
721
|
+
return { error: "Impersonation is not configured." };
|
|
722
|
+
}
|
|
723
|
+
return config.auth.stopImpersonation(request);
|
|
724
|
+
case "custom":
|
|
725
|
+
return handleCustomAction(config, tableMetas, formData);
|
|
726
|
+
default:
|
|
727
|
+
return { error: `Unknown action: "${action}".` };
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
async function handleCreate(db, tableMetas, formData) {
|
|
732
|
+
const tableName = formData.get("_table");
|
|
733
|
+
if (typeof tableName !== "string" || !tableName) {
|
|
734
|
+
return { error: "Missing _table field." };
|
|
735
|
+
}
|
|
736
|
+
const meta = findTableMeta2(tableMetas, tableName);
|
|
737
|
+
if (!meta) {
|
|
738
|
+
return { error: `Table "${tableName}" not found.` };
|
|
739
|
+
}
|
|
740
|
+
const values = extractFormValues(formData, meta.columns);
|
|
741
|
+
try {
|
|
742
|
+
await db.insert(meta.drizzleTable).values(values).run({});
|
|
743
|
+
return { success: `Created new ${meta.label} record.` };
|
|
744
|
+
} catch (err) {
|
|
745
|
+
const message = err instanceof Error ? err.message : "Unknown error during create.";
|
|
746
|
+
return { error: message };
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async function handleUpdate(db, tableMetas, formData) {
|
|
750
|
+
const tableName = formData.get("_table");
|
|
751
|
+
if (typeof tableName !== "string" || !tableName) {
|
|
752
|
+
return { error: "Missing _table field." };
|
|
753
|
+
}
|
|
754
|
+
const id = formData.get("_id");
|
|
755
|
+
if (typeof id !== "string" || !id) {
|
|
756
|
+
return { error: "Missing _id field." };
|
|
757
|
+
}
|
|
758
|
+
const meta = findTableMeta2(tableMetas, tableName);
|
|
759
|
+
if (!meta) {
|
|
760
|
+
return { error: `Table "${tableName}" not found.` };
|
|
761
|
+
}
|
|
762
|
+
const pkCol = getColumn2(meta.drizzleTable, meta.primaryKey);
|
|
763
|
+
if (!pkCol) {
|
|
764
|
+
return { error: `Primary key column "${meta.primaryKey}" not found.` };
|
|
765
|
+
}
|
|
766
|
+
const values = extractFormValues(formData, meta.columns);
|
|
767
|
+
try {
|
|
768
|
+
await db.update(meta.drizzleTable).set(values).where(eq2(pkCol, id)).run({});
|
|
769
|
+
return { success: `Updated ${meta.label} record.` };
|
|
770
|
+
} catch (err) {
|
|
771
|
+
const message = err instanceof Error ? err.message : "Unknown error during update.";
|
|
772
|
+
return { error: message };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function handleDelete(db, tableMetas, formData) {
|
|
776
|
+
const tableName = formData.get("_table");
|
|
777
|
+
if (typeof tableName !== "string" || !tableName) {
|
|
778
|
+
return { error: "Missing _table field." };
|
|
779
|
+
}
|
|
780
|
+
const id = formData.get("_id");
|
|
781
|
+
if (typeof id !== "string" || !id) {
|
|
782
|
+
return { error: "Missing _id field." };
|
|
783
|
+
}
|
|
784
|
+
const meta = findTableMeta2(tableMetas, tableName);
|
|
785
|
+
if (!meta) {
|
|
786
|
+
return { error: `Table "${tableName}" not found.` };
|
|
787
|
+
}
|
|
788
|
+
const pkCol = getColumn2(meta.drizzleTable, meta.primaryKey);
|
|
789
|
+
if (!pkCol) {
|
|
790
|
+
return { error: `Primary key column "${meta.primaryKey}" not found.` };
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
await db.delete(meta.drizzleTable).where(eq2(pkCol, id)).run({});
|
|
794
|
+
return { success: `Deleted ${meta.label} record.` };
|
|
795
|
+
} catch (err) {
|
|
796
|
+
const message = err instanceof Error ? err.message : "Unknown error during delete.";
|
|
797
|
+
return { error: message };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
async function handleSetRole(config, formData) {
|
|
801
|
+
const userId = formData.get("_id");
|
|
802
|
+
if (typeof userId !== "string" || !userId) {
|
|
803
|
+
return { error: "Missing _id field." };
|
|
804
|
+
}
|
|
805
|
+
const role = formData.get("role");
|
|
806
|
+
if (typeof role !== "string" || !role) {
|
|
807
|
+
return { error: "Missing role field." };
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
await config.auth.setRole(userId, role);
|
|
811
|
+
return { success: `Role "${role}" added to user.` };
|
|
812
|
+
} catch (err) {
|
|
813
|
+
const message = err instanceof Error ? err.message : "Unknown error setting role.";
|
|
814
|
+
return { error: message };
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async function handleRemoveRole(config, formData) {
|
|
818
|
+
const userId = formData.get("_id");
|
|
819
|
+
if (typeof userId !== "string" || !userId) {
|
|
820
|
+
return { error: "Missing _id field." };
|
|
821
|
+
}
|
|
822
|
+
const role = formData.get("role");
|
|
823
|
+
if (typeof role !== "string" || !role) {
|
|
824
|
+
return { error: "Missing role field." };
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
await config.auth.removeRole(userId, role);
|
|
828
|
+
return { success: `Role "${role}" removed from user.` };
|
|
829
|
+
} catch (err) {
|
|
830
|
+
const message = err instanceof Error ? err.message : "Unknown error removing role.";
|
|
831
|
+
return { error: message };
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function handleImpersonate(config, adminId, formData, request) {
|
|
835
|
+
const targetId = formData.get("_id");
|
|
836
|
+
if (typeof targetId !== "string" || !targetId) {
|
|
837
|
+
throw new Response("Missing _id for impersonation", { status: 400 });
|
|
838
|
+
}
|
|
839
|
+
return config.auth.impersonate(adminId, targetId, request);
|
|
840
|
+
}
|
|
841
|
+
async function handleCustomAction(_config, tableMetas, formData) {
|
|
842
|
+
const tableName = formData.get("_table");
|
|
843
|
+
if (typeof tableName !== "string" || !tableName) {
|
|
844
|
+
return { error: "Missing _table field." };
|
|
845
|
+
}
|
|
846
|
+
const actionName = formData.get("_actionName");
|
|
847
|
+
if (typeof actionName !== "string" || !actionName) {
|
|
848
|
+
return { error: "Missing _actionName field." };
|
|
849
|
+
}
|
|
850
|
+
const id = formData.get("_id");
|
|
851
|
+
if (typeof id !== "string" || !id) {
|
|
852
|
+
return { error: "Missing _id field." };
|
|
853
|
+
}
|
|
854
|
+
const meta = findTableMeta2(tableMetas, tableName);
|
|
855
|
+
if (!meta) {
|
|
856
|
+
return { error: `Table "${tableName}" not found.` };
|
|
857
|
+
}
|
|
858
|
+
const rowActions = meta.overrides.actions?.row;
|
|
859
|
+
if (!rowActions) {
|
|
860
|
+
return { error: `No custom actions defined for table "${tableName}".` };
|
|
861
|
+
}
|
|
862
|
+
const rowAction = rowActions.find((a) => a.label === actionName);
|
|
863
|
+
if (!rowAction) {
|
|
864
|
+
return { error: `Action "${actionName}" not found for table "${tableName}".` };
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
await rowAction.action(id, formData);
|
|
868
|
+
return { success: `Action "${actionName}" completed.` };
|
|
869
|
+
} catch (err) {
|
|
870
|
+
const message = err instanceof Error ? err.message : "Unknown error during custom action.";
|
|
871
|
+
return { error: message };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/components/admin-root.tsx
|
|
876
|
+
import "react";
|
|
877
|
+
import { useLoaderData, useActionData, useSubmit as useSubmit6 } from "react-router";
|
|
878
|
+
import Box8 from "@mui/joy/Box";
|
|
879
|
+
import Button5 from "@mui/joy/Button";
|
|
880
|
+
import Typography8 from "@mui/joy/Typography";
|
|
881
|
+
import { ConfirmProvider } from "@cfast/ui";
|
|
882
|
+
|
|
883
|
+
// src/components/sidebar.tsx
|
|
884
|
+
import "react";
|
|
885
|
+
import { Link, useSearchParams } from "react-router";
|
|
886
|
+
import Sheet from "@mui/joy/Sheet";
|
|
887
|
+
import List from "@mui/joy/List";
|
|
888
|
+
import ListItem from "@mui/joy/ListItem";
|
|
889
|
+
import ListItemButton from "@mui/joy/ListItemButton";
|
|
890
|
+
import Typography from "@mui/joy/Typography";
|
|
891
|
+
import Divider from "@mui/joy/Divider";
|
|
892
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
893
|
+
function Sidebar({ tables }) {
|
|
894
|
+
const [searchParams] = useSearchParams();
|
|
895
|
+
const currentView = searchParams.get("view");
|
|
896
|
+
return /* @__PURE__ */ jsxs(
|
|
897
|
+
Sheet,
|
|
898
|
+
{
|
|
899
|
+
sx: {
|
|
900
|
+
width: 240,
|
|
901
|
+
minHeight: "100vh",
|
|
902
|
+
borderRight: "1px solid",
|
|
903
|
+
borderColor: "divider",
|
|
904
|
+
p: 2
|
|
905
|
+
},
|
|
906
|
+
children: [
|
|
907
|
+
/* @__PURE__ */ jsx(Typography, { level: "h4", sx: { mb: 2 }, children: "Admin" }),
|
|
908
|
+
/* @__PURE__ */ jsxs(List, { size: "sm", children: [
|
|
909
|
+
/* @__PURE__ */ jsx(ListItem, { children: /* @__PURE__ */ jsx(
|
|
910
|
+
ListItemButton,
|
|
911
|
+
{
|
|
912
|
+
selected: currentView === null,
|
|
913
|
+
component: Link,
|
|
914
|
+
to: buildAdminUrl({ kind: "dashboard" }) || ".",
|
|
915
|
+
children: "Dashboard"
|
|
916
|
+
}
|
|
917
|
+
) }),
|
|
918
|
+
/* @__PURE__ */ jsx(Divider, { sx: { my: 1 } }),
|
|
919
|
+
tables.map((table) => /* @__PURE__ */ jsx(ListItem, { children: /* @__PURE__ */ jsx(
|
|
920
|
+
ListItemButton,
|
|
921
|
+
{
|
|
922
|
+
selected: currentView === table.name,
|
|
923
|
+
component: Link,
|
|
924
|
+
to: buildAdminUrl({
|
|
925
|
+
kind: "list",
|
|
926
|
+
table: table.name,
|
|
927
|
+
page: 1,
|
|
928
|
+
sort: null,
|
|
929
|
+
direction: "desc",
|
|
930
|
+
search: ""
|
|
931
|
+
}),
|
|
932
|
+
children: table.label
|
|
933
|
+
}
|
|
934
|
+
) }, table.name)),
|
|
935
|
+
/* @__PURE__ */ jsx(Divider, { sx: { my: 1 } }),
|
|
936
|
+
/* @__PURE__ */ jsx(ListItem, { children: /* @__PURE__ */ jsx(
|
|
937
|
+
ListItemButton,
|
|
938
|
+
{
|
|
939
|
+
selected: currentView === "_users",
|
|
940
|
+
component: Link,
|
|
941
|
+
to: buildAdminUrl({ kind: "user-list", page: 1, search: "" }),
|
|
942
|
+
children: "Users"
|
|
943
|
+
}
|
|
944
|
+
) })
|
|
945
|
+
] })
|
|
946
|
+
]
|
|
947
|
+
}
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/components/dashboard.tsx
|
|
952
|
+
import "react";
|
|
953
|
+
import { Link as Link2 } from "react-router";
|
|
954
|
+
import Box from "@mui/joy/Box";
|
|
955
|
+
import Card from "@mui/joy/Card";
|
|
956
|
+
import Typography2 from "@mui/joy/Typography";
|
|
957
|
+
import Table from "@mui/joy/Table";
|
|
958
|
+
import Sheet2 from "@mui/joy/Sheet";
|
|
959
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
960
|
+
function Dashboard({ stats, recentItems }) {
|
|
961
|
+
return /* @__PURE__ */ jsxs2(Box, { children: [
|
|
962
|
+
/* @__PURE__ */ jsx2(Typography2, { level: "h2", sx: { mb: 3 }, children: "Dashboard" }),
|
|
963
|
+
stats.length > 0 && /* @__PURE__ */ jsx2(
|
|
964
|
+
Box,
|
|
965
|
+
{
|
|
966
|
+
sx: {
|
|
967
|
+
display: "grid",
|
|
968
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
|
|
969
|
+
gap: 2,
|
|
970
|
+
mb: 4
|
|
971
|
+
},
|
|
972
|
+
children: stats.map((stat) => /* @__PURE__ */ jsxs2(Card, { variant: "outlined", children: [
|
|
973
|
+
/* @__PURE__ */ jsx2(Typography2, { level: "body-sm", textColor: "text.secondary", children: stat.label }),
|
|
974
|
+
/* @__PURE__ */ jsx2(Typography2, { level: "h3", children: stat.value })
|
|
975
|
+
] }, stat.label))
|
|
976
|
+
}
|
|
977
|
+
),
|
|
978
|
+
recentItems.map((section) => /* @__PURE__ */ jsxs2(Box, { sx: { mb: 4 }, children: [
|
|
979
|
+
/* @__PURE__ */ jsxs2(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 1 }, children: [
|
|
980
|
+
/* @__PURE__ */ jsx2(Typography2, { level: "title-lg", children: section.label }),
|
|
981
|
+
/* @__PURE__ */ jsx2(
|
|
982
|
+
Typography2,
|
|
983
|
+
{
|
|
984
|
+
component: Link2,
|
|
985
|
+
to: buildAdminUrl({
|
|
986
|
+
kind: "list",
|
|
987
|
+
table: section.table,
|
|
988
|
+
page: 1,
|
|
989
|
+
sort: null,
|
|
990
|
+
direction: "desc",
|
|
991
|
+
search: ""
|
|
992
|
+
}),
|
|
993
|
+
level: "body-sm",
|
|
994
|
+
children: "View all"
|
|
995
|
+
}
|
|
996
|
+
)
|
|
997
|
+
] }),
|
|
998
|
+
/* @__PURE__ */ jsx2(Sheet2, { variant: "outlined", sx: { borderRadius: "sm", overflow: "auto" }, children: /* @__PURE__ */ jsxs2(Table, { hoverRow: true, children: [
|
|
999
|
+
/* @__PURE__ */ jsx2("thead", { children: /* @__PURE__ */ jsx2("tr", { children: section.columns.map((col) => /* @__PURE__ */ jsx2("th", { children: col }, col)) }) }),
|
|
1000
|
+
/* @__PURE__ */ jsxs2("tbody", { children: [
|
|
1001
|
+
section.items.map((item, idx) => /* @__PURE__ */ jsx2("tr", { children: section.columns.map((col) => /* @__PURE__ */ jsx2("td", { children: formatCellValue(item[col]) }, col)) }, idx)),
|
|
1002
|
+
section.items.length === 0 && /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2("td", { colSpan: section.columns.length, children: /* @__PURE__ */ jsx2(Typography2, { level: "body-sm", textAlign: "center", sx: { py: 2 }, children: "No items yet." }) }) })
|
|
1003
|
+
] })
|
|
1004
|
+
] }) })
|
|
1005
|
+
] }, section.table))
|
|
1006
|
+
] });
|
|
1007
|
+
}
|
|
1008
|
+
function formatCellValue(value) {
|
|
1009
|
+
if (value === null || value === void 0) return "-";
|
|
1010
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
1011
|
+
return String(value);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/components/table-list.tsx
|
|
1015
|
+
import { useState } from "react";
|
|
1016
|
+
import { Link as Link3, useSubmit, useNavigate } from "react-router";
|
|
1017
|
+
import Box3 from "@mui/joy/Box";
|
|
1018
|
+
import Typography3 from "@mui/joy/Typography";
|
|
1019
|
+
import Button from "@mui/joy/Button";
|
|
1020
|
+
import Input from "@mui/joy/Input";
|
|
1021
|
+
import Table2 from "@mui/joy/Table";
|
|
1022
|
+
import Sheet3 from "@mui/joy/Sheet";
|
|
1023
|
+
import Stack from "@mui/joy/Stack";
|
|
1024
|
+
import { useConfirm } from "@cfast/ui";
|
|
1025
|
+
|
|
1026
|
+
// src/components/action-result.tsx
|
|
1027
|
+
import "react";
|
|
1028
|
+
import Box2 from "@mui/joy/Box";
|
|
1029
|
+
import Chip from "@mui/joy/Chip";
|
|
1030
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1031
|
+
function ActionResultDisplay({
|
|
1032
|
+
result
|
|
1033
|
+
}) {
|
|
1034
|
+
if (!result) return null;
|
|
1035
|
+
if ("success" in result) {
|
|
1036
|
+
return /* @__PURE__ */ jsx3(Chip, { color: "success", variant: "soft", sx: { mb: 2 }, children: result.success });
|
|
1037
|
+
}
|
|
1038
|
+
if ("error" in result) {
|
|
1039
|
+
return /* @__PURE__ */ jsx3(Chip, { color: "danger", variant: "soft", sx: { mb: 2 }, children: result.error });
|
|
1040
|
+
}
|
|
1041
|
+
if ("fieldErrors" in result) {
|
|
1042
|
+
return /* @__PURE__ */ jsx3(Box2, { sx: { mb: 2 }, children: Object.entries(result.fieldErrors).map(([field, error]) => /* @__PURE__ */ jsxs3(Chip, { color: "danger", variant: "soft", sx: { mr: 1, mb: 1 }, children: [
|
|
1043
|
+
field,
|
|
1044
|
+
": ",
|
|
1045
|
+
error
|
|
1046
|
+
] }, field)) });
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/components/table-list.tsx
|
|
1052
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1053
|
+
function TableList({
|
|
1054
|
+
tableName,
|
|
1055
|
+
tableLabel,
|
|
1056
|
+
items,
|
|
1057
|
+
total,
|
|
1058
|
+
page,
|
|
1059
|
+
totalPages,
|
|
1060
|
+
columns,
|
|
1061
|
+
searchable,
|
|
1062
|
+
sort,
|
|
1063
|
+
search,
|
|
1064
|
+
primaryKey,
|
|
1065
|
+
actionResult
|
|
1066
|
+
}) {
|
|
1067
|
+
const submit = useSubmit();
|
|
1068
|
+
const navigate = useNavigate();
|
|
1069
|
+
const confirm = useConfirm();
|
|
1070
|
+
const [searchValue, setSearchValue] = useState(search);
|
|
1071
|
+
const listColumns = columns.filter((c) => !c.isPrimaryKey);
|
|
1072
|
+
function sortUrl(columnName) {
|
|
1073
|
+
const isSameColumn = sort.column === columnName;
|
|
1074
|
+
const newDirection = isSameColumn && sort.direction === "asc" ? "desc" : "asc";
|
|
1075
|
+
return buildAdminUrl({
|
|
1076
|
+
kind: "list",
|
|
1077
|
+
table: tableName,
|
|
1078
|
+
page: 1,
|
|
1079
|
+
sort: columnName,
|
|
1080
|
+
direction: newDirection,
|
|
1081
|
+
search
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
function handleSearchSubmit(e) {
|
|
1085
|
+
e.preventDefault();
|
|
1086
|
+
const url = buildAdminUrl({
|
|
1087
|
+
kind: "list",
|
|
1088
|
+
table: tableName,
|
|
1089
|
+
page: 1,
|
|
1090
|
+
sort: sort.column,
|
|
1091
|
+
direction: sort.direction,
|
|
1092
|
+
search: searchValue
|
|
1093
|
+
});
|
|
1094
|
+
navigate({ search: url.startsWith("?") ? url : "" });
|
|
1095
|
+
}
|
|
1096
|
+
async function handleDelete2(id) {
|
|
1097
|
+
const confirmed = await confirm({
|
|
1098
|
+
title: "Delete record",
|
|
1099
|
+
description: `Are you sure you want to delete this ${tableLabel} record?`,
|
|
1100
|
+
variant: "danger",
|
|
1101
|
+
confirmLabel: "Delete"
|
|
1102
|
+
});
|
|
1103
|
+
if (!confirmed) return;
|
|
1104
|
+
const formData = new FormData();
|
|
1105
|
+
formData.set("_action", "delete");
|
|
1106
|
+
formData.set("_table", tableName);
|
|
1107
|
+
formData.set("_id", id);
|
|
1108
|
+
submit(formData, { method: "post" });
|
|
1109
|
+
}
|
|
1110
|
+
return /* @__PURE__ */ jsxs4(Box3, { children: [
|
|
1111
|
+
/* @__PURE__ */ jsxs4(Box3, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 2 }, children: [
|
|
1112
|
+
/* @__PURE__ */ jsx4(Typography3, { level: "h2", children: tableLabel }),
|
|
1113
|
+
/* @__PURE__ */ jsx4(
|
|
1114
|
+
Button,
|
|
1115
|
+
{
|
|
1116
|
+
component: Link3,
|
|
1117
|
+
to: buildAdminUrl({ kind: "create", table: tableName }),
|
|
1118
|
+
children: "Create"
|
|
1119
|
+
}
|
|
1120
|
+
)
|
|
1121
|
+
] }),
|
|
1122
|
+
/* @__PURE__ */ jsx4(ActionResultDisplay, { result: actionResult }),
|
|
1123
|
+
searchable.length > 0 && /* @__PURE__ */ jsx4(Box3, { component: "form", onSubmit: handleSearchSubmit, sx: { mb: 2 }, children: /* @__PURE__ */ jsx4(
|
|
1124
|
+
Input,
|
|
1125
|
+
{
|
|
1126
|
+
placeholder: `Search ${tableLabel.toLowerCase()}...`,
|
|
1127
|
+
value: searchValue,
|
|
1128
|
+
onChange: (e) => setSearchValue(e.target.value),
|
|
1129
|
+
endDecorator: /* @__PURE__ */ jsx4(Button, { type: "submit", variant: "soft", size: "sm", children: "Search" })
|
|
1130
|
+
}
|
|
1131
|
+
) }),
|
|
1132
|
+
/* @__PURE__ */ jsx4(Sheet3, { variant: "outlined", sx: { borderRadius: "sm", overflow: "auto" }, children: /* @__PURE__ */ jsxs4(Table2, { hoverRow: true, children: [
|
|
1133
|
+
/* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
|
|
1134
|
+
listColumns.map((col) => /* @__PURE__ */ jsx4("th", { children: /* @__PURE__ */ jsxs4(
|
|
1135
|
+
Typography3,
|
|
1136
|
+
{
|
|
1137
|
+
component: Link3,
|
|
1138
|
+
to: sortUrl(col.name),
|
|
1139
|
+
level: "body-sm",
|
|
1140
|
+
fontWeight: "lg",
|
|
1141
|
+
sx: { textDecoration: "none", color: "inherit" },
|
|
1142
|
+
children: [
|
|
1143
|
+
col.label,
|
|
1144
|
+
sort.column === col.name ? sort.direction === "asc" ? " \u2191" : " \u2193" : ""
|
|
1145
|
+
]
|
|
1146
|
+
}
|
|
1147
|
+
) }, col.name)),
|
|
1148
|
+
/* @__PURE__ */ jsx4("th", { style: { width: 120 }, children: "Actions" })
|
|
1149
|
+
] }) }),
|
|
1150
|
+
/* @__PURE__ */ jsxs4("tbody", { children: [
|
|
1151
|
+
items.map((item) => {
|
|
1152
|
+
const id = String(item[primaryKey] ?? "");
|
|
1153
|
+
return /* @__PURE__ */ jsxs4("tr", { children: [
|
|
1154
|
+
listColumns.map((col) => /* @__PURE__ */ jsx4("td", { children: formatCellValue2(item[col.name]) }, col.name)),
|
|
1155
|
+
/* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsxs4(Stack, { direction: "row", spacing: 1, children: [
|
|
1156
|
+
/* @__PURE__ */ jsx4(
|
|
1157
|
+
Button,
|
|
1158
|
+
{
|
|
1159
|
+
component: Link3,
|
|
1160
|
+
to: buildAdminUrl({ kind: "detail", table: tableName, id }),
|
|
1161
|
+
size: "sm",
|
|
1162
|
+
variant: "plain",
|
|
1163
|
+
children: "View"
|
|
1164
|
+
}
|
|
1165
|
+
),
|
|
1166
|
+
/* @__PURE__ */ jsx4(
|
|
1167
|
+
Button,
|
|
1168
|
+
{
|
|
1169
|
+
size: "sm",
|
|
1170
|
+
variant: "plain",
|
|
1171
|
+
color: "danger",
|
|
1172
|
+
onClick: () => {
|
|
1173
|
+
void handleDelete2(id);
|
|
1174
|
+
},
|
|
1175
|
+
children: "Delete"
|
|
1176
|
+
}
|
|
1177
|
+
)
|
|
1178
|
+
] }) })
|
|
1179
|
+
] }, id);
|
|
1180
|
+
}),
|
|
1181
|
+
items.length === 0 && /* @__PURE__ */ jsx4("tr", { children: /* @__PURE__ */ jsx4("td", { colSpan: listColumns.length + 1, children: /* @__PURE__ */ jsx4(Typography3, { level: "body-sm", textAlign: "center", sx: { py: 2 }, children: "No records found." }) }) })
|
|
1182
|
+
] })
|
|
1183
|
+
] }) }),
|
|
1184
|
+
totalPages > 1 && /* @__PURE__ */ jsxs4(Stack, { direction: "row", spacing: 1, justifyContent: "center", alignItems: "center", sx: { mt: 2 }, children: [
|
|
1185
|
+
page > 1 && /* @__PURE__ */ jsx4(
|
|
1186
|
+
Button,
|
|
1187
|
+
{
|
|
1188
|
+
component: Link3,
|
|
1189
|
+
to: buildAdminUrl({
|
|
1190
|
+
kind: "list",
|
|
1191
|
+
table: tableName,
|
|
1192
|
+
page: page - 1,
|
|
1193
|
+
sort: sort.column,
|
|
1194
|
+
direction: sort.direction,
|
|
1195
|
+
search
|
|
1196
|
+
}),
|
|
1197
|
+
size: "sm",
|
|
1198
|
+
variant: "outlined",
|
|
1199
|
+
children: "Previous"
|
|
1200
|
+
}
|
|
1201
|
+
),
|
|
1202
|
+
/* @__PURE__ */ jsxs4(Typography3, { level: "body-sm", children: [
|
|
1203
|
+
"Page ",
|
|
1204
|
+
page,
|
|
1205
|
+
" of ",
|
|
1206
|
+
totalPages,
|
|
1207
|
+
" (",
|
|
1208
|
+
total,
|
|
1209
|
+
" total)"
|
|
1210
|
+
] }),
|
|
1211
|
+
page < totalPages && /* @__PURE__ */ jsx4(
|
|
1212
|
+
Button,
|
|
1213
|
+
{
|
|
1214
|
+
component: Link3,
|
|
1215
|
+
to: buildAdminUrl({
|
|
1216
|
+
kind: "list",
|
|
1217
|
+
table: tableName,
|
|
1218
|
+
page: page + 1,
|
|
1219
|
+
sort: sort.column,
|
|
1220
|
+
direction: sort.direction,
|
|
1221
|
+
search
|
|
1222
|
+
}),
|
|
1223
|
+
size: "sm",
|
|
1224
|
+
variant: "outlined",
|
|
1225
|
+
children: "Next"
|
|
1226
|
+
}
|
|
1227
|
+
)
|
|
1228
|
+
] })
|
|
1229
|
+
] });
|
|
1230
|
+
}
|
|
1231
|
+
function formatCellValue2(value) {
|
|
1232
|
+
if (value === null || value === void 0) return "-";
|
|
1233
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
1234
|
+
return String(value);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/components/table-detail.tsx
|
|
1238
|
+
import "react";
|
|
1239
|
+
import { Link as Link4, useSubmit as useSubmit2 } from "react-router";
|
|
1240
|
+
import Box4 from "@mui/joy/Box";
|
|
1241
|
+
import Typography4 from "@mui/joy/Typography";
|
|
1242
|
+
import Button2 from "@mui/joy/Button";
|
|
1243
|
+
import Card2 from "@mui/joy/Card";
|
|
1244
|
+
import Stack2 from "@mui/joy/Stack";
|
|
1245
|
+
import { useConfirm as useConfirm2 } from "@cfast/ui";
|
|
1246
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1247
|
+
function TableDetail({
|
|
1248
|
+
tableName,
|
|
1249
|
+
tableLabel,
|
|
1250
|
+
item,
|
|
1251
|
+
columns,
|
|
1252
|
+
primaryKey
|
|
1253
|
+
}) {
|
|
1254
|
+
const submit = useSubmit2();
|
|
1255
|
+
const confirm = useConfirm2();
|
|
1256
|
+
const id = String(item[primaryKey] ?? "");
|
|
1257
|
+
async function handleDelete2() {
|
|
1258
|
+
const confirmed = await confirm({
|
|
1259
|
+
title: "Delete record",
|
|
1260
|
+
description: `Are you sure you want to delete this ${tableLabel} record?`,
|
|
1261
|
+
variant: "danger",
|
|
1262
|
+
confirmLabel: "Delete"
|
|
1263
|
+
});
|
|
1264
|
+
if (!confirmed) return;
|
|
1265
|
+
const formData = new FormData();
|
|
1266
|
+
formData.set("_action", "delete");
|
|
1267
|
+
formData.set("_table", tableName);
|
|
1268
|
+
formData.set("_id", id);
|
|
1269
|
+
submit(formData, { method: "post" });
|
|
1270
|
+
}
|
|
1271
|
+
return /* @__PURE__ */ jsxs5(Box4, { children: [
|
|
1272
|
+
/* @__PURE__ */ jsxs5(Stack2, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 2 }, children: [
|
|
1273
|
+
/* @__PURE__ */ jsx5(
|
|
1274
|
+
Typography4,
|
|
1275
|
+
{
|
|
1276
|
+
component: Link4,
|
|
1277
|
+
to: buildAdminUrl({
|
|
1278
|
+
kind: "list",
|
|
1279
|
+
table: tableName,
|
|
1280
|
+
page: 1,
|
|
1281
|
+
sort: null,
|
|
1282
|
+
direction: "desc",
|
|
1283
|
+
search: ""
|
|
1284
|
+
}),
|
|
1285
|
+
level: "body-sm",
|
|
1286
|
+
children: tableLabel
|
|
1287
|
+
}
|
|
1288
|
+
),
|
|
1289
|
+
/* @__PURE__ */ jsx5(Typography4, { level: "body-sm", children: "/" }),
|
|
1290
|
+
/* @__PURE__ */ jsx5(Typography4, { level: "body-sm", children: id })
|
|
1291
|
+
] }),
|
|
1292
|
+
/* @__PURE__ */ jsxs5(Box4, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 3 }, children: [
|
|
1293
|
+
/* @__PURE__ */ jsxs5(Typography4, { level: "h2", children: [
|
|
1294
|
+
tableLabel,
|
|
1295
|
+
" Detail"
|
|
1296
|
+
] }),
|
|
1297
|
+
/* @__PURE__ */ jsxs5(Stack2, { direction: "row", spacing: 1, children: [
|
|
1298
|
+
/* @__PURE__ */ jsx5(
|
|
1299
|
+
Button2,
|
|
1300
|
+
{
|
|
1301
|
+
component: Link4,
|
|
1302
|
+
to: buildAdminUrl({ kind: "edit", table: tableName, id }),
|
|
1303
|
+
variant: "outlined",
|
|
1304
|
+
children: "Edit"
|
|
1305
|
+
}
|
|
1306
|
+
),
|
|
1307
|
+
/* @__PURE__ */ jsx5(
|
|
1308
|
+
Button2,
|
|
1309
|
+
{
|
|
1310
|
+
variant: "outlined",
|
|
1311
|
+
color: "danger",
|
|
1312
|
+
onClick: () => {
|
|
1313
|
+
void handleDelete2();
|
|
1314
|
+
},
|
|
1315
|
+
children: "Delete"
|
|
1316
|
+
}
|
|
1317
|
+
)
|
|
1318
|
+
] })
|
|
1319
|
+
] }),
|
|
1320
|
+
/* @__PURE__ */ jsx5(Card2, { variant: "outlined", children: /* @__PURE__ */ jsx5(
|
|
1321
|
+
Box4,
|
|
1322
|
+
{
|
|
1323
|
+
sx: {
|
|
1324
|
+
display: "grid",
|
|
1325
|
+
gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" },
|
|
1326
|
+
gap: 2
|
|
1327
|
+
},
|
|
1328
|
+
children: columns.map((col) => /* @__PURE__ */ jsxs5(Box4, { children: [
|
|
1329
|
+
/* @__PURE__ */ jsx5(Typography4, { level: "body-xs", textColor: "text.secondary", fontWeight: "lg", children: col.label }),
|
|
1330
|
+
/* @__PURE__ */ jsx5(Typography4, { level: "body-md", children: formatFieldValue(item[col.name]) })
|
|
1331
|
+
] }, col.name))
|
|
1332
|
+
}
|
|
1333
|
+
) })
|
|
1334
|
+
] });
|
|
1335
|
+
}
|
|
1336
|
+
function formatFieldValue(value) {
|
|
1337
|
+
if (value === null || value === void 0) return "-";
|
|
1338
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
1339
|
+
if (value instanceof Date) return value.toLocaleString();
|
|
1340
|
+
return String(value);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/components/table-form.tsx
|
|
1344
|
+
import "react";
|
|
1345
|
+
import { Link as Link5, useSubmit as useSubmit3 } from "react-router";
|
|
1346
|
+
import Box5 from "@mui/joy/Box";
|
|
1347
|
+
import Typography5 from "@mui/joy/Typography";
|
|
1348
|
+
import Stack3 from "@mui/joy/Stack";
|
|
1349
|
+
import { AutoForm } from "@cfast/forms/joy";
|
|
1350
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1351
|
+
var AUTO_TIMESTAMP_NAMES = /* @__PURE__ */ new Set([
|
|
1352
|
+
"created_at",
|
|
1353
|
+
"createdAt",
|
|
1354
|
+
"updated_at",
|
|
1355
|
+
"updatedAt"
|
|
1356
|
+
]);
|
|
1357
|
+
function TableForm({
|
|
1358
|
+
tableName,
|
|
1359
|
+
tableLabel,
|
|
1360
|
+
mode,
|
|
1361
|
+
drizzleTable,
|
|
1362
|
+
item,
|
|
1363
|
+
columns,
|
|
1364
|
+
primaryKey,
|
|
1365
|
+
actionResult
|
|
1366
|
+
}) {
|
|
1367
|
+
const submit = useSubmit3();
|
|
1368
|
+
const id = item ? String(item[primaryKey] ?? "") : void 0;
|
|
1369
|
+
const excludeFields = columns.filter((c) => c.isPrimaryKey || AUTO_TIMESTAMP_NAMES.has(c.name)).map((c) => c.name);
|
|
1370
|
+
function handleSubmit(values) {
|
|
1371
|
+
const formData = new FormData();
|
|
1372
|
+
formData.set("_action", mode === "create" ? "create" : "update");
|
|
1373
|
+
formData.set("_table", tableName);
|
|
1374
|
+
if (id) {
|
|
1375
|
+
formData.set("_id", id);
|
|
1376
|
+
}
|
|
1377
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1378
|
+
if (value !== null && value !== void 0) {
|
|
1379
|
+
formData.set(key, String(value));
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
submit(formData, { method: "post" });
|
|
1383
|
+
}
|
|
1384
|
+
const listUrl = buildAdminUrl({
|
|
1385
|
+
kind: "list",
|
|
1386
|
+
table: tableName,
|
|
1387
|
+
page: 1,
|
|
1388
|
+
sort: null,
|
|
1389
|
+
direction: "desc",
|
|
1390
|
+
search: ""
|
|
1391
|
+
});
|
|
1392
|
+
return /* @__PURE__ */ jsxs6(Box5, { children: [
|
|
1393
|
+
/* @__PURE__ */ jsxs6(Stack3, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 2 }, children: [
|
|
1394
|
+
/* @__PURE__ */ jsx6(Typography5, { component: Link5, to: listUrl, level: "body-sm", children: tableLabel }),
|
|
1395
|
+
/* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "/" }),
|
|
1396
|
+
mode === "edit" && id ? /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
1397
|
+
/* @__PURE__ */ jsx6(
|
|
1398
|
+
Typography5,
|
|
1399
|
+
{
|
|
1400
|
+
component: Link5,
|
|
1401
|
+
to: buildAdminUrl({ kind: "detail", table: tableName, id }),
|
|
1402
|
+
level: "body-sm",
|
|
1403
|
+
children: id
|
|
1404
|
+
}
|
|
1405
|
+
),
|
|
1406
|
+
/* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "/" }),
|
|
1407
|
+
/* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "Edit" })
|
|
1408
|
+
] }) : /* @__PURE__ */ jsx6(Typography5, { level: "body-sm", children: "Create" })
|
|
1409
|
+
] }),
|
|
1410
|
+
/* @__PURE__ */ jsx6(Typography5, { level: "h2", sx: { mb: 3 }, children: mode === "create" ? `Create ${tableLabel}` : `Edit ${tableLabel}` }),
|
|
1411
|
+
/* @__PURE__ */ jsx6(ActionResultDisplay, { result: actionResult }),
|
|
1412
|
+
/* @__PURE__ */ jsx6(
|
|
1413
|
+
AutoForm,
|
|
1414
|
+
{
|
|
1415
|
+
table: drizzleTable,
|
|
1416
|
+
mode,
|
|
1417
|
+
data: item,
|
|
1418
|
+
onSubmit: handleSubmit,
|
|
1419
|
+
exclude: excludeFields
|
|
1420
|
+
}
|
|
1421
|
+
)
|
|
1422
|
+
] });
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// src/components/user-list.tsx
|
|
1426
|
+
import { useState as useState2 } from "react";
|
|
1427
|
+
import { Link as Link6, useSubmit as useSubmit4, useNavigate as useNavigate2 } from "react-router";
|
|
1428
|
+
import Box6 from "@mui/joy/Box";
|
|
1429
|
+
import Typography6 from "@mui/joy/Typography";
|
|
1430
|
+
import Button3 from "@mui/joy/Button";
|
|
1431
|
+
import Input2 from "@mui/joy/Input";
|
|
1432
|
+
import Table3 from "@mui/joy/Table";
|
|
1433
|
+
import Sheet4 from "@mui/joy/Sheet";
|
|
1434
|
+
import Stack4 from "@mui/joy/Stack";
|
|
1435
|
+
import { RoleBadge } from "@cfast/ui/joy";
|
|
1436
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1437
|
+
function UserList({
|
|
1438
|
+
items,
|
|
1439
|
+
total,
|
|
1440
|
+
page,
|
|
1441
|
+
totalPages,
|
|
1442
|
+
search,
|
|
1443
|
+
currentUser,
|
|
1444
|
+
actionResult
|
|
1445
|
+
}) {
|
|
1446
|
+
const submit = useSubmit4();
|
|
1447
|
+
const navigate = useNavigate2();
|
|
1448
|
+
const [searchValue, setSearchValue] = useState2(search);
|
|
1449
|
+
function handleSearchSubmit(e) {
|
|
1450
|
+
e.preventDefault();
|
|
1451
|
+
const url = buildAdminUrl({
|
|
1452
|
+
kind: "user-list",
|
|
1453
|
+
page: 1,
|
|
1454
|
+
search: searchValue
|
|
1455
|
+
});
|
|
1456
|
+
navigate({ search: url.startsWith("?") ? url : "" });
|
|
1457
|
+
}
|
|
1458
|
+
function handleImpersonate2(userId) {
|
|
1459
|
+
const formData = new FormData();
|
|
1460
|
+
formData.set("_action", "impersonate");
|
|
1461
|
+
formData.set("_id", userId);
|
|
1462
|
+
submit(formData, { method: "post" });
|
|
1463
|
+
}
|
|
1464
|
+
return /* @__PURE__ */ jsxs7(Box6, { children: [
|
|
1465
|
+
/* @__PURE__ */ jsx7(Typography6, { level: "h2", sx: { mb: 2 }, children: "Users" }),
|
|
1466
|
+
/* @__PURE__ */ jsx7(ActionResultDisplay, { result: actionResult }),
|
|
1467
|
+
/* @__PURE__ */ jsx7(Box6, { component: "form", onSubmit: handleSearchSubmit, sx: { mb: 2 }, children: /* @__PURE__ */ jsx7(
|
|
1468
|
+
Input2,
|
|
1469
|
+
{
|
|
1470
|
+
placeholder: "Search users...",
|
|
1471
|
+
value: searchValue,
|
|
1472
|
+
onChange: (e) => setSearchValue(e.target.value),
|
|
1473
|
+
endDecorator: /* @__PURE__ */ jsx7(Button3, { type: "submit", variant: "soft", size: "sm", children: "Search" })
|
|
1474
|
+
}
|
|
1475
|
+
) }),
|
|
1476
|
+
/* @__PURE__ */ jsx7(Sheet4, { variant: "outlined", sx: { borderRadius: "sm", overflow: "auto" }, children: /* @__PURE__ */ jsxs7(Table3, { hoverRow: true, children: [
|
|
1477
|
+
/* @__PURE__ */ jsx7("thead", { children: /* @__PURE__ */ jsxs7("tr", { children: [
|
|
1478
|
+
/* @__PURE__ */ jsx7("th", { children: "Name" }),
|
|
1479
|
+
/* @__PURE__ */ jsx7("th", { children: "Email" }),
|
|
1480
|
+
/* @__PURE__ */ jsx7("th", { children: "Roles" }),
|
|
1481
|
+
/* @__PURE__ */ jsx7("th", { children: "Joined" }),
|
|
1482
|
+
/* @__PURE__ */ jsx7("th", { style: { width: 160 }, children: "Actions" })
|
|
1483
|
+
] }) }),
|
|
1484
|
+
/* @__PURE__ */ jsxs7("tbody", { children: [
|
|
1485
|
+
items.map((user) => /* @__PURE__ */ jsxs7("tr", { children: [
|
|
1486
|
+
/* @__PURE__ */ jsx7("td", { children: user.name }),
|
|
1487
|
+
/* @__PURE__ */ jsx7("td", { children: user.email }),
|
|
1488
|
+
/* @__PURE__ */ jsx7("td", { children: /* @__PURE__ */ jsxs7(Stack4, { direction: "row", spacing: 0.5, flexWrap: "wrap", children: [
|
|
1489
|
+
user.roles.map((role) => /* @__PURE__ */ jsx7(RoleBadge, { role }, role)),
|
|
1490
|
+
user.roles.length === 0 && /* @__PURE__ */ jsx7(Typography6, { level: "body-xs", textColor: "text.tertiary", children: "No roles" })
|
|
1491
|
+
] }) }),
|
|
1492
|
+
/* @__PURE__ */ jsx7("td", { children: user.createdAt ?? "-" }),
|
|
1493
|
+
/* @__PURE__ */ jsx7("td", { children: /* @__PURE__ */ jsxs7(Stack4, { direction: "row", spacing: 1, children: [
|
|
1494
|
+
/* @__PURE__ */ jsx7(
|
|
1495
|
+
Button3,
|
|
1496
|
+
{
|
|
1497
|
+
component: Link6,
|
|
1498
|
+
to: buildAdminUrl({ kind: "user-detail", id: user.id }),
|
|
1499
|
+
size: "sm",
|
|
1500
|
+
variant: "plain",
|
|
1501
|
+
children: "View"
|
|
1502
|
+
}
|
|
1503
|
+
),
|
|
1504
|
+
user.id !== currentUser.id && /* @__PURE__ */ jsx7(
|
|
1505
|
+
Button3,
|
|
1506
|
+
{
|
|
1507
|
+
size: "sm",
|
|
1508
|
+
variant: "plain",
|
|
1509
|
+
color: "warning",
|
|
1510
|
+
onClick: () => handleImpersonate2(user.id),
|
|
1511
|
+
children: "Impersonate"
|
|
1512
|
+
}
|
|
1513
|
+
)
|
|
1514
|
+
] }) })
|
|
1515
|
+
] }, user.id)),
|
|
1516
|
+
items.length === 0 && /* @__PURE__ */ jsx7("tr", { children: /* @__PURE__ */ jsx7("td", { colSpan: 5, children: /* @__PURE__ */ jsx7(Typography6, { level: "body-sm", textAlign: "center", sx: { py: 2 }, children: "No users found." }) }) })
|
|
1517
|
+
] })
|
|
1518
|
+
] }) }),
|
|
1519
|
+
totalPages > 1 && /* @__PURE__ */ jsxs7(Stack4, { direction: "row", spacing: 1, justifyContent: "center", alignItems: "center", sx: { mt: 2 }, children: [
|
|
1520
|
+
page > 1 && /* @__PURE__ */ jsx7(
|
|
1521
|
+
Button3,
|
|
1522
|
+
{
|
|
1523
|
+
component: Link6,
|
|
1524
|
+
to: buildAdminUrl({
|
|
1525
|
+
kind: "user-list",
|
|
1526
|
+
page: page - 1,
|
|
1527
|
+
search
|
|
1528
|
+
}),
|
|
1529
|
+
size: "sm",
|
|
1530
|
+
variant: "outlined",
|
|
1531
|
+
children: "Previous"
|
|
1532
|
+
}
|
|
1533
|
+
),
|
|
1534
|
+
/* @__PURE__ */ jsxs7(Typography6, { level: "body-sm", children: [
|
|
1535
|
+
"Page ",
|
|
1536
|
+
page,
|
|
1537
|
+
" of ",
|
|
1538
|
+
totalPages,
|
|
1539
|
+
" (",
|
|
1540
|
+
total,
|
|
1541
|
+
" total)"
|
|
1542
|
+
] }),
|
|
1543
|
+
page < totalPages && /* @__PURE__ */ jsx7(
|
|
1544
|
+
Button3,
|
|
1545
|
+
{
|
|
1546
|
+
component: Link6,
|
|
1547
|
+
to: buildAdminUrl({
|
|
1548
|
+
kind: "user-list",
|
|
1549
|
+
page: page + 1,
|
|
1550
|
+
search
|
|
1551
|
+
}),
|
|
1552
|
+
size: "sm",
|
|
1553
|
+
variant: "outlined",
|
|
1554
|
+
children: "Next"
|
|
1555
|
+
}
|
|
1556
|
+
)
|
|
1557
|
+
] })
|
|
1558
|
+
] });
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/components/user-detail.tsx
|
|
1562
|
+
import { useState as useState3 } from "react";
|
|
1563
|
+
import { Link as Link7, useSubmit as useSubmit5 } from "react-router";
|
|
1564
|
+
import Box7 from "@mui/joy/Box";
|
|
1565
|
+
import Typography7 from "@mui/joy/Typography";
|
|
1566
|
+
import Button4 from "@mui/joy/Button";
|
|
1567
|
+
import Card3 from "@mui/joy/Card";
|
|
1568
|
+
import Stack5 from "@mui/joy/Stack";
|
|
1569
|
+
import Chip2 from "@mui/joy/Chip";
|
|
1570
|
+
import Select from "@mui/joy/Select";
|
|
1571
|
+
import Option from "@mui/joy/Option";
|
|
1572
|
+
import Divider2 from "@mui/joy/Divider";
|
|
1573
|
+
import { RoleBadge as RoleBadge2, AvatarWithInitials } from "@cfast/ui/joy";
|
|
1574
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1575
|
+
function UserDetail({
|
|
1576
|
+
targetUser,
|
|
1577
|
+
assignableRoles,
|
|
1578
|
+
currentUser,
|
|
1579
|
+
actionResult
|
|
1580
|
+
}) {
|
|
1581
|
+
const submit = useSubmit5();
|
|
1582
|
+
const [selectedRole, setSelectedRole] = useState3(null);
|
|
1583
|
+
const availableRoles = assignableRoles.filter(
|
|
1584
|
+
(role) => !targetUser.roles.includes(role)
|
|
1585
|
+
);
|
|
1586
|
+
function handleAddRole() {
|
|
1587
|
+
if (!selectedRole) return;
|
|
1588
|
+
const formData = new FormData();
|
|
1589
|
+
formData.set("_action", "setRole");
|
|
1590
|
+
formData.set("_id", targetUser.id);
|
|
1591
|
+
formData.set("role", selectedRole);
|
|
1592
|
+
submit(formData, { method: "post" });
|
|
1593
|
+
setSelectedRole(null);
|
|
1594
|
+
}
|
|
1595
|
+
function handleRemoveRole2(role) {
|
|
1596
|
+
const formData = new FormData();
|
|
1597
|
+
formData.set("_action", "removeRole");
|
|
1598
|
+
formData.set("_id", targetUser.id);
|
|
1599
|
+
formData.set("role", role);
|
|
1600
|
+
submit(formData, { method: "post" });
|
|
1601
|
+
}
|
|
1602
|
+
function handleImpersonate2() {
|
|
1603
|
+
const formData = new FormData();
|
|
1604
|
+
formData.set("_action", "impersonate");
|
|
1605
|
+
formData.set("_id", targetUser.id);
|
|
1606
|
+
submit(formData, { method: "post" });
|
|
1607
|
+
}
|
|
1608
|
+
return /* @__PURE__ */ jsxs8(Box7, { children: [
|
|
1609
|
+
/* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 2 }, children: [
|
|
1610
|
+
/* @__PURE__ */ jsx8(
|
|
1611
|
+
Typography7,
|
|
1612
|
+
{
|
|
1613
|
+
component: Link7,
|
|
1614
|
+
to: buildAdminUrl({ kind: "user-list", page: 1, search: "" }),
|
|
1615
|
+
level: "body-sm",
|
|
1616
|
+
children: "Users"
|
|
1617
|
+
}
|
|
1618
|
+
),
|
|
1619
|
+
/* @__PURE__ */ jsx8(Typography7, { level: "body-sm", children: "/" }),
|
|
1620
|
+
/* @__PURE__ */ jsx8(Typography7, { level: "body-sm", children: targetUser.name })
|
|
1621
|
+
] }),
|
|
1622
|
+
/* @__PURE__ */ jsx8(ActionResultDisplay, { result: actionResult }),
|
|
1623
|
+
/* @__PURE__ */ jsx8(Card3, { variant: "outlined", sx: { mb: 3 }, children: /* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 2, alignItems: "center", children: [
|
|
1624
|
+
/* @__PURE__ */ jsx8(
|
|
1625
|
+
AvatarWithInitials,
|
|
1626
|
+
{
|
|
1627
|
+
src: targetUser.avatarUrl,
|
|
1628
|
+
name: targetUser.name,
|
|
1629
|
+
size: "lg"
|
|
1630
|
+
}
|
|
1631
|
+
),
|
|
1632
|
+
/* @__PURE__ */ jsxs8(Box7, { children: [
|
|
1633
|
+
/* @__PURE__ */ jsx8(Typography7, { level: "h3", children: targetUser.name }),
|
|
1634
|
+
/* @__PURE__ */ jsx8(Typography7, { level: "body-md", textColor: "text.secondary", children: targetUser.email }),
|
|
1635
|
+
targetUser.createdAt && /* @__PURE__ */ jsxs8(Typography7, { level: "body-xs", textColor: "text.tertiary", children: [
|
|
1636
|
+
"Joined ",
|
|
1637
|
+
targetUser.createdAt
|
|
1638
|
+
] })
|
|
1639
|
+
] }),
|
|
1640
|
+
targetUser.id !== currentUser.id && /* @__PURE__ */ jsx8(Box7, { sx: { ml: "auto" }, children: /* @__PURE__ */ jsx8(
|
|
1641
|
+
Button4,
|
|
1642
|
+
{
|
|
1643
|
+
variant: "outlined",
|
|
1644
|
+
color: "warning",
|
|
1645
|
+
onClick: handleImpersonate2,
|
|
1646
|
+
children: "Impersonate"
|
|
1647
|
+
}
|
|
1648
|
+
) })
|
|
1649
|
+
] }) }),
|
|
1650
|
+
/* @__PURE__ */ jsxs8(Card3, { variant: "outlined", children: [
|
|
1651
|
+
/* @__PURE__ */ jsx8(Typography7, { level: "title-lg", sx: { mb: 2 }, children: "Roles" }),
|
|
1652
|
+
/* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 1, flexWrap: "wrap", sx: { mb: 2 }, children: [
|
|
1653
|
+
targetUser.roles.map((role) => /* @__PURE__ */ jsx8(
|
|
1654
|
+
Chip2,
|
|
1655
|
+
{
|
|
1656
|
+
variant: "soft",
|
|
1657
|
+
endDecorator: /* @__PURE__ */ jsx8(
|
|
1658
|
+
Button4,
|
|
1659
|
+
{
|
|
1660
|
+
size: "sm",
|
|
1661
|
+
variant: "plain",
|
|
1662
|
+
color: "danger",
|
|
1663
|
+
sx: { minWidth: 0, px: 0.5 },
|
|
1664
|
+
onClick: () => handleRemoveRole2(role),
|
|
1665
|
+
children: "x"
|
|
1666
|
+
}
|
|
1667
|
+
),
|
|
1668
|
+
children: /* @__PURE__ */ jsx8(RoleBadge2, { role })
|
|
1669
|
+
},
|
|
1670
|
+
role
|
|
1671
|
+
)),
|
|
1672
|
+
targetUser.roles.length === 0 && /* @__PURE__ */ jsx8(Typography7, { level: "body-sm", textColor: "text.tertiary", children: "No roles assigned." })
|
|
1673
|
+
] }),
|
|
1674
|
+
availableRoles.length > 0 && /* @__PURE__ */ jsxs8(Fragment2, { children: [
|
|
1675
|
+
/* @__PURE__ */ jsx8(Divider2, { sx: { my: 2 } }),
|
|
1676
|
+
/* @__PURE__ */ jsxs8(Stack5, { direction: "row", spacing: 1, alignItems: "center", children: [
|
|
1677
|
+
/* @__PURE__ */ jsx8(
|
|
1678
|
+
Select,
|
|
1679
|
+
{
|
|
1680
|
+
placeholder: "Select role...",
|
|
1681
|
+
value: selectedRole,
|
|
1682
|
+
onChange: (_e, value) => setSelectedRole(value),
|
|
1683
|
+
sx: { minWidth: 200 },
|
|
1684
|
+
children: availableRoles.map((role) => /* @__PURE__ */ jsx8(Option, { value: role, children: role }, role))
|
|
1685
|
+
}
|
|
1686
|
+
),
|
|
1687
|
+
/* @__PURE__ */ jsx8(
|
|
1688
|
+
Button4,
|
|
1689
|
+
{
|
|
1690
|
+
onClick: handleAddRole,
|
|
1691
|
+
disabled: !selectedRole,
|
|
1692
|
+
size: "sm",
|
|
1693
|
+
children: "Add Role"
|
|
1694
|
+
}
|
|
1695
|
+
)
|
|
1696
|
+
] })
|
|
1697
|
+
] })
|
|
1698
|
+
] })
|
|
1699
|
+
] });
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/components/admin-root.tsx
|
|
1703
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1704
|
+
function createAdminComponent(tableMetas) {
|
|
1705
|
+
const tableMetaMap = new Map(tableMetas.map((m) => [m.name, m]));
|
|
1706
|
+
function AdminRoot() {
|
|
1707
|
+
const data = useLoaderData();
|
|
1708
|
+
const actionData = useActionData();
|
|
1709
|
+
return /* @__PURE__ */ jsxs9(ConfirmProvider, { children: [
|
|
1710
|
+
data.user.isImpersonating && /* @__PURE__ */ jsx9(ImpersonationBar, { user: data.user }),
|
|
1711
|
+
/* @__PURE__ */ jsxs9(Box8, { sx: { display: "flex", minHeight: "100vh" }, children: [
|
|
1712
|
+
/* @__PURE__ */ jsx9(Sidebar, { tables: data.tables }),
|
|
1713
|
+
/* @__PURE__ */ jsx9(Box8, { sx: { flex: 1, p: 3, overflow: "auto" }, children: /* @__PURE__ */ jsx9(
|
|
1714
|
+
AdminContent,
|
|
1715
|
+
{
|
|
1716
|
+
data,
|
|
1717
|
+
actionResult: actionData,
|
|
1718
|
+
tableMetaMap
|
|
1719
|
+
}
|
|
1720
|
+
) })
|
|
1721
|
+
] })
|
|
1722
|
+
] });
|
|
1723
|
+
}
|
|
1724
|
+
return AdminRoot;
|
|
1725
|
+
}
|
|
1726
|
+
function AdminContent({ data, actionResult, tableMetaMap }) {
|
|
1727
|
+
switch (data.view) {
|
|
1728
|
+
case "dashboard":
|
|
1729
|
+
return /* @__PURE__ */ jsx9(Dashboard, { stats: data.stats, recentItems: data.recentItems });
|
|
1730
|
+
case "list": {
|
|
1731
|
+
const meta = tableMetaMap.get(data.tableName);
|
|
1732
|
+
const primaryKey = meta?.primaryKey ?? "id";
|
|
1733
|
+
return /* @__PURE__ */ jsx9(
|
|
1734
|
+
TableList,
|
|
1735
|
+
{
|
|
1736
|
+
tableName: data.tableName,
|
|
1737
|
+
tableLabel: data.tableLabel,
|
|
1738
|
+
items: data.items,
|
|
1739
|
+
total: data.total,
|
|
1740
|
+
page: data.page,
|
|
1741
|
+
totalPages: data.totalPages,
|
|
1742
|
+
columns: data.columns,
|
|
1743
|
+
searchable: data.searchable,
|
|
1744
|
+
sort: data.sort,
|
|
1745
|
+
search: data.search,
|
|
1746
|
+
primaryKey,
|
|
1747
|
+
actionResult
|
|
1748
|
+
}
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
case "detail": {
|
|
1752
|
+
const meta = tableMetaMap.get(data.tableName);
|
|
1753
|
+
const primaryKey = meta?.primaryKey ?? "id";
|
|
1754
|
+
return /* @__PURE__ */ jsx9(
|
|
1755
|
+
TableDetail,
|
|
1756
|
+
{
|
|
1757
|
+
tableName: data.tableName,
|
|
1758
|
+
tableLabel: data.tableLabel,
|
|
1759
|
+
item: data.item,
|
|
1760
|
+
columns: data.columns,
|
|
1761
|
+
primaryKey
|
|
1762
|
+
}
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
case "create": {
|
|
1766
|
+
const meta = tableMetaMap.get(data.tableName);
|
|
1767
|
+
if (!meta) {
|
|
1768
|
+
return /* @__PURE__ */ jsx9(Box8, { sx: { p: 4 }, children: /* @__PURE__ */ jsxs9(Typography8, { color: "danger", children: [
|
|
1769
|
+
"Table \u201C",
|
|
1770
|
+
data.tableName,
|
|
1771
|
+
"\u201D not found in schema."
|
|
1772
|
+
] }) });
|
|
1773
|
+
}
|
|
1774
|
+
return /* @__PURE__ */ jsx9(
|
|
1775
|
+
TableForm,
|
|
1776
|
+
{
|
|
1777
|
+
tableName: data.tableName,
|
|
1778
|
+
tableLabel: data.tableLabel,
|
|
1779
|
+
mode: "create",
|
|
1780
|
+
drizzleTable: meta.drizzleTable,
|
|
1781
|
+
columns: data.columns,
|
|
1782
|
+
primaryKey: meta.primaryKey,
|
|
1783
|
+
actionResult
|
|
1784
|
+
}
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
case "edit": {
|
|
1788
|
+
const meta = tableMetaMap.get(data.tableName);
|
|
1789
|
+
if (!meta) {
|
|
1790
|
+
return /* @__PURE__ */ jsx9(Box8, { sx: { p: 4 }, children: /* @__PURE__ */ jsxs9(Typography8, { color: "danger", children: [
|
|
1791
|
+
"Table \u201C",
|
|
1792
|
+
data.tableName,
|
|
1793
|
+
"\u201D not found in schema."
|
|
1794
|
+
] }) });
|
|
1795
|
+
}
|
|
1796
|
+
return /* @__PURE__ */ jsx9(
|
|
1797
|
+
TableForm,
|
|
1798
|
+
{
|
|
1799
|
+
tableName: data.tableName,
|
|
1800
|
+
tableLabel: data.tableLabel,
|
|
1801
|
+
mode: "edit",
|
|
1802
|
+
drizzleTable: meta.drizzleTable,
|
|
1803
|
+
item: data.item,
|
|
1804
|
+
columns: data.columns,
|
|
1805
|
+
primaryKey: meta.primaryKey,
|
|
1806
|
+
actionResult
|
|
1807
|
+
}
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
case "users":
|
|
1811
|
+
return /* @__PURE__ */ jsx9(
|
|
1812
|
+
UserList,
|
|
1813
|
+
{
|
|
1814
|
+
items: data.items,
|
|
1815
|
+
total: data.total,
|
|
1816
|
+
page: data.page,
|
|
1817
|
+
totalPages: data.totalPages,
|
|
1818
|
+
search: data.search,
|
|
1819
|
+
currentUser: data.user,
|
|
1820
|
+
actionResult
|
|
1821
|
+
}
|
|
1822
|
+
);
|
|
1823
|
+
case "user-detail":
|
|
1824
|
+
return /* @__PURE__ */ jsx9(
|
|
1825
|
+
UserDetail,
|
|
1826
|
+
{
|
|
1827
|
+
targetUser: data.targetUser,
|
|
1828
|
+
assignableRoles: data.assignableRoles,
|
|
1829
|
+
currentUser: data.user,
|
|
1830
|
+
actionResult
|
|
1831
|
+
}
|
|
1832
|
+
);
|
|
1833
|
+
case "error":
|
|
1834
|
+
return /* @__PURE__ */ jsxs9(Box8, { sx: { p: 4, textAlign: "center" }, children: [
|
|
1835
|
+
/* @__PURE__ */ jsx9(Typography8, { level: "h3", color: "danger", children: "Error" }),
|
|
1836
|
+
/* @__PURE__ */ jsx9(Typography8, { level: "body-md", sx: { mt: 1 }, children: data.message })
|
|
1837
|
+
] });
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function ImpersonationBar({ user }) {
|
|
1841
|
+
const submit = useSubmit6();
|
|
1842
|
+
function handleStopImpersonation() {
|
|
1843
|
+
const formData = new FormData();
|
|
1844
|
+
formData.set("_action", "stopImpersonation");
|
|
1845
|
+
submit(formData, { method: "post" });
|
|
1846
|
+
}
|
|
1847
|
+
return /* @__PURE__ */ jsxs9(
|
|
1848
|
+
Box8,
|
|
1849
|
+
{
|
|
1850
|
+
sx: {
|
|
1851
|
+
bgcolor: "warning.softBg",
|
|
1852
|
+
color: "warning.softColor",
|
|
1853
|
+
py: 1,
|
|
1854
|
+
px: 3,
|
|
1855
|
+
display: "flex",
|
|
1856
|
+
alignItems: "center",
|
|
1857
|
+
justifyContent: "center",
|
|
1858
|
+
gap: 2
|
|
1859
|
+
},
|
|
1860
|
+
children: [
|
|
1861
|
+
/* @__PURE__ */ jsxs9(Typography8, { level: "body-sm", fontWeight: "lg", children: [
|
|
1862
|
+
"Impersonating ",
|
|
1863
|
+
user.name,
|
|
1864
|
+
" (",
|
|
1865
|
+
user.email,
|
|
1866
|
+
")",
|
|
1867
|
+
user.realUser && ` \u2014 logged in as ${user.realUser.name}`
|
|
1868
|
+
] }),
|
|
1869
|
+
/* @__PURE__ */ jsx9(
|
|
1870
|
+
Button5,
|
|
1871
|
+
{
|
|
1872
|
+
size: "sm",
|
|
1873
|
+
variant: "solid",
|
|
1874
|
+
color: "warning",
|
|
1875
|
+
onClick: handleStopImpersonation,
|
|
1876
|
+
children: "Stop Impersonation"
|
|
1877
|
+
}
|
|
1878
|
+
)
|
|
1879
|
+
]
|
|
1880
|
+
}
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// src/create-admin.ts
|
|
1885
|
+
function createAdmin(config) {
|
|
1886
|
+
const tableMetas = introspectSchema(config.schema, config.tables);
|
|
1887
|
+
const loader = createAdminLoader(config, tableMetas);
|
|
1888
|
+
const action = createAdminAction(config, tableMetas);
|
|
1889
|
+
const Component = createAdminComponent(tableMetas);
|
|
1890
|
+
return { loader, action, Component };
|
|
1891
|
+
}
|
|
1892
|
+
export {
|
|
1893
|
+
createAdmin,
|
|
1894
|
+
createAdminAction,
|
|
1895
|
+
createAdminComponent,
|
|
1896
|
+
createAdminLoader,
|
|
1897
|
+
introspectSchema
|
|
1898
|
+
};
|