@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,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CollectionDefinition, AccessConfig } from "../../core";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
|
|
6
|
+
interface TableEngineProps {
|
|
7
|
+
collection: CollectionDefinition;
|
|
8
|
+
data: unknown[];
|
|
9
|
+
basePath: string;
|
|
10
|
+
role?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasAccess(access: AccessConfig | undefined, operation: string, role?: string): boolean {
|
|
14
|
+
if (!access) return true;
|
|
15
|
+
const allowed = access[operation as keyof AccessConfig];
|
|
16
|
+
if (!allowed || !Array.isArray(allowed)) return true;
|
|
17
|
+
if (!role) return false;
|
|
18
|
+
return allowed.includes(role);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatRelativeTime(date: Date) {
|
|
22
|
+
const diff = Date.now() - date.getTime();
|
|
23
|
+
const minutes = Math.floor(diff / 60000);
|
|
24
|
+
const hours = Math.floor(minutes / 60);
|
|
25
|
+
const days = Math.floor(hours / 24);
|
|
26
|
+
|
|
27
|
+
if (isNaN(date.getTime())) return "Invalid date";
|
|
28
|
+
if (minutes < 1) return "Just now";
|
|
29
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
30
|
+
if (hours < 24) return `${hours}h ago`;
|
|
31
|
+
return `${days}d ago`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ActivityCell({ record }: { record: Record<string, unknown> }) {
|
|
35
|
+
const createdAtStr = (record.createdAt || record.created_at) as string;
|
|
36
|
+
const updatedAtStr = (record.updatedAt || record.updated_at) as string;
|
|
37
|
+
|
|
38
|
+
const createdAt = createdAtStr ? new Date(createdAtStr) : null;
|
|
39
|
+
const updatedAt = updatedAtStr ? new Date(updatedAtStr) : null;
|
|
40
|
+
|
|
41
|
+
const isUpdated = updatedAt && createdAt && updatedAt.getTime() > createdAt.getTime() + 1000;
|
|
42
|
+
const date = isUpdated ? updatedAt : createdAt;
|
|
43
|
+
const label = isUpdated ? "Updated" : "Created";
|
|
44
|
+
|
|
45
|
+
if (!date) return <span style={{ color: '#94a3b8', fontSize: '13px' }}>No activity</span>;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px', minWidth: '120px' }}>
|
|
49
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}>
|
|
50
|
+
<span
|
|
51
|
+
style={{
|
|
52
|
+
fontSize: '10px',
|
|
53
|
+
padding: '1px 6px',
|
|
54
|
+
borderRadius: '10px',
|
|
55
|
+
background: isUpdated ? '#fef3c7' : '#dcfce7',
|
|
56
|
+
color: isUpdated ? '#92400e' : '#166534',
|
|
57
|
+
fontWeight: 700,
|
|
58
|
+
textTransform: 'uppercase'
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{label}
|
|
62
|
+
</span>
|
|
63
|
+
<span style={{ fontWeight: 500, color: 'var(--color-text)' }}>{formatRelativeTime(date)}</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div style={{ fontSize: '11px', color: '#64748b' }}>
|
|
66
|
+
by <span style={{ color: '#0f172a', fontWeight: 600 }}>Admin</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function TableEngine({ collection, data, basePath, role }: TableEngineProps) {
|
|
73
|
+
const records = data as Record<string, unknown>[];
|
|
74
|
+
const fieldKeys = Object.keys(collection.fields);
|
|
75
|
+
const displayKeys = ["id", ...fieldKeys.slice(0, 4)]; // Show up to 4 fields + ID + Activity
|
|
76
|
+
const canDelete = hasAccess(collection.access, "delete", role);
|
|
77
|
+
const canUpdate = hasAccess(collection.access, "update", role);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="adminforge-table-wrapper">
|
|
81
|
+
<table className="adminforge-table">
|
|
82
|
+
<thead>
|
|
83
|
+
<tr>
|
|
84
|
+
{displayKeys.map((key) => <th key={key}>{key}</th>)}
|
|
85
|
+
<th>Activity</th>
|
|
86
|
+
<th style={{ textAlign: 'right', paddingRight: '20px' }}>Actions</th>
|
|
87
|
+
</tr>
|
|
88
|
+
</thead>
|
|
89
|
+
<tbody>
|
|
90
|
+
{records.map((record) => (
|
|
91
|
+
<tr key={record.id as string}>
|
|
92
|
+
{displayKeys.map((key) => (
|
|
93
|
+
<td key={key}>
|
|
94
|
+
{key === 'id' ? (
|
|
95
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
96
|
+
<span className="adminforge-id-badge" title={String(record[key])}>
|
|
97
|
+
{String(record[key]).substring(0, 8)}...
|
|
98
|
+
</span>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="adminforge-btn-icon"
|
|
102
|
+
style={{ width: '24px', height: '24px', minWidth: '24px' }}
|
|
103
|
+
title="Copy ID"
|
|
104
|
+
onClick={(e) => {
|
|
105
|
+
e.stopPropagation();
|
|
106
|
+
navigator.clipboard.writeText(String(record[key]));
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<span className="material-symbols-outlined" style={{ fontSize: '14px' }}>content_copy</span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
) : (
|
|
113
|
+
<div style={{ maxWidth: '140px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
114
|
+
{String(record[key] ?? "")}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</td>
|
|
118
|
+
))}
|
|
119
|
+
<td>
|
|
120
|
+
<ActivityCell record={record} />
|
|
121
|
+
</td>
|
|
122
|
+
<td style={{ textAlign: 'right', paddingRight: '20px' }}>
|
|
123
|
+
<div style={{ display: 'flex', gap: '4px', justifyContent: 'flex-end' }}>
|
|
124
|
+
{canUpdate && (
|
|
125
|
+
<Link href={`${basePath}/${record.id}`} className="adminforge-btn-icon" title="Edit">
|
|
126
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>edit</span>
|
|
127
|
+
</Link>
|
|
128
|
+
)}
|
|
129
|
+
{canDelete && (
|
|
130
|
+
<button
|
|
131
|
+
className="adminforge-btn-icon adminforge-btn-icon-danger"
|
|
132
|
+
title="Delete"
|
|
133
|
+
onClick={async () => {
|
|
134
|
+
if (confirm("Are you sure you want to delete this item?")) {
|
|
135
|
+
const res = await fetch(`/api/${collection.name}/${record.id}`, { method: 'DELETE' });
|
|
136
|
+
if (res.ok) {
|
|
137
|
+
window.location.reload();
|
|
138
|
+
} else {
|
|
139
|
+
const err = await res.json();
|
|
140
|
+
alert(err.error || "Failed to delete item");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>delete</span>
|
|
146
|
+
</button>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</td>
|
|
150
|
+
</tr>
|
|
151
|
+
))}
|
|
152
|
+
{records.length === 0 && <tr><td colSpan={displayKeys.length + 1}>No records found</td></tr>}
|
|
153
|
+
</tbody>
|
|
154
|
+
</table>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
package/src/ui.ts
ADDED
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
const isDev = process.env.DEV === "true";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
entry: {
|
|
7
|
+
index: "src/index.ts",
|
|
8
|
+
ui: "src/ui.ts",
|
|
9
|
+
next: "src/next.ts",
|
|
10
|
+
auth: "src/auth/index.ts",
|
|
11
|
+
"auth-client": "src/auth/provider.tsx",
|
|
12
|
+
styles: "src/styles/adminforge.css",
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
format: ["esm", "cjs"],
|
|
16
|
+
dts: !isDev,
|
|
17
|
+
sourcemap: true,
|
|
18
|
+
clean: !isDev,
|
|
19
|
+
external: [
|
|
20
|
+
"react",
|
|
21
|
+
"react-dom",
|
|
22
|
+
"next",
|
|
23
|
+
"next-auth",
|
|
24
|
+
"@auth/core",
|
|
25
|
+
"@prisma/client",
|
|
26
|
+
"zod",
|
|
27
|
+
"jsonwebtoken",
|
|
28
|
+
"@tanstack/react-table",
|
|
29
|
+
"@tiptap/core",
|
|
30
|
+
"@tiptap/react",
|
|
31
|
+
"@tiptap/pm",
|
|
32
|
+
"@tiptap/starter-kit",
|
|
33
|
+
"@tiptap/extension-bubble-menu",
|
|
34
|
+
"@tiptap/extension-floating-menu",
|
|
35
|
+
"@tiptap/extension-highlight",
|
|
36
|
+
"@tiptap/extension-horizontal-rule",
|
|
37
|
+
"@tiptap/extension-image",
|
|
38
|
+
"@tiptap/extension-link",
|
|
39
|
+
"@tiptap/extension-placeholder",
|
|
40
|
+
"@tiptap/extension-subscript",
|
|
41
|
+
"@tiptap/extension-superscript",
|
|
42
|
+
"@tiptap/extension-task-item",
|
|
43
|
+
"@tiptap/extension-task-list",
|
|
44
|
+
"@tiptap/extension-text-align",
|
|
45
|
+
"@tiptap/extension-typography",
|
|
46
|
+
"@tiptap/extension-underline",
|
|
47
|
+
"prosemirror-view",
|
|
48
|
+
"prosemirror-state",
|
|
49
|
+
"prosemirror-model",
|
|
50
|
+
"prosemirror-transform",
|
|
51
|
+
"prosemirror-keymap",
|
|
52
|
+
],
|
|
53
|
+
splitting: false,
|
|
54
|
+
});
|