@loom-framework/core 0.1.0-alpha.141 → 0.1.0-alpha.143

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.
Files changed (157) hide show
  1. package/dist/adapters/base.d.ts +31 -0
  2. package/dist/adapters/base.d.ts.map +1 -0
  3. package/dist/adapters/base.js +69 -0
  4. package/dist/adapters/base.js.map +1 -0
  5. package/dist/adapters/factory.d.ts +8 -0
  6. package/dist/adapters/factory.d.ts.map +1 -0
  7. package/dist/adapters/factory.js +25 -0
  8. package/dist/adapters/factory.js.map +1 -0
  9. package/dist/adapters/filesystem.d.ts +46 -0
  10. package/dist/adapters/filesystem.d.ts.map +1 -0
  11. package/dist/adapters/filesystem.js +321 -0
  12. package/dist/adapters/filesystem.js.map +1 -0
  13. package/dist/adapters/index.d.ts +10 -0
  14. package/dist/adapters/index.d.ts.map +1 -0
  15. package/dist/adapters/index.js +8 -0
  16. package/dist/adapters/index.js.map +1 -0
  17. package/dist/adapters/sqlite.d.ts +37 -0
  18. package/dist/adapters/sqlite.d.ts.map +1 -0
  19. package/dist/adapters/sqlite.js +264 -0
  20. package/dist/adapters/sqlite.js.map +1 -0
  21. package/dist/backend/ai/button-resolver.d.ts +1 -1
  22. package/dist/backend/ai/button-resolver.d.ts.map +1 -1
  23. package/dist/backend/ai/engine.d.ts +1 -1
  24. package/dist/backend/ai/engine.d.ts.map +1 -1
  25. package/dist/backend/ai/output-parser.d.ts +1 -1
  26. package/dist/backend/ai/output-parser.d.ts.map +1 -1
  27. package/dist/backend/ai/session-manager.d.ts +1 -1
  28. package/dist/backend/ai/session-manager.d.ts.map +1 -1
  29. package/dist/backend/index.d.ts +3 -5
  30. package/dist/backend/index.d.ts.map +1 -1
  31. package/dist/backend/index.js +3 -15
  32. package/dist/backend/index.js.map +1 -1
  33. package/dist/backend/observe/logger.d.ts +1 -4
  34. package/dist/backend/observe/logger.d.ts.map +1 -1
  35. package/dist/backend/observe/logger.js +3 -18
  36. package/dist/backend/observe/logger.js.map +1 -1
  37. package/dist/backend/routes/chat-sse.d.ts +17 -0
  38. package/dist/backend/routes/chat-sse.d.ts.map +1 -0
  39. package/dist/backend/routes/chat-sse.js +60 -0
  40. package/dist/backend/routes/chat-sse.js.map +1 -0
  41. package/dist/backend/routes/chat.d.ts +2 -6
  42. package/dist/backend/routes/chat.d.ts.map +1 -1
  43. package/dist/backend/routes/chat.js +4 -137
  44. package/dist/backend/routes/chat.js.map +1 -1
  45. package/dist/backend/routes/data.d.ts +2 -1
  46. package/dist/backend/routes/data.d.ts.map +1 -1
  47. package/dist/backend/routes/data.js +1 -1
  48. package/dist/backend/routes/data.js.map +1 -1
  49. package/dist/backend/routes/health.d.ts +1 -1
  50. package/dist/backend/routes/health.d.ts.map +1 -1
  51. package/dist/backend/routes/index.d.ts +2 -0
  52. package/dist/backend/routes/index.d.ts.map +1 -1
  53. package/dist/backend/routes/index.js +1 -0
  54. package/dist/backend/routes/index.js.map +1 -1
  55. package/dist/backend/routes/session-routes.d.ts +20 -0
  56. package/dist/backend/routes/session-routes.d.ts.map +1 -0
  57. package/dist/backend/routes/session-routes.js +94 -0
  58. package/dist/backend/routes/session-routes.js.map +1 -0
  59. package/dist/backend/routes/skill-archive.d.ts +13 -0
  60. package/dist/backend/routes/skill-archive.d.ts.map +1 -0
  61. package/dist/backend/routes/skill-archive.js +84 -0
  62. package/dist/backend/routes/skill-archive.js.map +1 -0
  63. package/dist/backend/routes/skill-parser.d.ts +41 -0
  64. package/dist/backend/routes/skill-parser.d.ts.map +1 -0
  65. package/dist/backend/routes/skill-parser.js +184 -0
  66. package/dist/backend/routes/skill-parser.js.map +1 -0
  67. package/dist/backend/routes/skill-service.d.ts +113 -0
  68. package/dist/backend/routes/skill-service.d.ts.map +1 -0
  69. package/dist/backend/routes/skill-service.js +265 -0
  70. package/dist/backend/routes/skill-service.js.map +1 -0
  71. package/dist/backend/routes/skills.d.ts +3 -0
  72. package/dist/backend/routes/skills.d.ts.map +1 -1
  73. package/dist/backend/routes/skills.js +33 -456
  74. package/dist/backend/routes/skills.js.map +1 -1
  75. package/dist/backend/services/skill-archive.d.ts +13 -0
  76. package/dist/backend/services/skill-archive.d.ts.map +1 -0
  77. package/dist/backend/services/skill-archive.js +84 -0
  78. package/dist/backend/services/skill-archive.js.map +1 -0
  79. package/dist/backend/services/skill-parser.d.ts +41 -0
  80. package/dist/backend/services/skill-parser.d.ts.map +1 -0
  81. package/dist/backend/services/skill-parser.js +184 -0
  82. package/dist/backend/services/skill-parser.js.map +1 -0
  83. package/dist/backend/services/skill-service.d.ts +113 -0
  84. package/dist/backend/services/skill-service.d.ts.map +1 -0
  85. package/dist/backend/services/skill-service.js +265 -0
  86. package/dist/backend/services/skill-service.js.map +1 -0
  87. package/dist/cli/commands/data.d.ts.map +1 -1
  88. package/dist/cli/commands/data.js +3 -1
  89. package/dist/cli/commands/data.js.map +1 -1
  90. package/dist/cli/commands/dev.d.ts.map +1 -1
  91. package/dist/cli/commands/dev.js +2 -1
  92. package/dist/cli/commands/dev.js.map +1 -1
  93. package/dist/cli/commands/generate-capabilities.d.ts.map +1 -1
  94. package/dist/cli/commands/generate-capabilities.js +2 -1
  95. package/dist/cli/commands/generate-capabilities.js.map +1 -1
  96. package/dist/cli/commands/generate-dashboard.d.ts.map +1 -1
  97. package/dist/cli/commands/generate-dashboard.js +3 -452
  98. package/dist/cli/commands/generate-dashboard.js.map +1 -1
  99. package/dist/cli/commands/generate-page.d.ts.map +1 -1
  100. package/dist/cli/commands/generate-page.js +6 -436
  101. package/dist/cli/commands/generate-page.js.map +1 -1
  102. package/dist/cli/commands/serve.d.ts +12 -0
  103. package/dist/cli/commands/serve.d.ts.map +1 -0
  104. package/dist/cli/commands/serve.js +43 -0
  105. package/dist/cli/commands/serve.js.map +1 -0
  106. package/dist/cli/framework.d.ts +28 -0
  107. package/dist/cli/framework.d.ts.map +1 -0
  108. package/dist/cli/framework.js +29 -0
  109. package/dist/cli/framework.js.map +1 -0
  110. package/dist/cli/helpers/app-tsx-wiring.d.ts.map +1 -1
  111. package/dist/cli/helpers/app-tsx-wiring.js +39 -24
  112. package/dist/cli/helpers/app-tsx-wiring.js.map +1 -1
  113. package/dist/cli/helpers/column-template.d.ts +3 -1
  114. package/dist/cli/helpers/column-template.d.ts.map +1 -1
  115. package/dist/cli/helpers/column-template.js +1 -1
  116. package/dist/cli/helpers/column-template.js.map +1 -1
  117. package/dist/cli/helpers/duration.d.ts +2 -2
  118. package/dist/cli/helpers/duration.d.ts.map +1 -1
  119. package/dist/cli/helpers/duration.js +2 -16
  120. package/dist/cli/helpers/duration.js.map +1 -1
  121. package/dist/cli/helpers/field-template.d.ts +1 -1
  122. package/dist/cli/helpers/field-template.d.ts.map +1 -1
  123. package/dist/cli/helpers/field-template.js +1 -1
  124. package/dist/cli/helpers/field-template.js.map +1 -1
  125. package/dist/cli/helpers/i18n-template.d.ts +13 -0
  126. package/dist/cli/helpers/i18n-template.d.ts.map +1 -0
  127. package/dist/cli/helpers/i18n-template.js +48 -0
  128. package/dist/cli/helpers/i18n-template.js.map +1 -0
  129. package/dist/cli/templates/crud-page.d.ts +14 -0
  130. package/dist/cli/templates/crud-page.d.ts.map +1 -0
  131. package/dist/cli/templates/crud-page.js +405 -0
  132. package/dist/cli/templates/crud-page.js.map +1 -0
  133. package/dist/cli/templates/dashboard-page.d.ts +11 -0
  134. package/dist/cli/templates/dashboard-page.d.ts.map +1 -0
  135. package/dist/cli/templates/dashboard-page.js +456 -0
  136. package/dist/cli/templates/dashboard-page.js.map +1 -0
  137. package/dist/cli/templates/index.d.ts +2 -0
  138. package/dist/cli/templates/index.d.ts.map +1 -1
  139. package/dist/cli/templates/index.js +2 -0
  140. package/dist/cli/templates/index.js.map +1 -1
  141. package/dist/config.d.ts +22 -22
  142. package/dist/dashboard-config.d.ts +10 -10
  143. package/dist/index.d.ts +8 -8
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +5 -5
  146. package/dist/index.js.map +1 -1
  147. package/dist/types/chat.d.ts +33 -32
  148. package/dist/types/chat.d.ts.map +1 -1
  149. package/dist/types/chat.js +4 -1
  150. package/dist/types/chat.js.map +1 -1
  151. package/dist/types/index.d.ts +1 -1
  152. package/dist/types/index.d.ts.map +1 -1
  153. package/dist/utils/duration.d.ts +8 -0
  154. package/dist/utils/duration.d.ts.map +1 -0
  155. package/dist/utils/duration.js +22 -0
  156. package/dist/utils/duration.js.map +1 -0
  157. package/package.json +2 -2
