@ramme-io/create-app 1.1.9 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/template/pkg.json +16 -13
- package/template/src/App.tsx +17 -11
- package/template/src/blocks/SmartTable.tsx +224 -0
- package/template/src/components/AutoForm.tsx +128 -0
- package/template/src/components/DynamicBlock.tsx +37 -31
- package/template/src/components/dev/GhostOverlay.tsx +26 -59
- package/template/src/config/app.manifest.ts +48 -48
- package/template/src/core/component-registry.tsx +21 -41
- package/template/src/core/data-seeder.ts +35 -0
- package/template/src/data/mockData.ts +163 -34
- package/template/src/generated/hooks.ts +64 -58
- package/template/src/hooks/useDataQuery.ts +84 -0
- package/template/src/hooks/useSignal.ts +43 -33
- package/template/src/hooks/useWorkflowEngine.ts +123 -0
- package/template/src/pages/Dashboard.tsx +43 -90
- package/template/src/pages/DynamicPage.tsx +54 -22
- package/template/src/pages/Welcome.tsx +162 -0
- package/template/src/templates/dashboard/DashboardLayout.tsx +2 -0
- package/template/src/templates/dashboard/dashboard.sitemap.ts +33 -72
- package/template/src/types/schema.ts +117 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramme-io/create-app",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-ramme-app": "./index.js"
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
"build": "cd template && tsc && vite build",
|
|
15
15
|
"preview": "cd template && vite preview",
|
|
16
16
|
"pack-tarball": "npm pack",
|
|
17
|
-
"bundle:ai": "repomix --ignore '**/*.lock,**/dist/**,**/template/dist/**,**/template/node_modules/**' --output 'ramme-starter-context.txt'"
|
|
17
|
+
"bundle:ai": "repomix --ignore '**/*.lock,**/dist/**,**/template/dist/**,**/template/node_modules/**' --output 'ramme-starter-context.txt'",
|
|
18
|
+
"zip:builder": "cd template && zip -r ../base.zip . -x \"node_modules/*\" \"dist/*\" \".DS_Store\" && mv ../base.zip ../../ramme-app-builder/public/base.zip && echo '✅ base.zip updated in Builder!'"
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@ramme-io/ui": "^1.1.
|
|
21
|
+
"@ramme-io/ui": "^1.1.5",
|
|
21
22
|
"ag-grid-community": "^34.1.2",
|
|
22
23
|
"ag-grid-enterprise": "^34.1.2",
|
|
23
24
|
"ag-grid-react": "^34.1.2",
|
package/template/pkg.json
CHANGED
|
@@ -9,28 +9,31 @@
|
|
|
9
9
|
"preview": "vite preview"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@ramme-io/ui": "^1.1.
|
|
12
|
+
"@ramme-io/ui": "^1.1.5",
|
|
13
13
|
"mqtt": "^5.3.5",
|
|
14
14
|
"zustand": "^4.5.0",
|
|
15
15
|
"ag-grid-community": "^31.3.1",
|
|
16
|
-
"ag-grid-enterprise": "^31.3.1",
|
|
17
|
-
"ag-grid-react": "^31.3.1",
|
|
18
|
-
"framer-motion": "^
|
|
19
|
-
"fs-extra": "^11.
|
|
16
|
+
"ag-grid-enterprise": "^31.3.1",
|
|
17
|
+
"ag-grid-react": "^31.3.1",
|
|
18
|
+
"framer-motion": "^11.0.0",
|
|
19
|
+
"fs-extra": "^11.2.0",
|
|
20
20
|
"react": "^18.2.0",
|
|
21
21
|
"react-dom": "^18.2.0",
|
|
22
|
-
"react-
|
|
23
|
-
"react-
|
|
22
|
+
"react-router-dom": "^6.22.0",
|
|
23
|
+
"react-resizable-panels": "^2.0.9",
|
|
24
|
+
"ai": "^3.0.0",
|
|
25
|
+
"@ai-sdk/google": "^0.0.10",
|
|
26
|
+
"zod": "^3.22.0"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^20.
|
|
27
|
-
"@types/react": "^18.2.
|
|
28
|
-
"@types/react-dom": "^18.2.
|
|
29
|
-
"@vitejs/plugin-react": "^4.2.
|
|
29
|
+
"@types/node": "^20.11.0",
|
|
30
|
+
"@types/react": "^18.2.0",
|
|
31
|
+
"@types/react-dom": "^18.2.0",
|
|
32
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
30
33
|
"autoprefixer": "^10.4.19",
|
|
31
34
|
"postcss": "^8.4.38",
|
|
32
|
-
"tailwindcss": "^3.4.
|
|
33
|
-
"typescript": "^5.2.
|
|
35
|
+
"tailwindcss": "^3.4.3",
|
|
36
|
+
"typescript": "^5.2.0",
|
|
34
37
|
"vite": "^5.2.0"
|
|
35
38
|
}
|
|
36
39
|
}
|
package/template/src/App.tsx
CHANGED
|
@@ -1,42 +1,48 @@
|
|
|
1
1
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
2
2
|
import { generateRoutes } from './core/route-generator';
|
|
3
|
+
import { useEffect } from 'react';
|
|
3
4
|
|
|
4
5
|
// --- 1. IMPORT ALL THREE TEMPLATES ---
|
|
5
6
|
import DashboardLayout from './templates/dashboard/DashboardLayout';
|
|
6
|
-
import { dashboardSitemap
|
|
7
|
+
import { dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
|
|
7
8
|
import DocsLayout from './templates/docs/DocsLayout';
|
|
8
|
-
import { docsSitemap
|
|
9
|
-
import SettingsLayout from './templates/settings/SettingsLayout';
|
|
10
|
-
import { settingsSitemap
|
|
9
|
+
import { docsSitemap } from './templates/docs/docs.sitemap';
|
|
10
|
+
import SettingsLayout from './templates/settings/SettingsLayout';
|
|
11
|
+
import { settingsSitemap } from './templates/settings/settings.sitemap';
|
|
11
12
|
|
|
12
13
|
// Other Imports
|
|
13
14
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
14
15
|
import LoginPage from './pages/LoginPage';
|
|
15
16
|
import NotFound from './pages/styleguide/NotFound';
|
|
16
17
|
|
|
18
|
+
// --- 2. IMPORT THE SEEDER ---
|
|
19
|
+
import { initializeDataLake } from './core/data-seeder';
|
|
20
|
+
|
|
17
21
|
function App() {
|
|
22
|
+
|
|
23
|
+
// ✅ 3. TRIGGER DATA SEEDING ON MOUNT
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
initializeDataLake();
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
18
28
|
return (
|
|
19
29
|
<Routes>
|
|
20
30
|
<Route path="/login" element={<LoginPage />} />
|
|
21
31
|
<Route element={<ProtectedRoute />}>
|
|
22
|
-
{/*
|
|
23
|
-
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
32
|
+
{/* ✅ FIX: Redirect root to the new Welcome page */}
|
|
33
|
+
<Route path="/" element={<Navigate to="/dashboard/welcome" replace />} />
|
|
24
34
|
|
|
25
35
|
{/* Dashboard Template Routes */}
|
|
26
36
|
<Route path="/dashboard/*" element={<DashboardLayout />}>
|
|
27
|
-
{' '}
|
|
28
|
-
{/* <-- Added /* */}
|
|
29
37
|
{generateRoutes(dashboardSitemap)}
|
|
30
38
|
</Route>
|
|
31
39
|
|
|
32
40
|
{/* Docs Template Routes */}
|
|
33
41
|
<Route path="/docs/*" element={<DocsLayout />}>
|
|
34
|
-
{' '}
|
|
35
|
-
{/* <-- Added /* */}
|
|
36
42
|
{generateRoutes(docsSitemap)}
|
|
37
43
|
</Route>
|
|
38
44
|
|
|
39
|
-
{/*
|
|
45
|
+
{/* Settings Layout Route */}
|
|
40
46
|
<Route path="/settings/*" element={<SettingsLayout />}>
|
|
41
47
|
{generateRoutes(settingsSitemap)}
|
|
42
48
|
</Route>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DataTable,
|
|
4
|
+
Button,
|
|
5
|
+
Icon,
|
|
6
|
+
Card,
|
|
7
|
+
useToast,
|
|
8
|
+
SearchInput,
|
|
9
|
+
type ColDef,
|
|
10
|
+
type GridApi
|
|
11
|
+
} from '@ramme-io/ui';
|
|
12
|
+
import { getResourceMeta, getMockData } from '../data/mockData';
|
|
13
|
+
import { AutoForm } from '../components/AutoForm';
|
|
14
|
+
import { useCrudLocalStorage } from '../hooks/useCrudLocalStorage';
|
|
15
|
+
|
|
16
|
+
interface SmartTableProps {
|
|
17
|
+
dataId: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
initialFilter?: Record<string, any>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SmartTable: React.FC<SmartTableProps> = ({
|
|
23
|
+
dataId,
|
|
24
|
+
title,
|
|
25
|
+
initialFilter
|
|
26
|
+
}) => {
|
|
27
|
+
const { addToast } = useToast();
|
|
28
|
+
|
|
29
|
+
// 1. DATA KERNEL
|
|
30
|
+
const meta = getResourceMeta(dataId);
|
|
31
|
+
const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
|
|
32
|
+
|
|
33
|
+
// We use the CRUD hook to persist changes to localStorage
|
|
34
|
+
const {
|
|
35
|
+
data: rowData,
|
|
36
|
+
createItem,
|
|
37
|
+
updateItem,
|
|
38
|
+
deleteItem
|
|
39
|
+
} = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
|
|
40
|
+
|
|
41
|
+
// 2. UI STATE
|
|
42
|
+
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
43
|
+
const [currentRecord, setCurrentRecord] = useState<any>(null);
|
|
44
|
+
const [gridApi, setGridApi] = useState<GridApi | null>(null);
|
|
45
|
+
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
46
|
+
const [quickFilterText, setQuickFilterText] = useState('');
|
|
47
|
+
|
|
48
|
+
// 3. COLUMN DEFINITIONS
|
|
49
|
+
const columns = useMemo<ColDef[]>(() => {
|
|
50
|
+
if (!meta?.fields) return [];
|
|
51
|
+
|
|
52
|
+
const generatedCols: ColDef[] = meta.fields.map((f: any) => {
|
|
53
|
+
const col: ColDef = {
|
|
54
|
+
field: f.key,
|
|
55
|
+
headerName: f.label,
|
|
56
|
+
filter: true,
|
|
57
|
+
sortable: true,
|
|
58
|
+
resizable: true,
|
|
59
|
+
flex: 1,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Smart Formatting based on type
|
|
63
|
+
if (f.type === 'currency') {
|
|
64
|
+
col.valueFormatter = (p: any) => p.value ? `$${Number(p.value).toLocaleString()}` : '';
|
|
65
|
+
}
|
|
66
|
+
if (f.type === 'date') {
|
|
67
|
+
col.valueFormatter = (p: any) => p.value ? new Date(p.value).toLocaleDateString() : '';
|
|
68
|
+
}
|
|
69
|
+
if (f.type === 'status') {
|
|
70
|
+
col.cellRenderer = (p: any) => {
|
|
71
|
+
const statusColors: any = {
|
|
72
|
+
active: 'bg-green-100 text-green-800',
|
|
73
|
+
paid: 'bg-green-100 text-green-800',
|
|
74
|
+
pending: 'bg-yellow-100 text-yellow-800',
|
|
75
|
+
inactive: 'bg-slate-100 text-slate-600',
|
|
76
|
+
overdue: 'bg-red-100 text-red-800'
|
|
77
|
+
};
|
|
78
|
+
const colorClass = statusColors[String(p.value).toLowerCase()] || 'bg-slate-100 text-slate-800';
|
|
79
|
+
return (
|
|
80
|
+
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
|
|
81
|
+
{p.value}
|
|
82
|
+
</span>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return col;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Add Checkbox Selection to the first column
|
|
90
|
+
if (generatedCols.length > 0) {
|
|
91
|
+
generatedCols[0].headerCheckboxSelection = true;
|
|
92
|
+
generatedCols[0].checkboxSelection = true;
|
|
93
|
+
generatedCols[0].minWidth = 180;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add Actions Column
|
|
97
|
+
generatedCols.push({
|
|
98
|
+
headerName: "Actions",
|
|
99
|
+
field: "id",
|
|
100
|
+
width: 100,
|
|
101
|
+
pinned: 'right',
|
|
102
|
+
cellRenderer: (params: any) => (
|
|
103
|
+
<div className="flex items-center gap-1">
|
|
104
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={(e) => { e.stopPropagation(); setCurrentRecord(params.data); setIsEditOpen(true); }}>
|
|
105
|
+
<Icon name="edit-2" size={14} className="text-slate-500" />
|
|
106
|
+
</Button>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return generatedCols;
|
|
112
|
+
}, [meta]);
|
|
113
|
+
|
|
114
|
+
// 4. HANDLERS
|
|
115
|
+
const onGridReady = useCallback((params: any) => {
|
|
116
|
+
setGridApi(params.api);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const onSelectionChanged = useCallback(() => {
|
|
120
|
+
if (gridApi) {
|
|
121
|
+
setSelectedRows(gridApi.getSelectedRows());
|
|
122
|
+
}
|
|
123
|
+
}, [gridApi]);
|
|
124
|
+
|
|
125
|
+
const handleBulkDelete = () => {
|
|
126
|
+
if (confirm(`Delete ${selectedRows.length} items?`)) {
|
|
127
|
+
selectedRows.forEach(row => deleteItem(row.id));
|
|
128
|
+
setSelectedRows([]);
|
|
129
|
+
addToast(`${selectedRows.length} items deleted`, 'success');
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleSave = (record: any) => {
|
|
134
|
+
if (record.id && currentRecord?.id) {
|
|
135
|
+
updateItem(record);
|
|
136
|
+
addToast('Item updated', 'success');
|
|
137
|
+
} else {
|
|
138
|
+
const { id, ...newItem } = record;
|
|
139
|
+
createItem(newItem);
|
|
140
|
+
addToast('Item created', 'success');
|
|
141
|
+
}
|
|
142
|
+
setIsEditOpen(false);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// --- RENDER ---
|
|
146
|
+
return (
|
|
147
|
+
<Card className="flex flex-col h-[600px] border border-border shadow-sm overflow-hidden bg-card">
|
|
148
|
+
|
|
149
|
+
{/* HEADER TOOLBAR */}
|
|
150
|
+
<div className="p-4 border-b border-border flex justify-between items-center gap-4 bg-muted/5">
|
|
151
|
+
|
|
152
|
+
{/* Left: Title or Bulk Actions */}
|
|
153
|
+
{selectedRows.length > 0 ? (
|
|
154
|
+
<div className="flex items-center gap-3 animate-in fade-in slide-in-from-left-2 duration-200">
|
|
155
|
+
<span className="bg-primary text-primary-foreground text-xs font-bold px-2 py-1 rounded-md">
|
|
156
|
+
{selectedRows.length} Selected
|
|
157
|
+
</span>
|
|
158
|
+
<Button size="sm" variant="danger" onClick={handleBulkDelete} iconLeft="trash-2">
|
|
159
|
+
Delete Selected
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
) : (
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<div className="p-2 bg-primary/10 rounded-md text-primary">
|
|
165
|
+
<Icon name="table" size={18} />
|
|
166
|
+
</div>
|
|
167
|
+
<div>
|
|
168
|
+
<h3 className="text-base font-bold text-foreground leading-tight">
|
|
169
|
+
{title || meta?.name || dataId}
|
|
170
|
+
</h3>
|
|
171
|
+
<p className="text-xs text-muted-foreground">
|
|
172
|
+
{rowData.length} records found
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Right: Actions & Filter */}
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
<div className="w-64">
|
|
181
|
+
<SearchInput
|
|
182
|
+
placeholder="Quick search..."
|
|
183
|
+
value={quickFilterText}
|
|
184
|
+
onChange={(e) => {
|
|
185
|
+
setQuickFilterText(e.target.value);
|
|
186
|
+
gridApi?.setQuickFilter(e.target.value);
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="h-6 w-px bg-border mx-1" />
|
|
191
|
+
<Button size="sm" variant="primary" iconLeft="plus" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
|
|
192
|
+
Add New
|
|
193
|
+
</Button>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* AG GRID */}
|
|
198
|
+
<div className="flex-1 w-full bg-card relative">
|
|
199
|
+
<DataTable
|
|
200
|
+
rowData={rowData}
|
|
201
|
+
columnDefs={columns}
|
|
202
|
+
onGridReady={onGridReady}
|
|
203
|
+
onSelectionChanged={onSelectionChanged}
|
|
204
|
+
rowSelection="multiple"
|
|
205
|
+
pagination={true}
|
|
206
|
+
paginationPageSize={10}
|
|
207
|
+
headerHeight={48}
|
|
208
|
+
rowHeight={48}
|
|
209
|
+
enableCellTextSelection={true}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* EDIT DRAWER */}
|
|
214
|
+
<AutoForm
|
|
215
|
+
isOpen={isEditOpen}
|
|
216
|
+
onClose={() => setIsEditOpen(false)}
|
|
217
|
+
onSubmit={handleSave}
|
|
218
|
+
title={meta?.name || 'Item'}
|
|
219
|
+
fields={meta?.fields || []}
|
|
220
|
+
initialData={currentRecord}
|
|
221
|
+
/>
|
|
222
|
+
</Card>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { Drawer, FormTemplate, Button, type FormField } from '@ramme-io/ui';
|
|
3
|
+
|
|
4
|
+
interface AutoFormProps {
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onSubmit: (data: any) => void;
|
|
8
|
+
title: string;
|
|
9
|
+
fields: any[]; // The raw metadata from app.manifest.ts
|
|
10
|
+
initialData?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const AutoForm: React.FC<AutoFormProps> = ({
|
|
14
|
+
isOpen,
|
|
15
|
+
onClose,
|
|
16
|
+
onSubmit,
|
|
17
|
+
title,
|
|
18
|
+
fields,
|
|
19
|
+
initialData
|
|
20
|
+
}) => {
|
|
21
|
+
|
|
22
|
+
// 1. Safe Data Handling
|
|
23
|
+
const safeData = initialData || {};
|
|
24
|
+
const isEditMode = !!safeData.id;
|
|
25
|
+
|
|
26
|
+
// 2. Schema-to-UI Mapping Engine
|
|
27
|
+
const formFields = useMemo<FormField[]>(() => {
|
|
28
|
+
return fields
|
|
29
|
+
// 🛑 RULE 1: Hide ID field during creation (System handles it)
|
|
30
|
+
.filter((f: any) => !(f.key === 'id' && !isEditMode))
|
|
31
|
+
.map((f: any) => {
|
|
32
|
+
|
|
33
|
+
const isIdField = f.key === 'id';
|
|
34
|
+
|
|
35
|
+
// Base Configuration for all fields
|
|
36
|
+
const baseConfig = {
|
|
37
|
+
name: f.key,
|
|
38
|
+
label: f.label,
|
|
39
|
+
required: f.required,
|
|
40
|
+
placeholder: f.description || `Enter ${f.label.toLowerCase()}`,
|
|
41
|
+
// Pre-fill data if editing, or use default from schema
|
|
42
|
+
value: safeData[f.key] !== undefined ? safeData[f.key] : (f.defaultValue || ''),
|
|
43
|
+
colSpan: 1,
|
|
44
|
+
// 🛑 RULE 2: If it's the ID field, force it to be disabled (View Only)
|
|
45
|
+
disabled: isIdField
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Intelligent Type Switching
|
|
49
|
+
switch (f.type) {
|
|
50
|
+
case 'number':
|
|
51
|
+
case 'integer':
|
|
52
|
+
case 'currency':
|
|
53
|
+
return {
|
|
54
|
+
...baseConfig,
|
|
55
|
+
type: 'number',
|
|
56
|
+
value: Number(baseConfig.value) || 0
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
case 'boolean':
|
|
60
|
+
return {
|
|
61
|
+
...baseConfig,
|
|
62
|
+
type: 'toggle',
|
|
63
|
+
checked: !!baseConfig.value
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
case 'date':
|
|
67
|
+
return {
|
|
68
|
+
...baseConfig,
|
|
69
|
+
type: 'datepicker',
|
|
70
|
+
value: baseConfig.value ? new Date(baseConfig.value) : null
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
case 'status':
|
|
74
|
+
return {
|
|
75
|
+
...baseConfig,
|
|
76
|
+
type: 'select',
|
|
77
|
+
options: [
|
|
78
|
+
{ value: 'Active', label: 'Active' },
|
|
79
|
+
{ value: 'Pending', label: 'Pending' },
|
|
80
|
+
{ value: 'Inactive', label: 'Inactive' },
|
|
81
|
+
{ value: 'Archived', label: 'Archived' }
|
|
82
|
+
]
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
case 'textarea':
|
|
86
|
+
return {
|
|
87
|
+
...baseConfig,
|
|
88
|
+
type: 'textarea',
|
|
89
|
+
colSpan: 2
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'email':
|
|
93
|
+
return { ...baseConfig, type: 'email' };
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
return { ...baseConfig, type: 'text' };
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}, [fields, safeData, isEditMode]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Drawer
|
|
103
|
+
isOpen={isOpen}
|
|
104
|
+
onClose={onClose}
|
|
105
|
+
title={`${isEditMode ? 'Edit' : 'New'} ${title}`}
|
|
106
|
+
size="500px"
|
|
107
|
+
>
|
|
108
|
+
<div className="p-6">
|
|
109
|
+
<FormTemplate
|
|
110
|
+
fields={formFields}
|
|
111
|
+
onSubmit={(formData) => {
|
|
112
|
+
// Merge original ID to ensure we update the correct record
|
|
113
|
+
onSubmit({ ...safeData, ...formData });
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<div className="flex justify-end gap-2 mt-8 pt-4 border-t border-border">
|
|
117
|
+
<Button variant="outline" onClick={onClose} type="button">
|
|
118
|
+
Cancel
|
|
119
|
+
</Button>
|
|
120
|
+
<Button type="submit" variant="primary">
|
|
121
|
+
{isEditMode ? 'Save Changes' : 'Create Record'}
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
</FormTemplate>
|
|
125
|
+
</div>
|
|
126
|
+
</Drawer>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -1,52 +1,58 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { getComponent } from '../core/component-registry';
|
|
3
|
-
|
|
4
|
-
import {
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
import { useGeneratedSignals } from '../generated/hooks';
|
|
5
|
+
import { getMockData } from '../data/mockData';
|
|
5
6
|
|
|
6
7
|
const mapSignalStatus = (status: string): string => {
|
|
7
8
|
switch (status) {
|
|
8
9
|
case 'fresh': return 'online';
|
|
9
10
|
case 'stale': return 'warning';
|
|
10
11
|
case 'disconnected': return 'offline';
|
|
11
|
-
case 'error': return 'error';
|
|
12
|
+
case 'error': return 'error';
|
|
12
13
|
default: return 'offline';
|
|
13
14
|
}
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
block: {
|
|
18
|
-
id: string;
|
|
19
|
-
type: string;
|
|
20
|
-
props: Record<string, any>;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const DynamicBlock: React.FC<DynamicBlockProps> = ({ block }) => {
|
|
17
|
+
export const DynamicBlock: React.FC<any> = ({ block }) => {
|
|
25
18
|
const Component = getComponent(block.type);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
// 1. Resolve Data
|
|
29
|
-
const resolvedData = dataId ? getMockData(dataId) : staticProps.data;
|
|
19
|
+
const signals = useGeneratedSignals();
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// 3. Merge Props
|
|
21
|
+
const { signalId, dataId, ...staticProps } = block.props;
|
|
22
|
+
|
|
35
23
|
const dynamicProps: Record<string, any> = {
|
|
36
24
|
...staticProps,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
rowData: resolvedData || [],
|
|
25
|
+
dataId,
|
|
26
|
+
signalId
|
|
40
27
|
};
|
|
41
28
|
|
|
42
|
-
//
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
29
|
+
// --- DATA INJECTION ---
|
|
30
|
+
if (dataId) {
|
|
31
|
+
const resolvedData = getMockData(dataId);
|
|
32
|
+
dynamicProps.data = resolvedData || [];
|
|
33
|
+
dynamicProps.rowData = resolvedData || [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- SIGNAL INJECTION ---
|
|
37
|
+
if (signalId && signals && signalId in signals) {
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
const signalState = signals[signalId];
|
|
40
|
+
|
|
41
|
+
if (signalState) {
|
|
42
|
+
// ✅ FIX: Handle both Raw Values (Legacy) and Signal Objects (New)
|
|
43
|
+
// If it's an object with a 'value' property, use that. Otherwise use it directly.
|
|
44
|
+
const rawValue = (typeof signalState === 'object' && 'value' in signalState)
|
|
45
|
+
? signalState.value
|
|
46
|
+
: signalState;
|
|
47
|
+
|
|
48
|
+
const status = (typeof signalState === 'object' && 'status' in signalState)
|
|
49
|
+
? signalState.status
|
|
50
|
+
: 'fresh'; // Default for raw values
|
|
51
|
+
|
|
52
|
+
dynamicProps.value = typeof rawValue === 'number' ? rawValue : String(rawValue);
|
|
53
|
+
dynamicProps.status = mapSignalStatus(status);
|
|
54
|
+
} else {
|
|
55
|
+
dynamicProps.status = mapSignalStatus('disconnected');
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|