@postxl/generators 1.17.2 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-actions/actions.generator.d.ts +1 -0
- package/dist/backend-actions/actions.generator.js +6 -0
- package/dist/backend-actions/actions.generator.js.map +1 -1
- package/dist/backend-actions/generators/actions-module.generator.js +3 -2
- package/dist/backend-actions/generators/actions-module.generator.js.map +1 -1
- package/dist/backend-actions/generators/authorization-policy-service.generator.d.ts +2 -0
- package/dist/backend-actions/generators/authorization-policy-service.generator.js +214 -0
- package/dist/backend-actions/generators/authorization-policy-service.generator.js.map +1 -0
- package/dist/backend-actions/generators/authorization-service.generator.d.ts +1 -1
- package/dist/backend-actions/generators/authorization-service.generator.js +20 -8
- package/dist/backend-actions/generators/authorization-service.generator.js.map +1 -1
- package/dist/backend-actions/generators/dispatcher-service.generator.js +3 -2
- package/dist/backend-actions/generators/dispatcher-service.generator.js.map +1 -1
- package/dist/backend-ai/generators/ai-route.generator.js +3 -3
- package/dist/backend-authentication/authentication.generator.js +23 -1
- package/dist/backend-authentication/authentication.generator.js.map +1 -1
- package/dist/backend-authentication/generators/auth-guard.generator.js +5 -8
- package/dist/backend-authentication/generators/auth-guard.generator.js.map +1 -1
- package/dist/backend-authentication/generators/authentication-module.generator.js +1 -1
- package/dist/backend-authentication/generators/authentication-service.generator.js +11 -8
- package/dist/backend-authentication/generators/authentication-service.generator.js.map +1 -1
- package/dist/backend-authentication/generators/authentication-types.generator.js +4 -3
- package/dist/backend-authentication/generators/authentication-types.generator.js.map +1 -1
- package/dist/backend-authentication/template/src/authentication.config.ts +9 -0
- package/dist/backend-authentication/template/src/authentication.mock.service.ts +77 -13
- package/dist/backend-authentication/template/src/utils.ts +45 -0
- package/dist/backend-core/backend.generator.js +16 -0
- package/dist/backend-core/backend.generator.js.map +1 -1
- package/dist/backend-core/generators/api-config.generator.js +5 -0
- package/dist/backend-core/generators/api-config.generator.js.map +1 -1
- package/dist/backend-core/types.d.ts +4 -0
- package/dist/backend-excel-io/generators/excel-io-service.generator.js +27 -11
- package/dist/backend-excel-io/generators/excel-io-service.generator.js.map +1 -1
- package/dist/backend-excel-io/template/excel-io.controller.ts +3 -3
- package/dist/backend-rest-api/generators/model-controller.generator.js +9 -5
- package/dist/backend-rest-api/generators/model-controller.generator.js.map +1 -1
- package/dist/backend-rest-api/template/restApi/src/restApi.utils.ts +9 -0
- package/dist/backend-router-trpc/generators/audit-log-route.generator.js +2 -2
- package/dist/backend-router-trpc/generators/excel-io-route.generator.js +1 -1
- package/dist/backend-router-trpc/generators/middleware.generator.js +8 -5
- package/dist/backend-router-trpc/generators/middleware.generator.js.map +1 -1
- package/dist/backend-router-trpc/generators/model-routes.generator.js +27 -7
- package/dist/backend-router-trpc/generators/model-routes.generator.js.map +1 -1
- package/dist/backend-router-trpc/generators/trpc-plugin.generator.js +9 -6
- package/dist/backend-router-trpc/generators/trpc-plugin.generator.js.map +1 -1
- package/dist/backend-router-trpc/generators/trpc-shared.generator.js +4 -24
- package/dist/backend-router-trpc/generators/trpc-shared.generator.js.map +1 -1
- package/dist/backend-router-trpc/router-trpc.generator.d.ts +4 -0
- package/dist/backend-router-trpc/router-trpc.generator.js +1 -0
- package/dist/backend-router-trpc/router-trpc.generator.js.map +1 -1
- package/dist/backend-router-trpc/template/viewer.router.ts +1 -6
- package/dist/backend-update/update-actions.decoders.d.ts +4 -4
- package/dist/backend-upload/template/src/upload.controller.ts +1 -1
- package/dist/backend-upload/template/src/upload.service.ts +11 -5
- package/dist/backend-view/model-view-service.generator.js +105 -52
- package/dist/backend-view/model-view-service.generator.js.map +1 -1
- package/dist/backend-view/view.generator.d.ts +2 -1
- package/dist/backend-view/view.generator.js +8 -1
- package/dist/backend-view/view.generator.js.map +1 -1
- package/dist/base/base.generator.js +2 -0
- package/dist/base/base.generator.js.map +1 -1
- package/dist/e2e/template/e2e/specs/example.spec.ts-snapshots/Navigate-to-homepage-and-take-snapshot-1-chromium-linux.png +0 -0
- package/dist/frontend-actions/actions.generator.js +1 -20
- package/dist/frontend-actions/actions.generator.js.map +1 -1
- package/dist/frontend-admin/admin.generator.js +4 -2
- package/dist/frontend-admin/admin.generator.js.map +1 -1
- package/dist/frontend-admin/generators/admin-sidebar.generator.d.ts +2 -1
- package/dist/frontend-admin/generators/admin-sidebar.generator.js +8 -26
- package/dist/frontend-admin/generators/admin-sidebar.generator.js.map +1 -1
- package/dist/frontend-admin/generators/authorization-utils.generator.d.ts +1 -0
- package/dist/frontend-admin/generators/authorization-utils.generator.js +20 -0
- package/dist/frontend-admin/generators/authorization-utils.generator.js.map +1 -0
- package/dist/frontend-admin/generators/comment-sidebar.generator.js +9 -1
- package/dist/frontend-admin/generators/comment-sidebar.generator.js.map +1 -1
- package/dist/frontend-admin/generators/data-management-page.generator.js +14 -7
- package/dist/frontend-admin/generators/data-management-page.generator.js.map +1 -1
- package/dist/frontend-admin/generators/excel-io-page.generator.js +16 -9
- package/dist/frontend-admin/generators/excel-io-page.generator.js.map +1 -1
- package/dist/frontend-admin/generators/import-review-page.generator.js +16 -10
- package/dist/frontend-admin/generators/import-review-page.generator.js.map +1 -1
- package/dist/frontend-admin/generators/model-admin-page.generator.js +399 -187
- package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
- package/dist/frontend-core/frontend.generator.d.ts +6 -0
- package/dist/frontend-core/frontend.generator.js +10 -3
- package/dist/frontend-core/frontend.generator.js.map +1 -1
- package/dist/frontend-core/template/README.md +2 -0
- package/dist/frontend-core/template/src/components/ui/application-header/application-header.tsx +44 -0
- package/dist/frontend-core/template/src/components/ui/color-mode-toggle/color-mode-toggle.tsx +1 -1
- package/dist/frontend-core/template/src/context-providers/auth-context-provider.tsx +1 -2
- package/dist/frontend-core/template/src/context-providers/header-context-provider.tsx +41 -0
- package/dist/frontend-core/template/src/pages/authorized-page-layout.tsx +49 -0
- package/dist/frontend-core/template/src/pages/dashboard/dashboard.page.tsx +82 -50
- package/dist/frontend-core/template/src/pages/login/login.page.tsx +1 -1
- package/dist/frontend-core/template/src/routes/_auth-routes.tsx +3 -2
- package/dist/frontend-core/template/src/styles/theme-default.css +7 -3
- package/dist/frontend-core/template/vite.config.ts +5 -0
- package/dist/frontend-core/types/component.d.ts +1 -1
- package/dist/frontend-core/types/contextprovider.d.ts +1 -1
- package/dist/frontend-core/types/hook.d.ts +1 -1
- package/dist/frontend-trpc-client/generators/model-hook.generator.js +104 -39
- package/dist/frontend-trpc-client/generators/model-hook.generator.js.map +1 -1
- package/dist/frontend-trpc-client/trpc-client.generator.js +28 -14
- package/dist/frontend-trpc-client/trpc-client.generator.js.map +1 -1
- package/dist/types/types.generator.d.ts +7 -0
- package/dist/types/types.generator.js +80 -0
- package/dist/types/types.generator.js.map +1 -1
- package/package.json +3 -3
|
@@ -39,12 +39,14 @@ const Generator = __importStar(require("@postxl/generator"));
|
|
|
39
39
|
* Generator function that generates the Admin page for a model
|
|
40
40
|
*/
|
|
41
41
|
function generateModelAdminPage({ model, context, }) {
|
|
42
|
+
const adminUiPolicy = resolveAdminUiPolicy(model, context);
|
|
43
|
+
const writePolicy = resolveWritePolicy(model, context);
|
|
42
44
|
const imports = Generator.ImportGenerator
|
|
43
45
|
//
|
|
44
46
|
.from(model.admin.page.pageComponent.location)
|
|
45
47
|
.add(model.table.component)
|
|
46
48
|
.add(context.admin.components.sidebar)
|
|
47
|
-
.add(context.admin.components.
|
|
49
|
+
.add(context.admin.components.auditLogSidebar)
|
|
48
50
|
.add(model.itemsHook)
|
|
49
51
|
.addImport({
|
|
50
52
|
from: Generator.toPackageName('react'),
|
|
@@ -85,8 +87,8 @@ function generateModelAdminPage({ model, context, }) {
|
|
|
85
87
|
items: [Generator.toFunctionName('useCommandPaletteActions')],
|
|
86
88
|
})
|
|
87
89
|
.addImport({
|
|
88
|
-
from: Generator.toBackendModuleLocation('@app-actions/ai-
|
|
89
|
-
items: [Generator.toFunctionName('
|
|
90
|
+
from: Generator.toBackendModuleLocation('@app-actions/ai-sidebar-content'),
|
|
91
|
+
items: [Generator.toFunctionName('AiSidebarContent')],
|
|
90
92
|
})
|
|
91
93
|
.addImport({
|
|
92
94
|
from: Generator.toBackendModuleLocation('@lib/command-palette-filter.utils'),
|
|
@@ -100,101 +102,45 @@ function generateModelAdminPage({ model, context, }) {
|
|
|
100
102
|
],
|
|
101
103
|
});
|
|
102
104
|
const showComments = model.name !== 'Comment' && !!context.admin.components.commentSidebar;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.addType(model.types.id)
|
|
120
|
-
.addImport({ from: Generator.toPackageName('react'), items: [Generator.toFunctionName('useRef')] })
|
|
121
|
-
.addImport({ from: Generator.toPackageName('sonner'), items: [Generator.toFunctionName('toast')] })
|
|
122
|
-
.addImport({ from: model.forms.createModal.location, items: [model.forms.createModal.name] });
|
|
123
|
-
// In case the model does not provide any explicit label field, we generally use the id field as fallback.
|
|
124
|
-
// However, in case of creating a new item, we do not have an Id yet and hence cannot show it in success and error messages.
|
|
125
|
-
const hasLabelField = model.labelField.name !== model.idField.name;
|
|
126
|
-
const variableName = hasLabelField ? `created${model.name}` : '_';
|
|
127
|
-
const label = hasLabelField ? ` "\${${variableName}.${model.labelField.name}}"` : '';
|
|
128
|
-
createModalHook = ` const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)`;
|
|
129
|
-
createModalIntentHook = `
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (typeof globalThis === 'undefined') {
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const storageKey = 'pxl:create-intent-model'
|
|
136
|
-
const targetModel = '${model.name}'
|
|
137
|
-
const openFromIntent = (intentModel: unknown) => {
|
|
138
|
-
if (intentModel === targetModel) {
|
|
139
|
-
setIsCreateModalOpen(true)
|
|
140
|
-
globalThis.sessionStorage.removeItem(storageKey)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
openFromIntent(globalThis.sessionStorage.getItem(storageKey))
|
|
145
|
-
|
|
146
|
-
const onIntent = (event: Event) => {
|
|
147
|
-
const detail = (event as CustomEvent<{ model?: string }>).detail
|
|
148
|
-
openFromIntent(detail?.model)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
globalThis.addEventListener('pxl:create-intent', onIntent)
|
|
152
|
-
return () => {
|
|
153
|
-
globalThis.removeEventListener('pxl:create-intent', onIntent)
|
|
154
|
-
}
|
|
155
|
-
}, [])`;
|
|
156
|
-
createModal = `
|
|
157
|
-
<${model.forms.createModal.name}
|
|
158
|
-
open={isCreateModalOpen}
|
|
159
|
-
onClose={() => setIsCreateModalOpen(false)}
|
|
160
|
-
onSuccess={(${variableName}) => {
|
|
161
|
-
toast.success(\`${model.userFriendlyName}${label} created successfully!\`)
|
|
162
|
-
setIsCreateModalOpen(false)
|
|
163
|
-
}}
|
|
164
|
-
onError={(${variableName}, error) => {
|
|
165
|
-
toast.error(\`Error creating ${model.userFriendlyName}${label}: \${String(error)}\`)
|
|
166
|
-
}}
|
|
167
|
-
/>`;
|
|
168
|
-
}
|
|
105
|
+
configureSidebarImports({ imports, context, showComments });
|
|
106
|
+
const relatedModels = collectRelatedModels({ model, context, imports });
|
|
107
|
+
const createModalContent = resolveCreateModalContent(model, imports);
|
|
108
|
+
const canAccessAdminExpression = toRoleExpression(adminUiPolicy, 'userRoles');
|
|
109
|
+
const canWriteModelExpression = toRoleExpression(writePolicy, 'userRoles');
|
|
110
|
+
const commentCountState = buildCommentCountState(showComments);
|
|
111
|
+
const modelHookMutationFields = buildModelHookMutationFields(model);
|
|
112
|
+
const commentSidebarLogic = buildCommentSidebarLogic({ showComments, model });
|
|
113
|
+
const commentSelectedEntityId = buildCommentSelectedEntityId(showComments);
|
|
114
|
+
const cellUpdateHandler = buildCellUpdateHandler(model);
|
|
115
|
+
const removeRowsHandler = buildRemoveRowsHandler(model);
|
|
116
|
+
const commentSidebarTab = buildCommentSidebarTab({ showComments, model });
|
|
117
|
+
const relatedTableProps = buildRelatedTableProps(relatedModels);
|
|
118
|
+
const addRowProp = buildAddRowProp(model);
|
|
119
|
+
const cellUpdateProp = buildCellUpdateProp(model);
|
|
120
|
+
const removeRowsProp = buildRemoveRowsProp(model);
|
|
169
121
|
return `
|
|
170
|
-
import {
|
|
171
|
-
import
|
|
122
|
+
import { SidebarTab, useDebouncedCallback } from '@postxl/ui-components'
|
|
123
|
+
import { APP_CONFIG } from '@lib/config'
|
|
172
124
|
import { useAuth } from '@context-providers/auth-context-provider'
|
|
125
|
+
import { hasAnyRole } from '@lib/authorization'
|
|
173
126
|
${imports.generate()}
|
|
174
127
|
|
|
175
|
-
const SIDEBAR_WIDTH_KEY = 'audit-log-sidebar-width'
|
|
176
|
-
|
|
177
128
|
export default function ${model.admin.page.pageComponent.name}() {
|
|
178
129
|
const navigate = useNavigate()
|
|
179
130
|
const { viewerData } = useAuth()
|
|
180
131
|
const currentUserId = viewerData?.user?.id
|
|
181
|
-
const
|
|
182
|
-
|
|
132
|
+
const userRoles = viewerData?.userRoles ?? []
|
|
133
|
+
const canAccessAdminPage = ${canAccessAdminExpression}
|
|
134
|
+
const canWriteModel = ${canWriteModelExpression}
|
|
135
|
+
${commentCountState}
|
|
136
|
+
|
|
183
137
|
const { selectedKey, focusedCell } = useSearch({
|
|
184
138
|
from: '/_auth-routes${model.admin.page.route}',
|
|
185
139
|
select: (search) => ({ selectedKey: search.selectedKey, focusedCell: search.focusedCell }),
|
|
186
140
|
})
|
|
187
141
|
|
|
188
|
-
${
|
|
189
|
-
${
|
|
190
|
-
const sidepanelOpen = useAiAssistantState((state) => state.open)
|
|
191
|
-
const { setOpen: setSidepanelOpen } = useAiAssistantActions()
|
|
192
|
-
|
|
193
|
-
// audit log sidebar width
|
|
194
|
-
const [sidebarWidth, setSidebarWidth] = useLocalStorageState(SIDEBAR_WIDTH_KEY, {
|
|
195
|
-
defaultValue: 350,
|
|
196
|
-
})
|
|
197
|
-
|
|
142
|
+
${createModalContent.hook}
|
|
143
|
+
${createModalContent.intentHook}
|
|
198
144
|
// Track selected row IDs from checkbox selection
|
|
199
145
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([])
|
|
200
146
|
|
|
@@ -212,7 +158,7 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
212
158
|
hasNextPage,
|
|
213
159
|
fetchNextPage,
|
|
214
160
|
total,
|
|
215
|
-
${
|
|
161
|
+
${modelHookMutationFields}
|
|
216
162
|
} = ${model.itemsHook.name}({ filters, sort: sortState })
|
|
217
163
|
|
|
218
164
|
// Queries for related models' maps used by the table (passed as separate props)
|
|
@@ -230,18 +176,7 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
230
176
|
return focusedCell
|
|
231
177
|
}, [focusedCell])
|
|
232
178
|
|
|
233
|
-
${
|
|
234
|
-
? `// Comment sidebar: compute filtered entity IDs and label map from table data
|
|
235
|
-
const commentFilteredIds = useMemo(() =>
|
|
236
|
-
filteredDataList?.map(item => String(item.id)) ?? [],
|
|
237
|
-
[filteredDataList]
|
|
238
|
-
)
|
|
239
|
-
const commentLabelMap = useMemo(() =>
|
|
240
|
-
Object.fromEntries((filteredDataList ?? []).map(item => [String(item.id), String(item.${model.labelField.name})])),
|
|
241
|
-
[filteredDataList]
|
|
242
|
-
)
|
|
243
|
-
`
|
|
244
|
-
: ''}
|
|
179
|
+
${commentSidebarLogic}
|
|
245
180
|
// Callback when row selection changes (checkbox)
|
|
246
181
|
const handleRowSelectionChange = useCallback((rowIds: string[]) => {
|
|
247
182
|
setSelectedRowIds(rowIds)
|
|
@@ -284,7 +219,7 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
284
219
|
// Callback when cell focus changes in the table — also sets selectedKey from the focused row
|
|
285
220
|
const handleCellChange = useCallback((args: { rowIndex: number; columnId: string }) => {
|
|
286
221
|
const row = filteredDataList?.[args.rowIndex]
|
|
287
|
-
const key = row?.${model.keyField.name}
|
|
222
|
+
const key = row?.${model.keyField.name} == null ? undefined : String(row.${model.keyField.name})
|
|
288
223
|
void navigate({ to: '${model.admin.page.route}', search: { selectedKey: key, focusedCell: args.columnId || undefined } })
|
|
289
224
|
// Clear checkbox selection when focusing a cell
|
|
290
225
|
setSelectedRowIds([])
|
|
@@ -292,6 +227,18 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
292
227
|
|
|
293
228
|
useEffect(() => {
|
|
294
229
|
const prefix = 'admin-${model._conjugated.kebabCase}-filter'
|
|
230
|
+
const getFilterInputSchema = (valueType: string) => {
|
|
231
|
+
if (valueType === 'number') {
|
|
232
|
+
return numberFilterInputSchema
|
|
233
|
+
}
|
|
234
|
+
if (valueType === 'date') {
|
|
235
|
+
return dateFilterInputSchema
|
|
236
|
+
}
|
|
237
|
+
if (valueType === 'boolean') {
|
|
238
|
+
return booleanFilterInputSchema
|
|
239
|
+
}
|
|
240
|
+
return stringFilterInputSchema
|
|
241
|
+
}
|
|
295
242
|
|
|
296
243
|
const filterActions = Object.entries(${model.types.filter.config.name}).map(([field, fieldConfig]) =>
|
|
297
244
|
input({
|
|
@@ -299,14 +246,7 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
299
246
|
visibility: 'visible',
|
|
300
247
|
group: 'Filter records by...',
|
|
301
248
|
label: toFilterActionLabel(field),
|
|
302
|
-
inputSchema:
|
|
303
|
-
fieldConfig.valueType === 'number'
|
|
304
|
-
? numberFilterInputSchema
|
|
305
|
-
: fieldConfig.valueType === 'date'
|
|
306
|
-
? dateFilterInputSchema
|
|
307
|
-
: fieldConfig.valueType === 'boolean'
|
|
308
|
-
? booleanFilterInputSchema
|
|
309
|
-
: stringFilterInputSchema,
|
|
249
|
+
inputSchema: getFilterInputSchema(fieldConfig.valueType),
|
|
310
250
|
run: async (query, inputValue) => {
|
|
311
251
|
if (fieldConfig.valueType === 'number') {
|
|
312
252
|
const parsed = numberFilterInputSchema.safeParse(inputValue)
|
|
@@ -400,9 +340,205 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
400
340
|
setSearchInput,
|
|
401
341
|
])
|
|
402
342
|
|
|
403
|
-
${
|
|
404
|
-
|
|
405
|
-
|
|
343
|
+
${commentSelectedEntityId}
|
|
344
|
+
|
|
345
|
+
${cellUpdateHandler}
|
|
346
|
+
|
|
347
|
+
${removeRowsHandler}
|
|
348
|
+
|
|
349
|
+
if (!canAccessAdminPage) {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<>
|
|
355
|
+
<SidebarTab
|
|
356
|
+
side="left"
|
|
357
|
+
id="admin-navigation"
|
|
358
|
+
icon={ListTree}
|
|
359
|
+
label="Navigation"
|
|
360
|
+
>
|
|
361
|
+
<AdminSidebarNavContent />
|
|
362
|
+
</SidebarTab>
|
|
363
|
+
${commentSidebarTab}
|
|
364
|
+
<SidebarTab
|
|
365
|
+
side="right"
|
|
366
|
+
id="audit-log-content"
|
|
367
|
+
icon={ScrollText}
|
|
368
|
+
label="Audit Log"
|
|
369
|
+
order={1}
|
|
370
|
+
>
|
|
371
|
+
<AuditLogSidebar
|
|
372
|
+
model="${model.name}"
|
|
373
|
+
entityId={selectedEntityId}
|
|
374
|
+
entityIds={selectedRowIds}
|
|
375
|
+
field={selectedField}
|
|
376
|
+
labelField="${model.labelField.name}"
|
|
377
|
+
onClearField={handleClearField}
|
|
378
|
+
onClearEntity={handleClearEntity}
|
|
379
|
+
/>
|
|
380
|
+
</SidebarTab>
|
|
381
|
+
|
|
382
|
+
{APP_CONFIG.enableAI && (
|
|
383
|
+
<SidebarTab
|
|
384
|
+
side="right"
|
|
385
|
+
id="ai-assistant-content"
|
|
386
|
+
icon={Sparkles}
|
|
387
|
+
label="AI Assistant"
|
|
388
|
+
order={2}
|
|
389
|
+
>
|
|
390
|
+
<AiSidebarContent />
|
|
391
|
+
</SidebarTab>
|
|
392
|
+
)}
|
|
393
|
+
<div className="p-4 overflow-hidden gap-4">
|
|
394
|
+
<h3 className="font-bold text-[1.75rem]">${model.userFriendlyName}</h3>
|
|
395
|
+
<${model.table.component.name}
|
|
396
|
+
data={filteredDataList ?? []}
|
|
397
|
+
filters={filters}
|
|
398
|
+
onFilterChange={(filterKey, val) => {
|
|
399
|
+
const config = ${model.types.filter.config.name}[filterKey]
|
|
400
|
+
if (config.valueType === 'boolean') {
|
|
401
|
+
handleFilterChange(
|
|
402
|
+
config.filterKey,
|
|
403
|
+
(val as string[]).map((v) => v === 'true'),
|
|
404
|
+
)
|
|
405
|
+
} else {
|
|
406
|
+
handleFilterChange(config.filterKey, val)
|
|
407
|
+
}
|
|
408
|
+
}}
|
|
409
|
+
searchInput={searchInput}
|
|
410
|
+
onSearchInputChange={handleSearchInputChange}
|
|
411
|
+
sort={sortState}
|
|
412
|
+
onSortChange={setSortState}
|
|
413
|
+
${cellUpdateProp}
|
|
414
|
+
${addRowProp}
|
|
415
|
+
${removeRowsProp}
|
|
416
|
+
onCellChange={handleCellChange}
|
|
417
|
+
onRowSelectionChange={handleRowSelectionChange}
|
|
418
|
+
selectedKey={selectedKey}
|
|
419
|
+
keyField="${model.keyField.name}"
|
|
420
|
+
hasNextPage={hasNextPage}
|
|
421
|
+
fetchNextPage={fetchNextPage}
|
|
422
|
+
total={total}
|
|
423
|
+
model="${model._conjugated.camelCase}"
|
|
424
|
+
currentUserId={currentUserId}
|
|
425
|
+
isAdmin={canWriteModel}
|
|
426
|
+
onFiltersReplace={setFilters}
|
|
427
|
+
onSortReplace={setSortState}
|
|
428
|
+
${relatedTableProps}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
${createModalContent.modal}
|
|
433
|
+
</>
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
function configureSidebarImports({ imports, context, showComments, }) {
|
|
439
|
+
if (showComments) {
|
|
440
|
+
imports.add(context.admin.components.commentSidebar);
|
|
441
|
+
imports.addImport({
|
|
442
|
+
from: Generator.toPackageName('lucide-react'),
|
|
443
|
+
items: [
|
|
444
|
+
Generator.toFunctionName('ListTree'),
|
|
445
|
+
Generator.toFunctionName('MessageSquareText'),
|
|
446
|
+
Generator.toFunctionName('ScrollText'),
|
|
447
|
+
Generator.toFunctionName('Sparkles'),
|
|
448
|
+
],
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
imports.addImport({
|
|
453
|
+
from: Generator.toPackageName('lucide-react'),
|
|
454
|
+
items: [
|
|
455
|
+
Generator.toFunctionName('ListTree'),
|
|
456
|
+
Generator.toFunctionName('ScrollText'),
|
|
457
|
+
Generator.toFunctionName('Sparkles'),
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function collectRelatedModels({ model, context, imports, }) {
|
|
462
|
+
const relatedModels = [];
|
|
463
|
+
for (const relatedModelName of model.relatedModelNames) {
|
|
464
|
+
const relatedModel = context.models.get(relatedModelName);
|
|
465
|
+
imports.add(relatedModel.itemsHook);
|
|
466
|
+
relatedModels.push({
|
|
467
|
+
model: relatedModel,
|
|
468
|
+
mapName: relatedModel.internalPluralName,
|
|
469
|
+
mapDefinition: `const {${relatedModel.itemsHook.map.name}: ${relatedModel.internalPluralName}} = ${relatedModel.itemsHook.name}()`,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return relatedModels;
|
|
473
|
+
}
|
|
474
|
+
function toRoleExpression(policy, userRolesVariableName) {
|
|
475
|
+
if (policy.allowAll) {
|
|
476
|
+
return 'true';
|
|
477
|
+
}
|
|
478
|
+
return `hasAnyRole(${userRolesVariableName}, ${JSON.stringify(policy.roles)})`;
|
|
479
|
+
}
|
|
480
|
+
function buildCommentCountState(showComments) {
|
|
481
|
+
if (!showComments) {
|
|
482
|
+
return '';
|
|
483
|
+
}
|
|
484
|
+
return 'const [commentCount, setCommentCount] = useState(0)';
|
|
485
|
+
}
|
|
486
|
+
function buildModelHookMutationFields(model) {
|
|
487
|
+
const fields = [];
|
|
488
|
+
if (model.itemsHook.updateField) {
|
|
489
|
+
fields.push(`${model.itemsHook.updateField.name}: updateFieldMutation,`);
|
|
490
|
+
}
|
|
491
|
+
if (model.itemsHook.updateFieldMany) {
|
|
492
|
+
fields.push(`${model.itemsHook.updateFieldMany.name}: updateFieldManyMutation,`);
|
|
493
|
+
}
|
|
494
|
+
if (model.itemsHook.deleteMany) {
|
|
495
|
+
fields.push(`${model.itemsHook.deleteMany.name}: deleteManyMutation,`);
|
|
496
|
+
}
|
|
497
|
+
return fields.join('\n ');
|
|
498
|
+
}
|
|
499
|
+
function buildCommentSidebarLogic({ showComments, model, }) {
|
|
500
|
+
if (!showComments) {
|
|
501
|
+
return '';
|
|
502
|
+
}
|
|
503
|
+
return `// Comment sidebar: compute filtered entity IDs and label map from table data
|
|
504
|
+
const commentFilteredIds = useMemo(() =>
|
|
505
|
+
filteredDataList?.map(item => String(item.id)) ?? [],
|
|
506
|
+
[filteredDataList]
|
|
507
|
+
)
|
|
508
|
+
const commentLabelMap = useMemo(() =>
|
|
509
|
+
Object.fromEntries((filteredDataList ?? []).map(item => [String(item.id), String(item.${model.labelField.name})])),
|
|
510
|
+
[filteredDataList]
|
|
511
|
+
)`;
|
|
512
|
+
}
|
|
513
|
+
function buildCommentSelectedEntityId(showComments) {
|
|
514
|
+
if (!showComments) {
|
|
515
|
+
return '';
|
|
516
|
+
}
|
|
517
|
+
return `
|
|
518
|
+
let commentSelectedEntityId: string | undefined
|
|
519
|
+
if (selectedEntityId) {
|
|
520
|
+
commentSelectedEntityId = String(selectedEntityId)
|
|
521
|
+
} else if (selectedRowIds.length === 1) {
|
|
522
|
+
commentSelectedEntityId = selectedRowIds[0]
|
|
523
|
+
}`;
|
|
524
|
+
}
|
|
525
|
+
function buildCellUpdateHandler(model) {
|
|
526
|
+
if (!model.itemsHook.updateField) {
|
|
527
|
+
return '';
|
|
528
|
+
}
|
|
529
|
+
const flushMutations = model.itemsHook.updateFieldMany
|
|
530
|
+
? `if (entry.ids.length === 1) {
|
|
531
|
+
void updateFieldMutation({ id: entry.ids[0], field: entry.field, value: entry.value } as Parameters<typeof updateFieldMutation>[0])
|
|
532
|
+
} else {
|
|
533
|
+
void updateFieldManyMutation({ ids: entry.ids, field: entry.field, value: entry.value } as Parameters<typeof updateFieldManyMutation>[0])
|
|
534
|
+
}`
|
|
535
|
+
: `for (const id of entry.ids) {
|
|
536
|
+
void updateFieldMutation({ id, field: entry.field, value: entry.value } as Parameters<typeof updateFieldMutation>[0])
|
|
537
|
+
}`;
|
|
538
|
+
const dependencies = model.itemsHook.updateFieldMany
|
|
539
|
+
? '[filteredDataList, updateFieldMutation, updateFieldManyMutation]'
|
|
540
|
+
: '[filteredDataList, updateFieldMutation]';
|
|
541
|
+
return `// Batch cell updates: collect updates within the same microtask and send as one updateFieldMany
|
|
406
542
|
const pendingUpdatesRef = useRef<Map<string, { field: string; value: unknown; ids: ${model.types.id.name}[] }> | null>(null)
|
|
407
543
|
|
|
408
544
|
const handleCellUpdate = useCallback((args: { rowIndex: number; columnId: string; value: unknown }) => {
|
|
@@ -418,11 +554,7 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
418
554
|
if (!pending) return
|
|
419
555
|
|
|
420
556
|
for (const entry of pending.values()) {
|
|
421
|
-
|
|
422
|
-
void updateFieldMutation({ id: entry.ids[0], field: entry.field, value: entry.value } as Parameters<typeof updateFieldMutation>[0])
|
|
423
|
-
} else {
|
|
424
|
-
void updateFieldManyMutation({ ids: entry.ids, field: entry.field, value: entry.value } as Parameters<typeof updateFieldManyMutation>[0])
|
|
425
|
-
}
|
|
557
|
+
${flushMutations}
|
|
426
558
|
}
|
|
427
559
|
})
|
|
428
560
|
}
|
|
@@ -435,81 +567,161 @@ export default function ${model.admin.page.pageComponent.name}() {
|
|
|
435
567
|
} else {
|
|
436
568
|
pendingUpdatesRef.current.set(key, { field: args.columnId, value: args.value, ids: [row.id] })
|
|
437
569
|
}
|
|
438
|
-
},
|
|
439
|
-
|
|
440
|
-
|
|
570
|
+
}, ${dependencies})`;
|
|
571
|
+
}
|
|
572
|
+
function buildRemoveRowsHandler(model) {
|
|
573
|
+
if (!model.itemsHook.deleteMany) {
|
|
574
|
+
return '';
|
|
575
|
+
}
|
|
576
|
+
return `function handleRemoveRows(rows: ${model.types.id.name}[]) {
|
|
441
577
|
void deleteManyMutation(rows)
|
|
442
|
-
}
|
|
443
|
-
|
|
578
|
+
}`;
|
|
579
|
+
}
|
|
580
|
+
function buildCommentSidebarTab({ showComments, model }) {
|
|
581
|
+
if (!showComments) {
|
|
582
|
+
return '';
|
|
583
|
+
}
|
|
584
|
+
return `<SidebarTab
|
|
585
|
+
side="right"
|
|
586
|
+
id="comment-content"
|
|
587
|
+
icon={MessageSquareText}
|
|
588
|
+
label="Comment"
|
|
589
|
+
order={0}
|
|
590
|
+
badge={{ label: String(commentCount) }}
|
|
591
|
+
>
|
|
592
|
+
<ModelCommentContent
|
|
593
|
+
model="${model.name}"
|
|
594
|
+
filteredIds={commentFilteredIds}
|
|
595
|
+
labelMap={commentLabelMap}
|
|
596
|
+
selectedEntityId={commentSelectedEntityId}
|
|
597
|
+
onCommentCountChange={setCommentCount}
|
|
598
|
+
/>
|
|
599
|
+
</SidebarTab>`;
|
|
600
|
+
}
|
|
601
|
+
function buildRelatedTableProps(relatedModels) {
|
|
602
|
+
return relatedModels.map((rm) => `${rm.model.table.propName}={${rm.mapName}}`).join('\n ');
|
|
603
|
+
}
|
|
604
|
+
function buildAddRowProp(model) {
|
|
605
|
+
if (model.isReadonly) {
|
|
606
|
+
return '';
|
|
607
|
+
}
|
|
608
|
+
return 'handleAddRow={() => setIsCreateModalOpen(true)}';
|
|
609
|
+
}
|
|
610
|
+
function buildCellUpdateProp(model) {
|
|
611
|
+
if (!model.itemsHook.updateField) {
|
|
612
|
+
return '';
|
|
613
|
+
}
|
|
614
|
+
return 'onCellUpdate={handleCellUpdate}';
|
|
615
|
+
}
|
|
616
|
+
function buildRemoveRowsProp(model) {
|
|
617
|
+
if (!model.itemsHook.deleteMany) {
|
|
618
|
+
return '';
|
|
619
|
+
}
|
|
620
|
+
return 'handleRemoveRows={handleRemoveRows}';
|
|
621
|
+
}
|
|
622
|
+
function resolveCreateModalContent(model, imports) {
|
|
623
|
+
if (!model.isReadonly) {
|
|
624
|
+
imports
|
|
625
|
+
.addType(model.types.id)
|
|
626
|
+
.addImport({ from: Generator.toPackageName('sonner'), items: [Generator.toFunctionName('toast')] })
|
|
627
|
+
.addImport({ from: model.forms.createModal.location, items: [model.forms.createModal.name] });
|
|
628
|
+
if (model.itemsHook.updateField) {
|
|
629
|
+
imports.addImport({ from: Generator.toPackageName('react'), items: [Generator.toFunctionName('useRef')] });
|
|
630
|
+
}
|
|
631
|
+
// In case the model does not provide any explicit label field, we generally use the id field as fallback.
|
|
632
|
+
// However, in case of creating a new item, we do not have an Id yet and hence cannot show it in success and error messages.
|
|
633
|
+
const hasLabelField = model.labelField.name !== model.idField.name;
|
|
634
|
+
const variableName = hasLabelField ? `created${model.name}` : '_';
|
|
635
|
+
const label = hasLabelField ? ` "\${${variableName}.${model.labelField.name}}"` : '';
|
|
636
|
+
return {
|
|
637
|
+
hook: ` const [isCreateModalOpen, setIsCreateModalOpen] = useState(false)`,
|
|
638
|
+
intentHook: `
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
if (typeof globalThis === 'undefined') {
|
|
641
|
+
return
|
|
642
|
+
}
|
|
444
643
|
|
|
445
|
-
|
|
446
|
-
${model.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
data={filteredDataList ?? []}
|
|
454
|
-
filters={filters}
|
|
455
|
-
onFilterChange={(filterKey, val) => {
|
|
456
|
-
const config = ${model.types.filter.config.name}[filterKey]
|
|
457
|
-
if (config.valueType === 'boolean') {
|
|
458
|
-
handleFilterChange(
|
|
459
|
-
config.filterKey,
|
|
460
|
-
(val as string[]).map((v) => v === 'true'),
|
|
461
|
-
)
|
|
462
|
-
} else {
|
|
463
|
-
handleFilterChange(config.filterKey, val)
|
|
464
|
-
}
|
|
465
|
-
}}
|
|
466
|
-
searchInput={searchInput}
|
|
467
|
-
onSearchInputChange={handleSearchInputChange}
|
|
468
|
-
sort={sortState}
|
|
469
|
-
onSortChange={setSortState}
|
|
470
|
-
${model.isReadonly
|
|
471
|
-
? ''
|
|
472
|
-
: `onCellUpdate={handleCellUpdate}
|
|
473
|
-
handleAddRow={() => setIsCreateModalOpen(true)}
|
|
474
|
-
handleRemoveRows={handleRemoveRows}`}
|
|
475
|
-
onCellChange={handleCellChange}
|
|
476
|
-
onRowSelectionChange={handleRowSelectionChange}
|
|
477
|
-
selectedKey={selectedKey}
|
|
478
|
-
keyField="${model.keyField.name}"
|
|
479
|
-
hasNextPage={hasNextPage}
|
|
480
|
-
fetchNextPage={fetchNextPage}
|
|
481
|
-
total={total}
|
|
482
|
-
model="${model._conjugated.camelCase}"
|
|
483
|
-
currentUserId={currentUserId}
|
|
484
|
-
isAdmin={isAdmin}
|
|
485
|
-
onFiltersReplace={setFilters}
|
|
486
|
-
onSortReplace={setSortState}
|
|
487
|
-
${relatedModels.map((rm) => `${rm.model.table.propName}={${rm.mapName}}`).join('\n ')}
|
|
488
|
-
/>
|
|
489
|
-
</SidebarInset>
|
|
644
|
+
const storageKey = 'pxl:create-intent-model'
|
|
645
|
+
const targetModel = '${model.name}'
|
|
646
|
+
const openFromIntent = (intentModel: unknown) => {
|
|
647
|
+
if (intentModel === targetModel) {
|
|
648
|
+
setIsCreateModalOpen(true)
|
|
649
|
+
globalThis.sessionStorage.removeItem(storageKey)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
490
652
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
653
|
+
openFromIntent(globalThis.sessionStorage.getItem(storageKey))
|
|
654
|
+
|
|
655
|
+
const onIntent = (event: Event) => {
|
|
656
|
+
const detail = (event as CustomEvent<{ model?: string }>).detail
|
|
657
|
+
openFromIntent(detail?.model)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
globalThis.addEventListener('pxl:create-intent', onIntent)
|
|
661
|
+
return () => {
|
|
662
|
+
globalThis.removeEventListener('pxl:create-intent', onIntent)
|
|
663
|
+
}
|
|
664
|
+
}, [])`,
|
|
665
|
+
modal: `
|
|
666
|
+
<${model.forms.createModal.name}
|
|
667
|
+
open={isCreateModalOpen}
|
|
668
|
+
onClose={() => setIsCreateModalOpen(false)}
|
|
669
|
+
onSuccess={(${variableName}) => {
|
|
670
|
+
toast.success(\`${model.userFriendlyName}${label} created successfully!\`)
|
|
671
|
+
setIsCreateModalOpen(false)
|
|
672
|
+
}}
|
|
673
|
+
onError={(${variableName}, error) => {
|
|
674
|
+
toast.error(\`Error creating ${model.userFriendlyName}${label}: \${String(error)}\`)
|
|
675
|
+
}}
|
|
676
|
+
/>`,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
return { hook: '', intentHook: '', modal: '' };
|
|
512
680
|
}
|
|
513
|
-
|
|
681
|
+
function resolveSchemaRuleSet(model, context) {
|
|
682
|
+
for (const [schemaName, schemaRules] of context.schema.schemaAuth.entries()) {
|
|
683
|
+
if (schemaName === model.databaseSchema) {
|
|
684
|
+
return (schemaRules ?? {});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return {};
|
|
688
|
+
}
|
|
689
|
+
function resolveEffectiveRuleSet(model, context) {
|
|
690
|
+
const schemaRuleSet = resolveSchemaRuleSet(model, context);
|
|
691
|
+
const modelRuleSet = (model.auth ?? {});
|
|
692
|
+
return { ...schemaRuleSet, ...modelRuleSet };
|
|
693
|
+
}
|
|
694
|
+
function resolveWritePolicy(model, context) {
|
|
695
|
+
const effectiveRules = resolveEffectiveRuleSet(model, context);
|
|
696
|
+
const roles = effectiveRules.write?.anyRole;
|
|
697
|
+
if (roles) {
|
|
698
|
+
return { allowAll: false, roles };
|
|
699
|
+
}
|
|
700
|
+
// If create/update/delete are configured independently, use a conservative write gate:
|
|
701
|
+
// users must satisfy all configured operation role sets to see generic write UI controls.
|
|
702
|
+
const operationRoleSets = [
|
|
703
|
+
effectiveRules.create?.anyRole,
|
|
704
|
+
effectiveRules.update?.anyRole,
|
|
705
|
+
effectiveRules.delete?.anyRole,
|
|
706
|
+
]
|
|
707
|
+
.filter((entry) => !!entry && entry.length > 0)
|
|
708
|
+
.map((entry) => Array.from(new Set(entry)));
|
|
709
|
+
if (operationRoleSets.length > 0) {
|
|
710
|
+
const [firstRoleSet, ...remainingRoleSets] = operationRoleSets;
|
|
711
|
+
if (!firstRoleSet) {
|
|
712
|
+
return { allowAll: !(context.schema.auth?.defaultDeny ?? true), roles: [] };
|
|
713
|
+
}
|
|
714
|
+
const intersection = remainingRoleSets.reduce((shared, current) => shared.filter((role) => current.includes(role)), firstRoleSet);
|
|
715
|
+
return { allowAll: false, roles: intersection };
|
|
716
|
+
}
|
|
717
|
+
return { allowAll: !(context.schema.auth?.defaultDeny ?? true), roles: [] };
|
|
718
|
+
}
|
|
719
|
+
function resolveAdminUiPolicy(model, context) {
|
|
720
|
+
const effectiveRules = resolveEffectiveRuleSet(model, context);
|
|
721
|
+
const roles = effectiveRules.adminUi?.visibleFor;
|
|
722
|
+
if (roles) {
|
|
723
|
+
return { allowAll: false, roles };
|
|
724
|
+
}
|
|
725
|
+
return { allowAll: !(context.schema.auth?.defaultDeny ?? true), roles: [] };
|
|
514
726
|
}
|
|
515
727
|
//# sourceMappingURL=model-admin-page.generator.js.map
|