@ramme-io/create-app 1.2.0 → 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 +1 -1
- package/template/src/App.tsx +7 -8
- package/template/src/blocks/SmartTable.tsx +161 -128
- package/template/src/generated/hooks.ts +64 -37
- package/template/src/hooks/useWorkflowEngine.ts +121 -4
- package/template/src/pages/Welcome.tsx +162 -0
- package/template/src/templates/dashboard/dashboard.sitemap.ts +15 -4
- package/template/src/types/schema.ts +36 -2
package/package.json
CHANGED
package/template/src/App.tsx
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
2
2
|
import { generateRoutes } from './core/route-generator';
|
|
3
|
-
import { useEffect } from 'react';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
4
|
|
|
5
5
|
// --- 1. IMPORT ALL THREE TEMPLATES ---
|
|
6
6
|
import DashboardLayout from './templates/dashboard/DashboardLayout';
|
|
7
|
-
import { dashboardSitemap
|
|
7
|
+
import { dashboardSitemap } from './templates/dashboard/dashboard.sitemap';
|
|
8
8
|
import DocsLayout from './templates/docs/DocsLayout';
|
|
9
|
-
import { docsSitemap
|
|
9
|
+
import { docsSitemap } from './templates/docs/docs.sitemap';
|
|
10
10
|
import SettingsLayout from './templates/settings/SettingsLayout';
|
|
11
|
-
import { settingsSitemap
|
|
11
|
+
import { settingsSitemap } from './templates/settings/settings.sitemap';
|
|
12
12
|
|
|
13
13
|
// Other Imports
|
|
14
14
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
@@ -16,12 +16,11 @@ import LoginPage from './pages/LoginPage';
|
|
|
16
16
|
import NotFound from './pages/styleguide/NotFound';
|
|
17
17
|
|
|
18
18
|
// --- 2. IMPORT THE SEEDER ---
|
|
19
|
-
import { initializeDataLake } from './core/data-seeder';
|
|
19
|
+
import { initializeDataLake } from './core/data-seeder';
|
|
20
20
|
|
|
21
21
|
function App() {
|
|
22
22
|
|
|
23
23
|
// ✅ 3. TRIGGER DATA SEEDING ON MOUNT
|
|
24
|
-
// This checks localStorage and injects our mock database if missing.
|
|
25
24
|
useEffect(() => {
|
|
26
25
|
initializeDataLake();
|
|
27
26
|
}, []);
|
|
@@ -30,8 +29,8 @@ function App() {
|
|
|
30
29
|
<Routes>
|
|
31
30
|
<Route path="/login" element={<LoginPage />} />
|
|
32
31
|
<Route element={<ProtectedRoute />}>
|
|
33
|
-
{/*
|
|
34
|
-
<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 />} />
|
|
35
34
|
|
|
36
35
|
{/* Dashboard Template Routes */}
|
|
37
36
|
<Route path="/dashboard/*" element={<DashboardLayout />}>
|
|
@@ -1,183 +1,216 @@
|
|
|
1
|
-
import React, { useState, useMemo } from 'react';
|
|
2
|
-
import {
|
|
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';
|
|
3
12
|
import { getResourceMeta, getMockData } from '../data/mockData';
|
|
4
13
|
import { AutoForm } from '../components/AutoForm';
|
|
5
|
-
// ✅ Import types that now definitely exist
|
|
6
|
-
import { useDataQuery, type FilterOption, type SortOption } from '../hooks/useDataQuery';
|
|
7
14
|
import { useCrudLocalStorage } from '../hooks/useCrudLocalStorage';
|
|
8
15
|
|
|
9
16
|
interface SmartTableProps {
|
|
10
17
|
dataId: string;
|
|
11
18
|
title?: string;
|
|
12
19
|
initialFilter?: Record<string, any>;
|
|
13
|
-
initialSort?: SortOption;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export const SmartTable: React.FC<SmartTableProps> = ({
|
|
17
23
|
dataId,
|
|
18
24
|
title,
|
|
19
|
-
initialFilter
|
|
20
|
-
initialSort
|
|
25
|
+
initialFilter
|
|
21
26
|
}) => {
|
|
22
27
|
const { addToast } = useToast();
|
|
23
28
|
|
|
24
|
-
// 1.
|
|
25
|
-
const [page, setPage] = useState(1);
|
|
26
|
-
const [pageSize] = useState(10); // Kept as state in case we add a selector later
|
|
27
|
-
|
|
28
|
-
// 2. METADATA
|
|
29
|
+
// 1. DATA KERNEL
|
|
29
30
|
const meta = getResourceMeta(dataId);
|
|
30
|
-
|
|
31
|
-
// 3. STORAGE LAYER
|
|
32
|
-
// Seed data if local storage is empty
|
|
33
31
|
const seedData = useMemo(() => getMockData(dataId) || [], [dataId]);
|
|
34
32
|
|
|
33
|
+
// We use the CRUD hook to persist changes to localStorage
|
|
35
34
|
const {
|
|
36
|
-
data:
|
|
35
|
+
data: rowData,
|
|
37
36
|
createItem,
|
|
38
|
-
updateItem
|
|
37
|
+
updateItem,
|
|
38
|
+
deleteItem
|
|
39
39
|
} = useCrudLocalStorage<any>(`ramme_db_${dataId}`, seedData);
|
|
40
40
|
|
|
41
|
-
//
|
|
42
|
-
const activeFilters = useMemo<FilterOption[]>(() => {
|
|
43
|
-
if (!initialFilter) return [];
|
|
44
|
-
return Object.entries(initialFilter).map(([key, value]) => ({
|
|
45
|
-
field: key,
|
|
46
|
-
operator: 'equals',
|
|
47
|
-
value
|
|
48
|
-
}));
|
|
49
|
-
}, [initialFilter]);
|
|
50
|
-
|
|
51
|
-
const { data: processedRows, total, pageCount } = useDataQuery(rawData, {
|
|
52
|
-
filters: activeFilters,
|
|
53
|
-
sort: initialSort, // Using the prop directly for now (Fixes 'setSort' unused)
|
|
54
|
-
page,
|
|
55
|
-
pageSize
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// 5. UI STATE
|
|
41
|
+
// 2. UI STATE
|
|
59
42
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
60
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('');
|
|
61
47
|
|
|
62
|
-
//
|
|
63
|
-
const columns = useMemo(() => {
|
|
64
|
-
if (meta?.fields)
|
|
65
|
-
const cols: ColDef[] = meta.fields.map((f: any) => {
|
|
66
|
-
const colDef: ColDef = {
|
|
67
|
-
field: f.key,
|
|
68
|
-
headerName: f.label,
|
|
69
|
-
filter: true,
|
|
70
|
-
sortable: true,
|
|
71
|
-
flex: 1,
|
|
72
|
-
};
|
|
48
|
+
// 3. COLUMN DEFINITIONS
|
|
49
|
+
const columns = useMemo<ColDef[]>(() => {
|
|
50
|
+
if (!meta?.fields) return [];
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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}`}>
|
|
93
81
|
{p.value}
|
|
94
82
|
</span>
|
|
95
83
|
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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" />
|
|
108
106
|
</Button>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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());
|
|
112
122
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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');
|
|
116
130
|
}
|
|
117
|
-
|
|
118
|
-
}, [meta, processedRows]);
|
|
131
|
+
};
|
|
119
132
|
|
|
120
133
|
const handleSave = (record: any) => {
|
|
121
134
|
if (record.id && currentRecord?.id) {
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
updateItem(record);
|
|
136
|
+
addToast('Item updated', 'success');
|
|
124
137
|
} else {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
const { id, ...newItem } = record;
|
|
139
|
+
createItem(newItem);
|
|
140
|
+
addToast('Item created', 'success');
|
|
128
141
|
}
|
|
129
142
|
setIsEditOpen(false);
|
|
130
143
|
};
|
|
131
144
|
|
|
145
|
+
// --- RENDER ---
|
|
132
146
|
return (
|
|
133
|
-
<Card className="
|
|
134
|
-
|
|
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 */}
|
|
135
179
|
<div className="flex items-center gap-2">
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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>
|
|
142
194
|
</div>
|
|
143
|
-
<Button size="sm" variant="outline" onClick={() => { setCurrentRecord({}); setIsEditOpen(true); }}>
|
|
144
|
-
<Icon name="plus" className="mr-2 w-4 h-4" /> Add Item
|
|
145
|
-
</Button>
|
|
146
195
|
</div>
|
|
147
196
|
|
|
197
|
+
{/* AG GRID */}
|
|
148
198
|
<div className="flex-1 w-full bg-card relative">
|
|
149
199
|
<DataTable
|
|
150
|
-
rowData={
|
|
200
|
+
rowData={rowData}
|
|
151
201
|
columnDefs={columns}
|
|
152
|
-
|
|
202
|
+
onGridReady={onGridReady}
|
|
203
|
+
onSelectionChanged={onSelectionChanged}
|
|
204
|
+
rowSelection="multiple"
|
|
205
|
+
pagination={true}
|
|
206
|
+
paginationPageSize={10}
|
|
207
|
+
headerHeight={48}
|
|
208
|
+
rowHeight={48}
|
|
209
|
+
enableCellTextSelection={true}
|
|
153
210
|
/>
|
|
154
|
-
|
|
155
|
-
{/* Pagination Controls */}
|
|
156
|
-
<div className="p-2 border-t border-border flex justify-between items-center text-sm">
|
|
157
|
-
<span className="text-muted-foreground">
|
|
158
|
-
Page {page} of {pageCount || 1}
|
|
159
|
-
</span>
|
|
160
|
-
<div className="flex gap-2">
|
|
161
|
-
<Button
|
|
162
|
-
variant="ghost"
|
|
163
|
-
size="sm"
|
|
164
|
-
disabled={page === 1}
|
|
165
|
-
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
166
|
-
>
|
|
167
|
-
Prev
|
|
168
|
-
</Button>
|
|
169
|
-
<Button
|
|
170
|
-
variant="ghost"
|
|
171
|
-
size="sm"
|
|
172
|
-
disabled={page >= pageCount}
|
|
173
|
-
onClick={() => setPage(p => p + 1)}
|
|
174
|
-
>
|
|
175
|
-
Next
|
|
176
|
-
</Button>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
211
|
</div>
|
|
180
212
|
|
|
213
|
+
{/* EDIT DRAWER */}
|
|
181
214
|
<AutoForm
|
|
182
215
|
isOpen={isEditOpen}
|
|
183
216
|
onClose={() => setIsEditOpen(false)}
|
|
@@ -1,40 +1,67 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { useEffect } from 'react';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
// --- 1. SIGNAL STORE ---
|
|
5
|
+
export interface SignalValue {
|
|
6
|
+
value: any;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SignalStore {
|
|
11
|
+
signals: Record<string, SignalValue>;
|
|
12
|
+
updateSignal: (id: string, value: any) => void;
|
|
13
|
+
updateSignals: (updates: Record<string, any>) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initial State matches app.manifest.ts
|
|
17
|
+
const initialState = {
|
|
18
|
+
living_room_ac: { value: 72, timestamp: Date.now() },
|
|
19
|
+
living_room_hum: { value: 45, timestamp: Date.now() },
|
|
20
|
+
server_01: { value: 42, timestamp: Date.now() },
|
|
21
|
+
front_door_lock: { value: 'LOCKED', timestamp: Date.now() }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const useSignalStore = create<SignalStore>((set) => ({
|
|
25
|
+
signals: initialState,
|
|
4
26
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
updateSignal: (id, value) => set((state) => ({
|
|
28
|
+
signals: {
|
|
29
|
+
...state.signals,
|
|
30
|
+
[id]: { value, timestamp: Date.now() }
|
|
31
|
+
}
|
|
32
|
+
})),
|
|
33
|
+
|
|
34
|
+
updateSignals: (updates) => set((state) => {
|
|
35
|
+
const newSignals = { ...state.signals };
|
|
36
|
+
Object.entries(updates).forEach(([id, val]) => {
|
|
37
|
+
newSignals[id] = { value: val, timestamp: Date.now() };
|
|
38
|
+
});
|
|
39
|
+
return { signals: newSignals };
|
|
40
|
+
})
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
export const useGeneratedSignals = () => {
|
|
44
|
+
return useSignalStore((state) => state.signals);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// --- 2. SIMULATION ENGINE ---
|
|
48
|
+
export const useSimulation = () => {
|
|
49
|
+
const { updateSignals } = useSignalStore();
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
console.log("[System] Simulation Started");
|
|
53
|
+
const interval = setInterval(() => {
|
|
54
|
+
// Simulate random fluctuations for sensors
|
|
55
|
+
const updates: Record<string, any> = {};
|
|
56
|
+
|
|
57
|
+
// Randomize values slightly
|
|
58
|
+
updates['living_room_ac'] = Number((72 + (Math.random() * 4 - 2)).toFixed(1));
|
|
59
|
+
updates['living_room_hum'] = Number((45 + (Math.random() * 6 - 3)).toFixed(1));
|
|
60
|
+
updates['server_01'] = Math.floor(Math.random() * 100);
|
|
61
|
+
|
|
62
|
+
updateSignals(updates);
|
|
63
|
+
}, 2000); // Update every 2 seconds
|
|
64
|
+
|
|
65
|
+
return () => clearInterval(interval);
|
|
66
|
+
}, [updateSignals]);
|
|
40
67
|
};
|
|
@@ -1,6 +1,123 @@
|
|
|
1
|
-
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useToast } from '@ramme-io/ui';
|
|
3
|
+
// @ts-ignore - These generated hooks exist in the build
|
|
4
|
+
import { useGeneratedSignals, useSimulation } from '../generated/hooks';
|
|
5
|
+
import { appManifest } from '../config/app.manifest';
|
|
6
|
+
// ✅ Import types to fix implicit 'any' errors
|
|
7
|
+
import type { ActionDefinition, WorkflowDefinition } from '../types/schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* THE RUNTIME LOGIC ENGINE
|
|
11
|
+
* This hook breathes life into the application.
|
|
12
|
+
* It watches for signal changes and executes the workflows defined in the manifest.
|
|
13
|
+
*/
|
|
2
14
|
export const useWorkflowEngine = () => {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
15
|
+
const signals = useGeneratedSignals();
|
|
16
|
+
const { addToast } = useToast();
|
|
17
|
+
|
|
18
|
+
// 1. Activate the Data Simulation (Randomizer)
|
|
19
|
+
useSimulation();
|
|
20
|
+
|
|
21
|
+
// 2. Define the Action Executor
|
|
22
|
+
// ✅ Explicitly typed 'action'
|
|
23
|
+
const executeAction = async (action: ActionDefinition, context: any) => {
|
|
24
|
+
console.log(`[Engine] Executing: ${action.type}`, action);
|
|
25
|
+
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case 'send_notification':
|
|
28
|
+
addToast(action.config.message || 'Notification Sent', 'info');
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
case 'update_resource':
|
|
32
|
+
// In a real app, this would call the API
|
|
33
|
+
addToast(`Updating Resource: ${JSON.stringify(action.config)}`, 'success');
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case 'agent_task':
|
|
37
|
+
// Simulate AI Agent processing
|
|
38
|
+
console.log(`[AI Agent] Thinking about: "${action.config.prompt}"...`);
|
|
39
|
+
// ✅ FIX: 'loading' is not a valid ToastType. Using 'info' instead.
|
|
40
|
+
addToast('AI Agent Analyzing...', 'info');
|
|
41
|
+
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
// Mock response based on prompt context
|
|
44
|
+
const response = action.config.prompt?.includes('health')
|
|
45
|
+
? "System Operating Normally."
|
|
46
|
+
: "Optimization Recommended.";
|
|
47
|
+
|
|
48
|
+
addToast(`🤖 Agent: "${response}"`, 'success', 5000);
|
|
49
|
+
}, 1500);
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 'navigate':
|
|
53
|
+
window.location.href = action.config.path;
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
console.warn('Unknown action type:', action.type);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 3. Watch Signals & Trigger Workflows
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!appManifest.domain?.workflows) return;
|
|
64
|
+
|
|
65
|
+
// ✅ Explicitly typed 'flow'
|
|
66
|
+
appManifest.domain.workflows.forEach((flow: WorkflowDefinition) => {
|
|
67
|
+
if (!flow.active) return;
|
|
68
|
+
|
|
69
|
+
// Check Trigger: Signal Change
|
|
70
|
+
if (flow.trigger.type === 'signal_change') {
|
|
71
|
+
const signalId = flow.trigger.config.signalId;
|
|
72
|
+
const condition = flow.trigger.config.condition; // e.g., "> 80"
|
|
73
|
+
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
const signal = signals[signalId];
|
|
76
|
+
|
|
77
|
+
if (signal) {
|
|
78
|
+
const val = typeof signal === 'object' ? signal.value : signal;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// Check if condition is met (e.g. "50 > 80")
|
|
82
|
+
const isMet = checkCondition(val, condition);
|
|
83
|
+
|
|
84
|
+
if (isMet) {
|
|
85
|
+
// Throttling logic would go here to prevent spam
|
|
86
|
+
console.log(`[Engine] Trigger Fired: ${flow.name}`);
|
|
87
|
+
flow.actions.forEach(action => executeAction(action, { signal: val }));
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// Ignore parse errors
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}, [signals, addToast]);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
// Exposed for manual triggering (e.g. Buttons)
|
|
99
|
+
triggerWorkflow: (workflowId: string) => {
|
|
100
|
+
const flow = appManifest.domain?.workflows?.find(w => w.id === workflowId);
|
|
101
|
+
if (flow) {
|
|
102
|
+
flow.actions.forEach(action => executeAction(action, { manual: true }));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// --- HELPER: Safe Condition Checker ---
|
|
109
|
+
const checkCondition = (value: number, condition: string): boolean => {
|
|
110
|
+
if (!condition) return false;
|
|
111
|
+
const parts = condition.trim().split(' ');
|
|
112
|
+
const operator = parts[0];
|
|
113
|
+
const target = parseFloat(parts[1]);
|
|
114
|
+
|
|
115
|
+
switch (operator) {
|
|
116
|
+
case '>': return value > target;
|
|
117
|
+
case '<': return value < target;
|
|
118
|
+
case '>=': return value >= target;
|
|
119
|
+
case '<=': return value <= target;
|
|
120
|
+
case '==': return value === target;
|
|
121
|
+
default: return false;
|
|
122
|
+
}
|
|
6
123
|
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
PageHeader,
|
|
5
|
+
Card,
|
|
6
|
+
Button,
|
|
7
|
+
Icon,
|
|
8
|
+
Badge,
|
|
9
|
+
SectionHeader
|
|
10
|
+
} from '@ramme-io/ui';
|
|
11
|
+
|
|
12
|
+
const Welcome: React.FC = () => {
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
|
17
|
+
|
|
18
|
+
{/* HERO */}
|
|
19
|
+
<div className="relative overflow-hidden rounded-xl bg-gradient-to-r from-primary to-violet-600 text-primary-foreground p-8 md:p-12">
|
|
20
|
+
<div className="relative z-10 max-w-2xl">
|
|
21
|
+
<Badge variant="secondary" className="mb-4 bg-white/20 text-white border-none backdrop-blur-sm">
|
|
22
|
+
v1.2.0 Starter Kit
|
|
23
|
+
</Badge>
|
|
24
|
+
<h1 className="text-4xl md:text-5xl font-extrabold mb-4 tracking-tight">
|
|
25
|
+
Your Ramme App is Ready.
|
|
26
|
+
</h1>
|
|
27
|
+
<p className="text-lg md:text-xl text-primary-foreground/90 mb-8 leading-relaxed">
|
|
28
|
+
You have successfully scaffolded a production-ready prototype environment.
|
|
29
|
+
This kit comes pre-wired with Authentication, Mock Data, and the A.D.A.P.T. architecture.
|
|
30
|
+
</p>
|
|
31
|
+
<div className="flex flex-wrap gap-4">
|
|
32
|
+
<Button
|
|
33
|
+
size="lg"
|
|
34
|
+
variant="secondary"
|
|
35
|
+
className="font-semibold"
|
|
36
|
+
iconLeft="layout-dashboard"
|
|
37
|
+
onClick={() => navigate('/dashboard/app')}
|
|
38
|
+
>
|
|
39
|
+
Open Live Dashboard
|
|
40
|
+
</Button>
|
|
41
|
+
<Button
|
|
42
|
+
size="lg"
|
|
43
|
+
variant="outline"
|
|
44
|
+
className="bg-transparent border-white/30 text-white hover:bg-white/10 hover:text-white"
|
|
45
|
+
iconLeft="book-open"
|
|
46
|
+
onClick={() => navigate('/docs')}
|
|
47
|
+
>
|
|
48
|
+
Read Documentation
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{/* Decorative Background Icon */}
|
|
54
|
+
<div className="absolute -right-10 -bottom-10 opacity-10 rotate-12">
|
|
55
|
+
<Icon name="box" size={300} />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
60
|
+
|
|
61
|
+
{/* SECTION 1: ARCHITECTURE */}
|
|
62
|
+
<div className="space-y-4">
|
|
63
|
+
<SectionHeader title="Project Architecture" />
|
|
64
|
+
<div className="grid gap-4">
|
|
65
|
+
<Card className="p-4 flex gap-4 hover:border-primary/50 transition-colors cursor-default">
|
|
66
|
+
<div className="p-3 rounded-lg bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 h-fit">
|
|
67
|
+
<Icon name="database" size={24} />
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<h3 className="font-semibold text-foreground">Data Lake</h3>
|
|
71
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
72
|
+
Mock data is seeded into <code>localStorage</code> on boot.
|
|
73
|
+
Edit <code>src/data/mockData.ts</code> to change the schema.
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
</Card>
|
|
77
|
+
|
|
78
|
+
<Card className="p-4 flex gap-4 hover:border-primary/50 transition-colors cursor-default">
|
|
79
|
+
<div className="p-3 rounded-lg bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400 h-fit">
|
|
80
|
+
<Icon name="git-branch" size={24} />
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<h3 className="font-semibold text-foreground">Logic Engine</h3>
|
|
84
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
85
|
+
Workflows and signals are processed in real-time by
|
|
86
|
+
<code>useWorkflowEngine.ts</code>. Supports MQTT and simulated sensors.
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</Card>
|
|
90
|
+
|
|
91
|
+
<Card className="p-4 flex gap-4 hover:border-primary/50 transition-colors cursor-default">
|
|
92
|
+
<div className="p-3 rounded-lg bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400 h-fit">
|
|
93
|
+
<Icon name="layout" size={24} />
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<h3 className="font-semibold text-foreground">Dynamic Routing</h3>
|
|
97
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
98
|
+
Pages are generated from <code>app.manifest.ts</code>.
|
|
99
|
+
Visual blocks connect automatically to data sources.
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
</Card>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* SECTION 2: RESOURCES */}
|
|
107
|
+
<div className="space-y-4">
|
|
108
|
+
<SectionHeader title="Developer Resources" />
|
|
109
|
+
|
|
110
|
+
<Card className="p-6 space-y-6">
|
|
111
|
+
<div>
|
|
112
|
+
<h4 className="font-semibold flex items-center gap-2 mb-2">
|
|
113
|
+
<Icon name="palette" size={16} className="text-muted-foreground" />
|
|
114
|
+
Component Library
|
|
115
|
+
</h4>
|
|
116
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
117
|
+
Browse the full suite of accessible UI components available in this project.
|
|
118
|
+
</p>
|
|
119
|
+
<Button variant="outline" size="sm" onClick={() => navigate('/styleguide')}>
|
|
120
|
+
View Style Guide
|
|
121
|
+
</Button>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="border-t border-border pt-6">
|
|
125
|
+
<h4 className="font-semibold flex items-center gap-2 mb-2">
|
|
126
|
+
<Icon name="settings" size={16} className="text-muted-foreground" />
|
|
127
|
+
Configuration
|
|
128
|
+
</h4>
|
|
129
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
130
|
+
Manage global settings, user profile templates, and billing layouts.
|
|
131
|
+
</p>
|
|
132
|
+
<Button variant="outline" size="sm" onClick={() => navigate('/settings')}>
|
|
133
|
+
Open Settings
|
|
134
|
+
</Button>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="border-t border-border pt-6">
|
|
138
|
+
<h4 className="font-semibold flex items-center gap-2 mb-2">
|
|
139
|
+
<Icon name="github" size={16} className="text-muted-foreground" />
|
|
140
|
+
Community
|
|
141
|
+
</h4>
|
|
142
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
143
|
+
Need help? Check the docs or open an issue on GitHub.
|
|
144
|
+
</p>
|
|
145
|
+
<a
|
|
146
|
+
href="https://github.com/ramme-io/create-app"
|
|
147
|
+
target="_blank"
|
|
148
|
+
rel="noreferrer"
|
|
149
|
+
className="inline-flex"
|
|
150
|
+
>
|
|
151
|
+
<Button variant="ghost" size="sm">GitHub Repo →</Button>
|
|
152
|
+
</a>
|
|
153
|
+
</div>
|
|
154
|
+
</Card>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export default Welcome;
|
|
@@ -2,8 +2,18 @@ import { type SitemapEntry } from '../../core/sitemap-entry';
|
|
|
2
2
|
import { appManifest } from '../../config/app.manifest';
|
|
3
3
|
import Dashboard from '../../pages/Dashboard';
|
|
4
4
|
import AiChat from '../../pages/AiChat';
|
|
5
|
+
import Welcome from '../../pages/Welcome'; // ✅ Import the new page
|
|
5
6
|
|
|
6
|
-
export const dashboardSitemap: SitemapEntry[] = [
|
|
7
|
+
export const dashboardSitemap: SitemapEntry[] = [
|
|
8
|
+
// ✅ 1. The New Landing Page
|
|
9
|
+
{
|
|
10
|
+
id: 'welcome',
|
|
11
|
+
path: 'welcome',
|
|
12
|
+
title: 'Start Here',
|
|
13
|
+
icon: 'rocket',
|
|
14
|
+
component: Welcome,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
7
17
|
|
|
8
18
|
// A. Dynamic Pages from Manifest
|
|
9
19
|
if (appManifest.pages) {
|
|
@@ -13,10 +23,11 @@ if (appManifest.pages) {
|
|
|
13
23
|
dashboardSitemap.push({
|
|
14
24
|
id: page.id,
|
|
15
25
|
title: page.title,
|
|
16
|
-
// Map
|
|
17
|
-
|
|
26
|
+
// ✅ FIX: Map the main dashboard to 'app' instead of root ''
|
|
27
|
+
// This prevents conflict with the layout root
|
|
28
|
+
path: isDashboard ? 'app' : page.slug,
|
|
18
29
|
icon: isDashboard ? 'layout-dashboard' : 'file-text',
|
|
19
|
-
component: Dashboard,
|
|
30
|
+
component: Dashboard,
|
|
20
31
|
});
|
|
21
32
|
});
|
|
22
33
|
}
|
|
@@ -70,6 +70,39 @@ export const EntitySchema = z.object({
|
|
|
70
70
|
});
|
|
71
71
|
export type EntityDefinition = z.infer<typeof EntitySchema>;
|
|
72
72
|
|
|
73
|
+
// ------------------------------------------------------------------
|
|
74
|
+
// ✅ NEW: LOGIC & WORKFLOW DEFINITIONS
|
|
75
|
+
// ------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export const TriggerSchema = z.object({
|
|
78
|
+
id: z.string(),
|
|
79
|
+
type: z.enum(['signal_change', 'manual_action', 'schedule', 'webhook']),
|
|
80
|
+
config: z.record(z.string(), z.any()),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const ActionSchema = z.object({
|
|
84
|
+
id: z.string(),
|
|
85
|
+
type: z.enum([
|
|
86
|
+
'update_resource',
|
|
87
|
+
'send_notification',
|
|
88
|
+
'mqtt_publish',
|
|
89
|
+
'api_call',
|
|
90
|
+
'navigate',
|
|
91
|
+
'agent_task'
|
|
92
|
+
]),
|
|
93
|
+
config: z.record(z.string(), z.any()),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const WorkflowSchema = z.object({
|
|
97
|
+
id: z.string(),
|
|
98
|
+
name: z.string(),
|
|
99
|
+
active: z.boolean().default(true),
|
|
100
|
+
trigger: TriggerSchema,
|
|
101
|
+
actions: z.array(ActionSchema),
|
|
102
|
+
});
|
|
103
|
+
export type WorkflowDefinition = z.infer<typeof WorkflowSchema>;
|
|
104
|
+
export type ActionDefinition = z.infer<typeof ActionSchema>;
|
|
105
|
+
|
|
73
106
|
|
|
74
107
|
// ------------------------------------------------------------------
|
|
75
108
|
// 3. UI LAYOUT DEFINITIONS (Presentation Layer)
|
|
@@ -78,7 +111,6 @@ export type EntityDefinition = z.infer<typeof EntitySchema>;
|
|
|
78
111
|
export const BlockSchema = z.object({
|
|
79
112
|
id: z.string(),
|
|
80
113
|
type: z.string(),
|
|
81
|
-
// ✅ FIX: Explicitly define Key and Value types
|
|
82
114
|
props: z.record(z.string(), z.any()),
|
|
83
115
|
layout: z.object({
|
|
84
116
|
colSpan: z.number().optional(),
|
|
@@ -115,7 +147,7 @@ export const AppSpecificationSchema = z.object({
|
|
|
115
147
|
version: z.string(),
|
|
116
148
|
description: z.string().optional(),
|
|
117
149
|
author: z.string().optional(),
|
|
118
|
-
createdAt: z.string().optional(),
|
|
150
|
+
createdAt: z.string().optional(),
|
|
119
151
|
}),
|
|
120
152
|
|
|
121
153
|
config: z.object({
|
|
@@ -130,6 +162,8 @@ export const AppSpecificationSchema = z.object({
|
|
|
130
162
|
domain: z.object({
|
|
131
163
|
signals: z.array(SignalSchema),
|
|
132
164
|
entities: z.array(EntitySchema),
|
|
165
|
+
// ✅ ADDED WORKFLOWS HERE
|
|
166
|
+
workflows: z.array(WorkflowSchema).optional(),
|
|
133
167
|
}),
|
|
134
168
|
|
|
135
169
|
pages: z.array(PageSchema).optional(),
|