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