@@ -0,0 +1,405 @@
1
+ /**
2
+ * CRUD page template generator
3
+ *
4
+ * Pure function: takes model schema + options, returns TSX string.
5
+ * No I/O side effects — file writing is done by the command.
6
+ */
7
+ import { toPascalCase } from '../helpers/naming.js';
8
+ import { fieldToFormItem } from '../helpers/field-template.js';
9
+ import { generateTagColors, generateColumns } from '../helpers/column-template.js';
10
+ import { generateRegisterMessages } from '../helpers/i18n-template.js';
11
+ /** Generate CRUD page template from model schema */
12
+ export function crudPageTemplate(model, aiButtons, relations) {
13
+ const pascalName = toPascalCase(model.name);
14
+ const recordType = `${pascalName}Record`;
15
+ const modelRelations = relations?.filter(r => r.from === model.name) ?? [];
16
+ const hasRelations = modelRelations.length > 0;
17
+ // Resolve CRUD config (all default to true)
18
+ const crud = model.crud ?? {};
19
+ const hasExport = crud.export !== false;
20
+ const hasImport = crud.import !== false;
21
+ const hasDeleteWithUndo = crud.deleteWithUndo !== false;
22
+ // Detect field types for conditional logic
23
+ const dateFields = model.fields.filter(f => f.type === 'date').map(f => f.name);
24
+ const hasDateFields = dateFields.length > 0;
25
+ // Build TypeScript interface from fields
26
+ const fieldDefs = model.fields.map((f) => {
27
+ const tsType = f.type === 'string[]' ? 'string[]'
28
+ : f.type === 'number[]' ? 'number[]'
29
+ : f.type === 'json' ? 'Record<string, unknown>'
30
+ : f.type === 'number' ? 'number'
31
+ : f.type === 'boolean' ? 'boolean'
32
+ : f.type === 'date' ? 'string'
33
+ : 'string';
34
+ return ` ${f.name}${f.required ? '' : '?'}: ${tsType};`;
35
+ }).join('\n');
36
+ const columns = generateColumns(model.fields, model.name, aiButtons, true);
37
+ const formItems = model.fields
38
+ .filter((f) => f.name !== 'id') // id is auto-generated
39
+ .map((f) => fieldToFormItem(f, model.name, true))
40
+ .join('\n');
41
+ // Generate handleEdit with date conversion
42
+ const handleEditBody = hasDateFields
43
+ ? `const handleEdit = (record: ${recordType}) => {
44
+ setEditingRecord(record);
45
+ const formValues = { ...record };
46
+ ${dateFields.map(f => ` if (formValues.${f}) formValues.${f} = dayjs(formValues.${f}) as any;`).join('\n')}
47
+ form.setFieldsValue(formValues);
48
+ setModalOpen(true);
49
+ };`
50
+ : `const handleEdit = (record: ${recordType}) => {
51
+ setEditingRecord(record);
52
+ form.setFieldsValue(record);
53
+ setModalOpen(true);
54
+ };`;
55
+ // Generate handleSave with date serialization
56
+ const handleSaveBody = hasDateFields
57
+ ? `const handleSave = async () => {
58
+ try {
59
+ const values = await form.validateFields();
60
+ ${dateFields.map(f => ` if (values.${f}) values.${f} = values.${f}.format('YYYY-MM-DD');`).join('\n')}
61
+ if (editingRecord?.id) {
62
+ await update(editingRecord.id, values);
63
+ message.success(t('crud.updateSuccess'));
64
+ } else {
65
+ await create(values);
66
+ message.success(t('crud.createSuccess'));
67
+ }
68
+ setModalOpen(false);
69
+ fetchData();
70
+ } catch {
71
+ // form validation failed
72
+ }
73
+ };`
74
+ : `const handleSave = async () => {
75
+ try {
76
+ const values = await form.validateFields();
77
+ if (editingRecord?.id) {
78
+ await update(editingRecord.id, values);
79
+ message.success(t('crud.updateSuccess'));
80
+ } else {
81
+ await create(values);
82
+ message.success(t('crud.createSuccess'));
83
+ }
84
+ setModalOpen(false);
85
+ fetchData();
86
+ } catch {
87
+ // form validation failed
88
+ }
89
+ };`;
90
+ const dayjsImport = hasDateFields ? `\nimport dayjs from 'dayjs';` : '';
91
+ // With useSchema, AI buttons come dynamically, so always include AI imports
92
+ const hasAIButtons = true; // always true with useSchema — AI buttons loaded at runtime
93
+ // Build imports — useSchema + filterOptions/selectOptions always included
94
+ const antdExtra = ', Dropdown';
95
+ const iconsExtra = ', ThunderboltOutlined';
96
+ const extraIcons = ', EditOutlined, DeleteOutlined';
97
+ const loomExtra = ', AIContext, useLocale, registerMessages, enumLabel, useAppShell';
98
+ const reactExtra = ', useContext, useEffect';
99
+ // Conditional imports based on CRUD config
100
+ const antdUpload = hasImport ? ', Upload' : '';
101
+ const iconsDownload = hasExport ? ', DownloadOutlined' : '';
102
+ const iconsUpload = hasImport ? ', UploadOutlined' : '';
103
+ const useDataExtra = `${hasExport ? ', exportCsv' : ''}${hasImport ? ', importCsv' : ''}`;
104
+ const i18nRegistration = generateRegisterMessages(model, relations);
105
+ const titleKey = `model.${model.name}.title`;
106
+ // Import modal template
107
+ const importModalTemplate = hasImport ? `
108
+ <Modal
109
+ title={t('crud.importTitle')}
110
+ open={importModalOpen}
111
+ onCancel={() => setImportModalOpen(false)}
112
+ footer={null}
113
+ width={480}
114
+ >
115
+ <Upload.Dragger
116
+ accept=".csv"
117
+ showUploadList={false}
118
+ beforeUpload={(file) => {
119
+ const reader = new FileReader();
120
+ reader.onload = async (e) => {
121
+ const text = e.target?.result as string;
122
+ try {
123
+ const result = await importCsv(text);
124
+ message.success(\`\${t('crud.importSuccess')}: \${result.imported}\`);
125
+ if (result.errors.length > 0) {
126
+ message.warning(result.errors.join('; '));
127
+ }
128
+ fetchData();
129
+ } catch {
130
+ message.error(t('crud.importFailed'));
131
+ }
132
+ setImportModalOpen(false);
133
+ };
134
+ reader.readAsText(file);
135
+ return false;
136
+ }}
137
+ >
138
+ <p className="ant-upload-drag-icon"><UploadOutlined /></p>
139
+ <p>{t('crud.importDrag')}</p>
140
+ </Upload.Dragger>
141
+ <div style={{ marginTop: 12, textAlign: 'center' }}>
142
+ <Button size="small" type="link" onClick={handleDownloadTemplate}>{t('crud.downloadTemplate')}</Button>
143
+ </div>
144
+ </Modal>` : '';
145
+ // Header action buttons
146
+ const headerButtons = [
147
+ hasExport ? `<Button icon={<DownloadOutlined />} onClick={() => exportCsv()}>{t('crud.export')}</Button>` : '',
148
+ hasImport ? `<Button icon={<UploadOutlined />} onClick={() => setImportModalOpen(true)}>{t('crud.import')}</Button>` : '',
149
+ `<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
150
+ {t('crud.add')}
151
+ </Button>`,
152
+ ].filter(Boolean).join('\n ');
153
+ return `import React, { useState, useCallback${reactExtra} } from 'react';
154
+ import { Table, Button, Modal, Form, Input, InputNumber, Select, Switch, DatePicker, Space, message, Tag, Card, Flex, Tooltip, Typography${antdUpload}, Drawer, Breadcrumb, type TableProps${antdExtra} } from 'antd';
155
+ import { PlusOutlined${iconsDownload}${iconsUpload}, EyeOutlined, HomeOutlined${iconsExtra}${extraIcons} } from '@ant-design/icons';
156
+ import { useData, useSchema, filterOptions, selectOptions${loomExtra} } from '@loom-framework/frontend-antd';${dayjsImport}${hasRelations ? "\nimport { RelationPanel } from '@loom-framework/frontend-antd';" : ''}
157
+
158
+ ${i18nRegistration}
159
+
160
+ interface ${recordType} {
161
+ id: string;
162
+ ${fieldDefs}
163
+ }
164
+
165
+ const modelFields: { name: string; type: string }[] = [
166
+ ${model.fields.map(f => ` { name: '${f.name}', type: '${f.type}' },`).join('\n')}
167
+ ];
168
+
169
+ export function ${pascalName}Page(): React.ReactElement {
170
+ const { t } = useLocale();
171
+ const { breadcrumbs, onNavClick } = useAppShell();
172
+ const { list, total, create, update, remove, loading, refresh${useDataExtra} } = useData<${recordType}>('${model.name}', { skipInitialFetch: true });
173
+ const { schema } = useSchema('${model.name}');
174
+ const [form] = Form.useForm();
175
+ const [modalOpen, setModalOpen] = useState(false);
176
+ const [editingRecord, setEditingRecord] = useState<${recordType} | null>(null);
177
+ const [tableFilters, setTableFilters] = useState<Record<string, any>>({});
178
+ const [tableSorter, setTableSorter] = useState<{ field: string; order: 'ascend' | 'descend' | null }>({ field: '', order: null });
179
+ const [currentPage, setCurrentPage] = useState(1);
180
+ const [pageSize, setPageSize] = useState(10);
181
+ const [importModalOpen, setImportModalOpen] = useState(false);${hasImport ? '' : ' // only used when import is enabled'}
182
+ const [detailRecord, setDetailRecord] = useState<${recordType} | null>(null);
183
+
184
+ const handleDownloadTemplate = () => {
185
+ const headers = ['${model.fields.filter(f => f.name !== 'id').map(f => f.name).join("','")}'];
186
+ const csv = headers.join(',');
187
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
188
+ const url = URL.createObjectURL(blob);
189
+ const a = document.createElement('a');
190
+ a.href = url;
191
+ a.download = '${model.name}_template.csv';
192
+ a.click();
193
+ URL.revokeObjectURL(url);
194
+ };
195
+
196
+ const fetchData = useCallback((filters?: Record<string, (string | number | boolean | null)[] | null>, sorter?: { field: string; order: 'ascend' | 'descend' | null }, page?: number, size?: number) => {
197
+ const currentFilters = filters ?? tableFilters;
198
+ const currentSorter = sorter ?? tableSorter;
199
+ const fetchPage = page ?? currentPage;
200
+ const fetchSize = size ?? pageSize;
201
+ const queryParams: any = {};
202
+ // Build filter from antd column filter values
203
+ const apiFilter: Record<string, unknown> = {};
204
+ for (const [key, val] of Object.entries(currentFilters)) {
205
+ if (val && val.length > 0) {
206
+ const field = modelFields.find(f => f.name === key);
207
+ if (field?.type === 'date') {
208
+ // Date range: val is [dayjs, dayjs] stored in filter
209
+ apiFilter[key] = { __range: val.map((v: any) => v?.format?.('YYYY-MM-DD') || v) };
210
+ } else if (val.length === 1) {
211
+ apiFilter[key] = val[0];
212
+ } else {
213
+ // Multiple filter values for a single column (e.g. subject in [数学, 英语])
214
+ apiFilter[key] = val;
215
+ }
216
+ }
217
+ }
218
+ if (Object.keys(apiFilter).length > 0) queryParams.filter = apiFilter;
219
+ // Build sort
220
+ if (currentSorter.field && currentSorter.order) {
221
+ queryParams.sort = currentSorter.field;
222
+ queryParams.sortOrder = currentSorter.order === 'descend' ? 'desc' : 'asc';
223
+ }
224
+ // Server-side pagination
225
+ queryParams.limit = fetchSize;
226
+ queryParams.offset = (fetchPage - 1) * fetchSize;
227
+ refresh(queryParams);
228
+ }, [refresh, tableFilters, tableSorter, currentPage, pageSize]);
229
+
230
+ // Initial fetch with pagination (useEffect with empty deps, fetchData is stable via useCallback)
231
+ useEffect(() => {
232
+ fetchData(undefined, undefined, 1, 10);
233
+ }, []);
234
+
235
+ const handleAdd = () => {
236
+ setEditingRecord(null);
237
+ form.resetFields();
238
+ setModalOpen(true);
239
+ };
240
+
241
+ ${handleEditBody}
242
+
243
+ const handleDelete = async (record: ${recordType}) => {
244
+ ${hasDeleteWithUndo ? `const snapshot = { ...record };
245
+ try {
246
+ await remove(record.id);
247
+ fetchData();
248
+ message.info({
249
+ content: t('crud.deleted'),
250
+ duration: 5,
251
+ btn: <Button size="small" type="link" onClick={() => { create(snapshot); fetchData(); message.destroy(); }}>{t('common.undo')}</Button>,
252
+ });
253
+ } catch {
254
+ message.error(t('crud.deleteFailed'));
255
+ }` : `Modal.confirm({
256
+ title: t('crud.deleteConfirm'),
257
+ onOk: async () => {
258
+ try {
259
+ await remove(record.id);
260
+ message.success(t('crud.deleted'));
261
+ fetchData();
262
+ } catch {
263
+ message.error(t('crud.deleteFailed'));
264
+ }
265
+ },
266
+ });`}
267
+ };
268
+
269
+ ${handleSaveBody}
270
+
271
+ const ai = useContext(AIContext);
272
+
273
+ // AI buttons from schema (dynamic via useSchema)
274
+ const aiButtonMenuItems = (record: ${recordType}) => {
275
+ const ctx: Record<string, string | string[] | number | boolean> = {};
276
+ for (const [k, v] of Object.entries(record)) {
277
+ if (Array.isArray(v) && v.length === 0) continue;
278
+ // null/undefined → "" so {{var}} resolves to empty string instead of "Missing"
279
+ ctx[k] = (v ?? '') as string | string[] | number | boolean;
280
+ }
281
+ const buttons = schema?.aiButtons ?? [];
282
+ return buttons.map(btn => ({
283
+ key: btn.id,
284
+ label: btn.label,
285
+ onClick: () => ai?.triggerAI({ buttonId: btn.id, label: btn.label, prompt: btn.prompt, context: ctx }),
286
+ }));
287
+ };
288
+
289
+ const columns: TableProps<${recordType}>['columns'] = [
290
+ ${columns}
291
+ ];${hasRelations ? `
292
+ // Add detail button to action column for relations
293
+ if (columns.length > 0) {
294
+ const lastCol = columns[columns.length - 1] as any;
295
+ if (lastCol?.render) {
296
+ const originalRender = lastCol.render;
297
+ lastCol.render = (_: unknown, record: ${recordType}) => {
298
+ const original = originalRender(_, record);
299
+ return (
300
+ <Space>
301
+ {original}
302
+ <Tooltip title={t('crud.detail')}><Button type="text" size="small" icon={<EyeOutlined />} onClick={() => setDetailRecord(record)} /></Tooltip>
303
+ </Space>
304
+ );
305
+ };
306
+ }
307
+ lastCol.width = ${hasAIButtons && hasRelations ? 160 : hasAIButtons ? 120 : hasRelations ? 120 : 90};
308
+ }` : ''}
309
+ ${generateTagColors(model.fields)}
310
+ const handleTableChange: TableProps<${recordType}>['onChange'] = (pagination, filters, sorter) => {
311
+ const newFilters = filters as Record<string, any>;
312
+ const newSorter = !Array.isArray(sorter) && sorter.field
313
+ ? { field: sorter.field as string, order: sorter.order as 'ascend' | 'descend' | null }
314
+ : { field: '', order: null };
315
+ const newPage = pagination.current || 1;
316
+ const newSize = pagination.pageSize || 10;
317
+ setTableFilters(newFilters);
318
+ setTableSorter(newSorter);
319
+ setCurrentPage(newPage);
320
+ setPageSize(newSize);
321
+ fetchData(newFilters, newSorter, newPage, newSize);
322
+ };
323
+
324
+ return (
325
+ <>
326
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
327
+ <Flex justify="space-between" align="center" style={{ marginBottom: 12 }}>
328
+ <Breadcrumb items={[{ title: <HomeOutlined onClick={() => onNavClick?.('')} style={{ cursor: 'pointer' }} /> }, ...(breadcrumbs || []).map(b => ({ title: b.path ? <a onClick={() => onNavClick?.(b.path!)}>{b.title}</a> : b.title }))] /**/} />
329
+ <Space>
330
+ ${headerButtons}
331
+ </Space>
332
+ </Flex>
333
+
334
+ <Table
335
+ columns={columns}
336
+ dataSource={list}
337
+ rowKey="id"
338
+ loading={loading}
339
+ size="middle"
340
+ scroll={{ x: 'max-content', y: 'calc(100vh - 280px)' }}
341
+ onChange={handleTableChange}
342
+ pagination={{ current: currentPage, pageSize, total, showSizeChanger: true, showTotal: (count) => t('crud.total', { total: count }) }}
343
+ />${hasRelations ? `
344
+ <Drawer
345
+ title={detailRecord ? \`\${t('crud.detail')}: \${detailRecord.id}\` : ''}
346
+ open={!!detailRecord}
347
+ onClose={() => setDetailRecord(null)}
348
+ width={640}
349
+ >
350
+ {detailRecord && (
351
+ <RelationPanel
352
+ model='${model.name}'
353
+ recordId={detailRecord.id}
354
+ relations={[
355
+ ${modelRelations.map(r => {
356
+ const labelKey = `model.${model.name}.relation.${r.name}`;
357
+ return `{ name: '${r.name}', label: t('${labelKey}'), from: '${r.from}', to: '${r.to}', foreignKey: '${r.foreignKey}', type: '${r.type}'${r.through ? `, through: '${r.through}'` : ''} }`;
358
+ }).join(',\n ')}
359
+ ]}
360
+ />
361
+ )}
362
+ </Drawer>` : ''}
363
+ </div>
364
+
365
+ <Modal
366
+ title={editingRecord ? \`\${t('crud.edit')}\${t('${titleKey}')}\` : \`\${t('crud.add')}\${t('${titleKey}')}\`}
367
+ open={modalOpen}
368
+ onOk={handleSave}
369
+ onCancel={() => setModalOpen(false)}
370
+ destroyOnHidden
371
+ width={640}
372
+ >
373
+ <Form form={form} layout="vertical">
374
+ ${formItems}
375
+ </Form>
376
+ </Modal>
377
+ ${importModalTemplate}
378
+ </>
379
+ );
380
+ }
381
+
382
+ export default ${pascalName}Page;
383
+ `;
384
+ }
385
+ /** Generate minimal skeleton page template (no model) */
386
+ export function skeletonPageTemplate(pascalName) {
387
+ return `import React from 'react';
388
+
389
+ export interface ${pascalName}Props {
390
+ // TODO: Define page props
391
+ }
392
+
393
+ export function ${pascalName}(props: ${pascalName}Props): React.ReactElement {
394
+ return (
395
+ <div className="${pascalName.toLowerCase()}-page">
396
+ <h1>${pascalName}</h1>
397
+ <p>TODO: Implement this page.</p>
398
+ </div>
399
+ );
400
+ }
401
+
402
+ export default ${pascalName};
403
+ `;
404
+ }
405
+ //# sourceMappingURL=crud-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crud-page.js","sourceRoot":"","sources":["../../../src/cli/templates/crud-page.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Dashboard page template generator
3
+ *
4
+ * Pure function: takes dashboard config + model schemas, returns TSX string.
5
+ * No I/O side effects — file writing is done by the command.
6
+ */
7
+ import type { DashboardConfig } from '../../types/dashboard.js';
8
+ import type { ModelSchema } from '../../types/model.js';
9
+ /** Generate the full Dashboard page TSX */
10
+ export declare function dashboardPageTemplate(dashboard: DashboardConfig, models: ModelSchema[]): string;
11
+ //# sourceMappingURL=dashboard-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-page.d.ts","sourceRoot":"","sources":["../../../src/cli/templates/dashboard-page.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,0BAA0B,CAAC;AACjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAiSxD,2CAA2C;AAC3C,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,eAAe,EAC1B,MAAM,EAAE,WAAW,EAAE,GACpB,MAAM,CAoMR"}