@zerotal/admin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/package.json +78 -0
- package/src/Cluster.ts +50 -0
- package/src/Panel.ts +288 -0
- package/src/PanelInstance.ts +644 -0
- package/src/Resource.ts +918 -0
- package/src/actions/Action.ts +607 -0
- package/src/actions/ImportRecordsJob.ts +108 -0
- package/src/actions/csv.ts +123 -0
- package/src/actions/index.ts +39 -0
- package/src/actions/render.tsx +181 -0
- package/src/actions/transfer.ts +307 -0
- package/src/actions/xlsx.ts +304 -0
- package/src/auth/AuthLayout.tsx +34 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/pages/ForgotPasswordPage.tsx +87 -0
- package/src/auth/pages/LoginPage.tsx +121 -0
- package/src/auth/pages/ProfilePage.tsx +216 -0
- package/src/auth/pages/ResetPasswordPage.tsx +103 -0
- package/src/auth/pages/VerifyEmailPage.tsx +68 -0
- package/src/auth/register.ts +44 -0
- package/src/authRoles.ts +141 -0
- package/src/commands/MakeAdminResourceCommand.ts +181 -0
- package/src/config.ts +128 -0
- package/src/dashboardLayout.ts +101 -0
- package/src/databaseMedia.ts +148 -0
- package/src/databaseNotifications.ts +169 -0
- package/src/form/Field.ts +928 -0
- package/src/form/ResourceForm.ts +48 -0
- package/src/form/Section.ts +364 -0
- package/src/form/editors.ts +43 -0
- package/src/form/index.ts +59 -0
- package/src/history.ts +151 -0
- package/src/impersonation.ts +126 -0
- package/src/index.ts +380 -0
- package/src/infolist/Entry.ts +537 -0
- package/src/infolist/Section.ts +99 -0
- package/src/infolist/index.ts +38 -0
- package/src/media.ts +297 -0
- package/src/notifications.ts +65 -0
- package/src/pages/AdminPage.ts +100 -0
- package/src/pages/ConsolePage.tsx +324 -0
- package/src/pages/DashboardPage.tsx +264 -0
- package/src/pages/MediaPage.tsx +346 -0
- package/src/pages/NotificationsPage.tsx +155 -0
- package/src/pages/RecordViewPage.tsx +951 -0
- package/src/pages/ResourceFormPage.tsx +1856 -0
- package/src/pages/ResourceListPage.tsx +2552 -0
- package/src/pages/RolesPage.tsx +325 -0
- package/src/pages/SearchPage.tsx +169 -0
- package/src/plugin.ts +283 -0
- package/src/provider/AdminAbilityMiddleware.ts +25 -0
- package/src/provider/AdminGuardMiddleware.ts +29 -0
- package/src/provider/AdminProvider.ts +334 -0
- package/src/relations/RelationManager.ts +114 -0
- package/src/renderHooks.ts +86 -0
- package/src/roles.ts +175 -0
- package/src/savedViews.ts +79 -0
- package/src/support/ability.ts +73 -0
- package/src/support/authorize.ts +105 -0
- package/src/support/countCache.ts +37 -0
- package/src/support/hostPage.ts +30 -0
- package/src/table/Column.ts +353 -0
- package/src/table/Constraint.ts +238 -0
- package/src/table/Filter.ts +275 -0
- package/src/table/Group.ts +73 -0
- package/src/table/Tab.ts +77 -0
- package/src/testing.ts +121 -0
- package/src/theme.ts +70 -0
- package/src/ui/AdminLayout.tsx +355 -0
- package/src/ui/Breadcrumbs.tsx +84 -0
- package/src/ui/environmentIndicator.tsx +63 -0
- package/src/ui/icons.tsx +124 -0
- package/src/widgets/Widget.ts +251 -0
- package/src/widgets/render.tsx +154 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impersonation — acting as another user, and getting back.
|
|
3
|
+
*
|
|
4
|
+
* The support request nobody can reproduce is usually solved by seeing what the
|
|
5
|
+
* person actually sees. This lets an operator become a user, marks the session
|
|
6
|
+
* so the panel can say so, and gives them one click back:
|
|
7
|
+
*
|
|
8
|
+
* static recordActions() {
|
|
9
|
+
* return [viewAction(), editAction(), impersonateAction()];
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* Two rules hold it together, and both matter:
|
|
13
|
+
*
|
|
14
|
+
* - **The original user is remembered in the session**, so returning is always
|
|
15
|
+
* possible and never depends on the impersonated account.
|
|
16
|
+
* - **Nobody may impersonate someone who could impersonate them back.** The
|
|
17
|
+
* resource's own `can("impersonate", record)` decides, and the default is to
|
|
18
|
+
* refuse rather than allow.
|
|
19
|
+
*/
|
|
20
|
+
import { frameworkLog } from "@zerotal/core/logger";
|
|
21
|
+
|
|
22
|
+
/** Session key holding the original user's id while impersonating. */
|
|
23
|
+
export const IMPERSONATOR_KEY = "admin.impersonator";
|
|
24
|
+
|
|
25
|
+
/** The session surface this needs — kept structural so `@zerotal/session` stays optional. */
|
|
26
|
+
interface SessionLike {
|
|
27
|
+
get<T = unknown>(key: string): T | undefined;
|
|
28
|
+
put(key: string, value: unknown): unknown;
|
|
29
|
+
forget(key: string): unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Resolve the request's session, or nothing when there isn't one. */
|
|
33
|
+
async function currentSession(): Promise<SessionLike | null> {
|
|
34
|
+
try {
|
|
35
|
+
const { RequestContext } = (await import("@zerotal/core")) as {
|
|
36
|
+
RequestContext: { tryGet: () => { session?: SessionLike } | undefined };
|
|
37
|
+
};
|
|
38
|
+
return RequestContext.tryGet()?.session ?? null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Auth, resolved lazily so it stays an optional peer. */
|
|
45
|
+
async function auth(): Promise<{
|
|
46
|
+
user: () => { id?: unknown; name?: unknown } | null;
|
|
47
|
+
loginUsingId?: (id: unknown) => Promise<unknown> | unknown;
|
|
48
|
+
} | null> {
|
|
49
|
+
try {
|
|
50
|
+
const mod = (await import(/* @vite-ignore */ "@zerotal/auth" as string)) as {
|
|
51
|
+
Auth?: {
|
|
52
|
+
user: () => { id?: unknown; name?: unknown } | null;
|
|
53
|
+
loginUsingId?: (id: unknown) => Promise<unknown> | unknown;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
return mod.Auth ?? null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether this request is running as somebody else. */
|
|
63
|
+
export async function isImpersonating(): Promise<boolean> {
|
|
64
|
+
const session = await currentSession();
|
|
65
|
+
return session?.get(IMPERSONATOR_KEY) != null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The impersonated user's display name, for the banner. */
|
|
69
|
+
export async function impersonatedName(): Promise<string | null> {
|
|
70
|
+
if (!(await isImpersonating())) return null;
|
|
71
|
+
const a = await auth();
|
|
72
|
+
const user = a?.user();
|
|
73
|
+
const name = user?.["name" as keyof typeof user];
|
|
74
|
+
return typeof name === "string" ? name : user?.id != null ? `#${String(user.id)}` : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Become `userId`, remembering who to come back as.
|
|
79
|
+
*
|
|
80
|
+
* Refuses to start a second impersonation on top of a first: nesting makes
|
|
81
|
+
* "stop" ambiguous, and there is no case where it helps.
|
|
82
|
+
*/
|
|
83
|
+
export async function startImpersonating(userId: unknown): Promise<[true] | [false, string]> {
|
|
84
|
+
const [session, a] = await Promise.all([currentSession(), auth()]);
|
|
85
|
+
if (!session) return [false, "Impersonation needs a session."];
|
|
86
|
+
if (!a?.loginUsingId) return [false, "Impersonation needs @zerotal/auth."];
|
|
87
|
+
if (session.get(IMPERSONATOR_KEY) != null) {
|
|
88
|
+
return [false, "Already impersonating — stop first."];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const current = a.user();
|
|
92
|
+
if (current?.id == null) return [false, "Nobody is signed in."];
|
|
93
|
+
if (String(current.id) === String(userId)) return [false, "That is already you."];
|
|
94
|
+
|
|
95
|
+
session.put(IMPERSONATOR_KEY, current.id);
|
|
96
|
+
try {
|
|
97
|
+
await a.loginUsingId(userId);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Put the marker back the way it was rather than leaving a session that
|
|
100
|
+
// claims to be impersonating when the switch never happened.
|
|
101
|
+
session.forget(IMPERSONATOR_KEY);
|
|
102
|
+
frameworkLog("admin").warn("Impersonation failed", { userId }, error);
|
|
103
|
+
return [false, "Could not switch to that user."];
|
|
104
|
+
}
|
|
105
|
+
return [true];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Return to whoever started the impersonation. */
|
|
109
|
+
export async function stopImpersonating(): Promise<[true] | [false, string]> {
|
|
110
|
+
const [session, a] = await Promise.all([currentSession(), auth()]);
|
|
111
|
+
const original = session?.get(IMPERSONATOR_KEY);
|
|
112
|
+
if (!session || original == null) return [false, "Not impersonating."];
|
|
113
|
+
if (!a?.loginUsingId) return [false, "Impersonation needs @zerotal/auth."];
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await a.loginUsingId(original);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
frameworkLog("admin").warn("Could not restore the original user", undefined, error);
|
|
119
|
+
return [false, "Could not switch back — sign in again."];
|
|
120
|
+
} finally {
|
|
121
|
+
// Cleared either way: a marker left behind would strand the session in a
|
|
122
|
+
// state where the banner shows but "stop" does nothing.
|
|
123
|
+
session.forget(IMPERSONATOR_KEY);
|
|
124
|
+
}
|
|
125
|
+
return [true];
|
|
126
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zerotal/admin — a declarative, server-driven admin panel built on
|
|
3
|
+
* @zerotal/flow (reactivity) and @zerotal/flow-ui (components).
|
|
4
|
+
*
|
|
5
|
+
* Quick start:
|
|
6
|
+
*
|
|
7
|
+
* // bootstrap/providers.ts
|
|
8
|
+
* import { FlowProvider } from "@zerotal/flow";
|
|
9
|
+
* import { AdminProvider } from "@zerotal/admin";
|
|
10
|
+
* export default [FlowProvider, AdminProvider];
|
|
11
|
+
*
|
|
12
|
+
* // app/admin.ts
|
|
13
|
+
* import { Panel, Resource, text } from "@zerotal/admin";
|
|
14
|
+
* import { User } from "./models/User.ts";
|
|
15
|
+
*
|
|
16
|
+
* Panel.configure({ brand: "Acme", path: "/admin" });
|
|
17
|
+
*
|
|
18
|
+
* class UserResource extends Resource {
|
|
19
|
+
* static model = User;
|
|
20
|
+
* static navigationIcon = "users";
|
|
21
|
+
* static navigationGroup = "Access";
|
|
22
|
+
* static columns() {
|
|
23
|
+
* return [
|
|
24
|
+
* text("id").sortable(),
|
|
25
|
+
* text("name").searchable().sortable(),
|
|
26
|
+
* text("email").searchable(),
|
|
27
|
+
* text("role").badge((v) => (v === "admin" ? "primary" : "muted")),
|
|
28
|
+
* ];
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Panel.register(UserResource);
|
|
33
|
+
*
|
|
34
|
+
* The default UI ships with light + dark mode out of the box (Tailwind via CDN
|
|
35
|
+
* for now; swap to a real build later by editing `theme.ts` only).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export { Resource } from "./Resource.ts";
|
|
39
|
+
export type {
|
|
40
|
+
AdminModel,
|
|
41
|
+
AdminQuery,
|
|
42
|
+
AdminRecord,
|
|
43
|
+
ListOptions,
|
|
44
|
+
RecordPage,
|
|
45
|
+
EmptyState,
|
|
46
|
+
QueryModifier,
|
|
47
|
+
} from "./Resource.ts";
|
|
48
|
+
|
|
49
|
+
export { Panel, PanelInstance, DEFAULT_PANEL_ID } from "./Panel.ts";
|
|
50
|
+
export type { ResourceClass, NavItem, NavGroup, PanelPage } from "./Panel.ts";
|
|
51
|
+
|
|
52
|
+
// Render hooks — named positions in the chrome anything can render into.
|
|
53
|
+
export { resolveRenderHooks } from "./renderHooks.ts";
|
|
54
|
+
export type { RenderHook, RenderHookName, RenderHookContext } from "./renderHooks.ts";
|
|
55
|
+
|
|
56
|
+
// Database-backed notifications for the bell.
|
|
57
|
+
export { databaseNotifications } from "./databaseNotifications.ts";
|
|
58
|
+
export type { DatabaseNotificationOptions, StoredNotification } from "./databaseNotifications.ts";
|
|
59
|
+
|
|
60
|
+
// Clusters — a shared URL segment and sidebar entry for a group of resources.
|
|
61
|
+
export { Cluster } from "./Cluster.ts";
|
|
62
|
+
export type { ClusterClass } from "./Cluster.ts";
|
|
63
|
+
|
|
64
|
+
// Custom pages — the app-facing door for anything that isn't a Resource.
|
|
65
|
+
export { AdminPage } from "./pages/AdminPage.ts";
|
|
66
|
+
export type { AdminPageClass } from "./pages/AdminPage.ts";
|
|
67
|
+
|
|
68
|
+
// The contribution surface. Packages push into the `admin.panel` binding rather
|
|
69
|
+
// than importing these types; they're exported for app-authored plugins.
|
|
70
|
+
export type {
|
|
71
|
+
AdminPanelHost,
|
|
72
|
+
AdminPlugin,
|
|
73
|
+
PageContribution,
|
|
74
|
+
ConsoleContribution,
|
|
75
|
+
ConsoleTab,
|
|
76
|
+
ConsoleColumn,
|
|
77
|
+
ConsoleAction,
|
|
78
|
+
ConsoleHeaderAction,
|
|
79
|
+
ConsoleRow,
|
|
80
|
+
WidgetContribution,
|
|
81
|
+
NavContribution,
|
|
82
|
+
PanelSearchProvider,
|
|
83
|
+
SearchHit,
|
|
84
|
+
TopbarSlot,
|
|
85
|
+
UserMenuContribution,
|
|
86
|
+
PanelPageClass,
|
|
87
|
+
} from "./plugin.ts";
|
|
88
|
+
export type { AdminAuthorizer } from "./support/ability.ts";
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
Column,
|
|
92
|
+
text,
|
|
93
|
+
toggleColumn,
|
|
94
|
+
imageColumn,
|
|
95
|
+
colorColumn,
|
|
96
|
+
iconColumn,
|
|
97
|
+
selectColumn,
|
|
98
|
+
textInputColumn,
|
|
99
|
+
} from "./table/Column.ts";
|
|
100
|
+
export type {
|
|
101
|
+
BadgeTone,
|
|
102
|
+
CellAlign,
|
|
103
|
+
ColumnKind,
|
|
104
|
+
ColumnOption,
|
|
105
|
+
RenderableCell,
|
|
106
|
+
SummaryKind,
|
|
107
|
+
ColumnSummary,
|
|
108
|
+
SummaryResult,
|
|
109
|
+
} from "./table/Column.ts";
|
|
110
|
+
|
|
111
|
+
export { Tab, tab } from "./table/Tab.ts";
|
|
112
|
+
|
|
113
|
+
export { Group, group } from "./table/Group.ts";
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
Filter,
|
|
117
|
+
selectFilter,
|
|
118
|
+
textFilter,
|
|
119
|
+
ternaryFilter,
|
|
120
|
+
queryBuilder,
|
|
121
|
+
parseRuleTree,
|
|
122
|
+
ruleTreeIsEmpty,
|
|
123
|
+
describeRuleTree,
|
|
124
|
+
} from "./table/Filter.ts";
|
|
125
|
+
export type { QueryRule } from "./table/Filter.ts";
|
|
126
|
+
|
|
127
|
+
// Query-builder constraints — what a build-your-own filter may compare.
|
|
128
|
+
export {
|
|
129
|
+
Constraint,
|
|
130
|
+
textConstraint,
|
|
131
|
+
numberConstraint,
|
|
132
|
+
dateConstraint,
|
|
133
|
+
booleanConstraint,
|
|
134
|
+
selectConstraint,
|
|
135
|
+
} from "./table/Constraint.ts";
|
|
136
|
+
export type {
|
|
137
|
+
ConstraintKind,
|
|
138
|
+
ConstraintOperator,
|
|
139
|
+
ConstraintOption,
|
|
140
|
+
Conjunction,
|
|
141
|
+
} from "./table/Constraint.ts";
|
|
142
|
+
export type { FilterType, FilterOption, FilterApply } from "./table/Filter.ts";
|
|
143
|
+
|
|
144
|
+
// Dashboard widgets — stat rows, charts and small tables.
|
|
145
|
+
export {
|
|
146
|
+
Stat,
|
|
147
|
+
StatsWidget,
|
|
148
|
+
stat,
|
|
149
|
+
statsWidget,
|
|
150
|
+
ChartWidget,
|
|
151
|
+
chartWidget,
|
|
152
|
+
TableWidget,
|
|
153
|
+
tableWidget,
|
|
154
|
+
widgetPollInterval,
|
|
155
|
+
} from "./widgets/Widget.ts";
|
|
156
|
+
export { renderWidgets } from "./widgets/render.tsx";
|
|
157
|
+
export type {
|
|
158
|
+
WidgetTone,
|
|
159
|
+
StatsResolver,
|
|
160
|
+
ChartType,
|
|
161
|
+
ChartData,
|
|
162
|
+
ChartDataset,
|
|
163
|
+
ChartResolver,
|
|
164
|
+
TableWidgetColumn,
|
|
165
|
+
TableRowsResolver,
|
|
166
|
+
DashboardWidget,
|
|
167
|
+
} from "./widgets/Widget.ts";
|
|
168
|
+
|
|
169
|
+
export { RelationManager, hasMany, belongsToMany } from "./relations/RelationManager.ts";
|
|
170
|
+
export type { RelationKind, PivotColumn } from "./relations/RelationManager.ts";
|
|
171
|
+
|
|
172
|
+
// Actions — per-row, above the table, and over a selection.
|
|
173
|
+
export {
|
|
174
|
+
Action,
|
|
175
|
+
ActionGroup,
|
|
176
|
+
action,
|
|
177
|
+
actionGroup,
|
|
178
|
+
flattenActions,
|
|
179
|
+
viewAction,
|
|
180
|
+
editAction,
|
|
181
|
+
deleteAction,
|
|
182
|
+
createAction,
|
|
183
|
+
replicateAction,
|
|
184
|
+
impersonateAction,
|
|
185
|
+
bulkEditAction,
|
|
186
|
+
bulkDeleteAction,
|
|
187
|
+
restoreAction,
|
|
188
|
+
forceDeleteAction,
|
|
189
|
+
bulkRestoreAction,
|
|
190
|
+
bulkForceDeleteAction,
|
|
191
|
+
exportAction,
|
|
192
|
+
bulkExportAction,
|
|
193
|
+
importAction,
|
|
194
|
+
importCsv,
|
|
195
|
+
IMPORT_ROW_LIMIT,
|
|
196
|
+
MAPPING_FIELD_PREFIX,
|
|
197
|
+
toCsv,
|
|
198
|
+
parseCsv,
|
|
199
|
+
guessColumnMapping,
|
|
200
|
+
renderAction,
|
|
201
|
+
renderActionGroup,
|
|
202
|
+
renderActionMenuItem,
|
|
203
|
+
} from "./actions/index.ts";
|
|
204
|
+
export { ImportRecordsJob, runQueuedImport, dispatchImport } from "./actions/ImportRecordsJob.ts";
|
|
205
|
+
export type { ImportRecordsPayload } from "./actions/ImportRecordsJob.ts";
|
|
206
|
+
export type {
|
|
207
|
+
ActionColor,
|
|
208
|
+
ActionContext,
|
|
209
|
+
ActionHandler,
|
|
210
|
+
ActionItem,
|
|
211
|
+
ActionPage,
|
|
212
|
+
ActionVisible,
|
|
213
|
+
ImportResult,
|
|
214
|
+
} from "./actions/index.ts";
|
|
215
|
+
|
|
216
|
+
// Infolist (View page) building blocks — read-only schemas.
|
|
217
|
+
export {
|
|
218
|
+
Entry,
|
|
219
|
+
textEntry,
|
|
220
|
+
iconEntry,
|
|
221
|
+
imageEntry,
|
|
222
|
+
colorEntry,
|
|
223
|
+
codeEntry,
|
|
224
|
+
keyValueEntry,
|
|
225
|
+
repeatableEntry,
|
|
226
|
+
Section,
|
|
227
|
+
section,
|
|
228
|
+
} from "./infolist/index.ts";
|
|
229
|
+
export type {
|
|
230
|
+
InfolistComponent,
|
|
231
|
+
EntryDisplay,
|
|
232
|
+
EntryKind,
|
|
233
|
+
EntrySize,
|
|
234
|
+
EntryWeight,
|
|
235
|
+
} from "./infolist/index.ts";
|
|
236
|
+
|
|
237
|
+
// Form (Create/Edit page) building blocks — editable schemas.
|
|
238
|
+
export {
|
|
239
|
+
Field,
|
|
240
|
+
textInput,
|
|
241
|
+
textarea,
|
|
242
|
+
select,
|
|
243
|
+
checkbox,
|
|
244
|
+
toggle,
|
|
245
|
+
radio,
|
|
246
|
+
checkboxList,
|
|
247
|
+
datePicker,
|
|
248
|
+
dateTimePicker,
|
|
249
|
+
timePicker,
|
|
250
|
+
colorPicker,
|
|
251
|
+
hidden,
|
|
252
|
+
tagsInput,
|
|
253
|
+
keyValue,
|
|
254
|
+
fileUpload,
|
|
255
|
+
mediaPicker,
|
|
256
|
+
slider,
|
|
257
|
+
toggleButtons,
|
|
258
|
+
codeEditor,
|
|
259
|
+
markdownEditor,
|
|
260
|
+
richEditor,
|
|
261
|
+
repeater,
|
|
262
|
+
builder,
|
|
263
|
+
customField,
|
|
264
|
+
BuilderBlock,
|
|
265
|
+
builderBlock,
|
|
266
|
+
FormSection,
|
|
267
|
+
formSection,
|
|
268
|
+
isFormSection,
|
|
269
|
+
flattenFields,
|
|
270
|
+
toFormSections,
|
|
271
|
+
toFormLayout,
|
|
272
|
+
FormTab,
|
|
273
|
+
FormTabs,
|
|
274
|
+
formTab,
|
|
275
|
+
formTabs,
|
|
276
|
+
WizardStep,
|
|
277
|
+
Wizard,
|
|
278
|
+
wizardStep,
|
|
279
|
+
wizard,
|
|
280
|
+
fieldset,
|
|
281
|
+
Callout,
|
|
282
|
+
callout,
|
|
283
|
+
Prime,
|
|
284
|
+
prime,
|
|
285
|
+
primeHtml,
|
|
286
|
+
primeImage,
|
|
287
|
+
FormSplit,
|
|
288
|
+
split,
|
|
289
|
+
makeResourceForm,
|
|
290
|
+
} from "./form/index.ts";
|
|
291
|
+
export type {
|
|
292
|
+
FieldType,
|
|
293
|
+
FieldMode,
|
|
294
|
+
FieldPredicate,
|
|
295
|
+
SelectOption,
|
|
296
|
+
FormComponent,
|
|
297
|
+
FormBlock,
|
|
298
|
+
CalloutTone,
|
|
299
|
+
PrimeKind,
|
|
300
|
+
ResourceFormClass,
|
|
301
|
+
} from "./form/index.ts";
|
|
302
|
+
|
|
303
|
+
export type { AdminConfigShape, AdminAuthConfig, UserMenu, UserMenuItem } from "./config.ts";
|
|
304
|
+
export { AdminConfig, DEFAULT_ADMIN_CONFIG } from "./config.ts";
|
|
305
|
+
// The auth pages themselves live behind the `@zerotal/admin/auth` subpath so the
|
|
306
|
+
// `@zerotal/auth` dependency they pull in stays optional for the core package.
|
|
307
|
+
|
|
308
|
+
export { AdminProvider } from "./provider/AdminProvider.ts";
|
|
309
|
+
export { AdminGuardMiddleware } from "./provider/AdminGuardMiddleware.ts";
|
|
310
|
+
export { AdminAbilityMiddleware } from "./provider/AdminAbilityMiddleware.ts";
|
|
311
|
+
|
|
312
|
+
// UI building blocks (for custom pages / theming).
|
|
313
|
+
export { AdminLayout, makeAdminLayout } from "./ui/AdminLayout.tsx";
|
|
314
|
+
export { Icon } from "./ui/icons.tsx";
|
|
315
|
+
export { adminHead, adminTokensCss, adminTailwindConfig, THEME_STORAGE_KEY } from "./theme.ts";
|
|
316
|
+
export type { AdminThemeConfig } from "./theme.ts";
|
|
317
|
+
|
|
318
|
+
// Pages (for advanced customization / custom routing).
|
|
319
|
+
export { DashboardPage, makeDashboardPage } from "./pages/DashboardPage.tsx";
|
|
320
|
+
export { SearchPage, makeSearchPage } from "./pages/SearchPage.tsx";
|
|
321
|
+
export { NotificationsPage, makeNotificationsPage } from "./pages/NotificationsPage.tsx";
|
|
322
|
+
export type { AdminNotification, NotificationProvider } from "./notifications.ts";
|
|
323
|
+
export { NOTIFICATION_CHANNEL, NOTIFICATION_EVENT } from "./notifications.ts";
|
|
324
|
+
export { ResourceListPage, makeResourceListPage } from "./pages/ResourceListPage.tsx";
|
|
325
|
+
export { RecordViewPage, makeRecordViewPage } from "./pages/RecordViewPage.tsx";
|
|
326
|
+
export { ResourceFormPage, registerResourceForm } from "./pages/ResourceFormPage.tsx";
|
|
327
|
+
export { ConsolePage, makeConsolePage } from "./pages/ConsolePage.tsx";
|
|
328
|
+
export type { FormModeConfig } from "./pages/ResourceFormPage.tsx";
|
|
329
|
+
|
|
330
|
+
// ── Media library ────────────────────────────────────────────────────────────
|
|
331
|
+
export { MediaPage, makeMediaPage } from "./pages/MediaPage.tsx";
|
|
332
|
+
export type { MediaItem, MediaProvider, StoreMediaOptions, UploadedFileLike } from "./media.ts";
|
|
333
|
+
export {
|
|
334
|
+
isImage,
|
|
335
|
+
isUpload,
|
|
336
|
+
formatSize,
|
|
337
|
+
mediaPath,
|
|
338
|
+
mediaUrl,
|
|
339
|
+
resolveMediaSrc,
|
|
340
|
+
storeMedia,
|
|
341
|
+
deleteMedia,
|
|
342
|
+
} from "./media.ts";
|
|
343
|
+
export { databaseMedia } from "./databaseMedia.ts";
|
|
344
|
+
export type { DatabaseMediaOptions } from "./databaseMedia.ts";
|
|
345
|
+
|
|
346
|
+
// ── Saved views ──────────────────────────────────────────────────────────────
|
|
347
|
+
export type { SavedView, SavedViewProvider } from "./savedViews.ts";
|
|
348
|
+
export { VIEW_PARAMS, viewQuery, viewIsActive } from "./savedViews.ts";
|
|
349
|
+
|
|
350
|
+
// ── Record history ───────────────────────────────────────────────────────────
|
|
351
|
+
export type { HistoryEntry, HistoryChange, HistoryOptions } from "./history.ts";
|
|
352
|
+
export { recordHistory, revertPayload } from "./history.ts";
|
|
353
|
+
|
|
354
|
+
// ── Impersonation ────────────────────────────────────────────────────────────
|
|
355
|
+
export {
|
|
356
|
+
IMPERSONATOR_KEY,
|
|
357
|
+
isImpersonating,
|
|
358
|
+
impersonatedName,
|
|
359
|
+
startImpersonating,
|
|
360
|
+
stopImpersonating,
|
|
361
|
+
} from "./impersonation.ts";
|
|
362
|
+
|
|
363
|
+
// ── Environment indicator ────────────────────────────────────────────────────
|
|
364
|
+
export { environmentIndicator } from "./ui/environmentIndicator.tsx";
|
|
365
|
+
export type { EnvironmentIndicatorOptions } from "./ui/environmentIndicator.tsx";
|
|
366
|
+
|
|
367
|
+
// ── Spreadsheet export ───────────────────────────────────────────────────────
|
|
368
|
+
export { toXlsx } from "./actions/xlsx.ts";
|
|
369
|
+
export type { ExportFormat } from "./actions/transfer.ts";
|
|
370
|
+
|
|
371
|
+
// ── Roles & permissions ──────────────────────────────────────────────────────
|
|
372
|
+
export { RolesPage, makeRolesPage } from "./pages/RolesPage.tsx";
|
|
373
|
+
export type { Role, Permission, RoleProvider } from "./roles.ts";
|
|
374
|
+
export { panelPermissions, groupPermissions, roleHas } from "./roles.ts";
|
|
375
|
+
|
|
376
|
+
// ── Dashboard layout ─────────────────────────────────────────────────────────
|
|
377
|
+
export type { DashboardLayout, DashboardLayoutStore } from "./dashboardLayout.ts";
|
|
378
|
+
export { EMPTY_LAYOUT, applyLayout, moveKey, reconcile } from "./dashboardLayout.ts";
|
|
379
|
+
export { authRoles } from "./authRoles.ts";
|
|
380
|
+
export type { AuthRolesOptions } from "./authRoles.ts";
|