@atlashub/smartstack-cli 4.34.0 → 4.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +28 -32
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +35 -303
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/skills/apex/references/checks/seed-checks.sh +1 -1
- package/templates/skills/apex/references/core-seed-data.md +39 -21
- package/templates/skills/application/references/application-roles-template.md +14 -8
- package/templates/skills/application/references/provider-template.md +32 -20
- package/templates/skills/application/templates-frontend.md +350 -89
- package/templates/skills/application/templates-seed.md +23 -11
- package/templates/skills/audit-route/SKILL.md +107 -0
- package/templates/skills/audit-route/references/routing-pattern.md +129 -0
- package/templates/skills/audit-route/steps/step-00-init.md +128 -0
- package/templates/skills/audit-route/steps/step-01-inventory.md +157 -0
- package/templates/skills/audit-route/steps/step-02-conformity.md +193 -0
- package/templates/skills/audit-route/steps/step-03-report.md +201 -0
- package/templates/skills/cli-app-sync/SKILL.md +2 -2
- package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
- package/templates/skills/dev-start/SKILL.md +12 -2
- package/templates/skills/documentation/steps/step-03-validate.md +12 -14
- package/templates/skills/efcore/SKILL.md +219 -67
- package/templates/agents/efcore/conflicts.md +0 -114
- package/templates/agents/efcore/db-deploy.md +0 -86
- package/templates/agents/efcore/db-reset.md +0 -98
- package/templates/agents/efcore/db-seed.md +0 -73
- package/templates/agents/efcore/db-status.md +0 -97
- package/templates/agents/efcore/scan.md +0 -124
- package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +0 -126
- package/templates/skills/efcore/references/both-contexts.md +0 -32
- package/templates/skills/efcore/references/destructive-operations.md +0 -38
- package/templates/skills/efcore/steps/db/step-deploy.md +0 -217
- package/templates/skills/efcore/steps/db/step-reset.md +0 -186
- package/templates/skills/efcore/steps/db/step-seed.md +0 -166
- package/templates/skills/efcore/steps/db/step-status.md +0 -173
- package/templates/skills/efcore/steps/migration/step-00-init.md +0 -102
- package/templates/skills/efcore/steps/migration/step-01-check.md +0 -164
- package/templates/skills/efcore/steps/migration/step-02-create.md +0 -160
- package/templates/skills/efcore/steps/migration/step-03-validate.md +0 -168
- package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +0 -173
- package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +0 -100
- package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +0 -115
- package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +0 -112
- package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +0 -157
- package/templates/skills/efcore/steps/shared/step-00-init.md +0 -131
- package/templates/skills/efcore/steps/squash/step-00-init.md +0 -141
- package/templates/skills/efcore/steps/squash/step-01-backup.md +0 -120
- package/templates/skills/efcore/steps/squash/step-02-fetch.md +0 -168
- package/templates/skills/efcore/steps/squash/step-03-create.md +0 -184
- package/templates/skills/efcore/steps/squash/step-04-validate.md +0 -174
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> These templates generate React/TypeScript code for new applications/modules.
|
|
4
4
|
|
|
5
|
-
> **
|
|
6
|
-
>
|
|
5
|
+
> **v3.7+:** Pattern A (mergeRoutes) and Pattern B (JSX Routes) were removed.
|
|
6
|
+
> The only supported routing pattern is PageRegistry.register() + DynamicRouter.
|
|
7
7
|
> See references/frontend-route-wiring-app-tsx.md for the current pattern.
|
|
8
8
|
|
|
9
9
|
---
|
|
@@ -79,6 +79,274 @@ export function $MODULE_PASCALPage() {
|
|
|
79
79
|
|
|
80
80
|
---
|
|
81
81
|
|
|
82
|
+
## TEMPLATE: DETAIL PAGE
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage.tsx
|
|
86
|
+
|
|
87
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
88
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
89
|
+
import { useTranslation } from 'react-i18next';
|
|
90
|
+
import { ArrowLeft, Pencil, Trash2, Loader2 } from 'lucide-react';
|
|
91
|
+
import { Breadcrumb } from '@/components/ui/Breadcrumb';
|
|
92
|
+
import { $moduleApi, type $ENTITY_PASCALDto } from '@/services/api/$moduleApi';
|
|
93
|
+
|
|
94
|
+
export function $MODULE_PASCALDetailPage() {
|
|
95
|
+
// ⚠️ CRITICAL: DynamicRouter generates /:id — use destructuring rename
|
|
96
|
+
const { id: $entityId } = useParams<{ id: string }>();
|
|
97
|
+
const navigate = useNavigate();
|
|
98
|
+
const { t } = useTranslation(['$module', 'common']);
|
|
99
|
+
|
|
100
|
+
const [$entity, set$ENTITY_PASCAL] = useState<$ENTITY_PASCALDto | null>(null);
|
|
101
|
+
const [loading, setLoading] = useState(true);
|
|
102
|
+
const [error, setError] = useState<string | null>(null);
|
|
103
|
+
|
|
104
|
+
const loadData = useCallback(async () => {
|
|
105
|
+
if (!$entityId) return;
|
|
106
|
+
try {
|
|
107
|
+
setLoading(true);
|
|
108
|
+
setError(null);
|
|
109
|
+
const data = await $moduleApi.getById($entityId);
|
|
110
|
+
set$ENTITY_PASCAL(data);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
setError(t('common:errors.loadFailed'));
|
|
113
|
+
console.error('Failed to load $entity:', err);
|
|
114
|
+
} finally {
|
|
115
|
+
setLoading(false);
|
|
116
|
+
}
|
|
117
|
+
}, [$entityId, t]);
|
|
118
|
+
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
loadData();
|
|
121
|
+
}, [loadData]);
|
|
122
|
+
|
|
123
|
+
const handleDelete = async () => {
|
|
124
|
+
if (!$entityId || !confirm(t('common:confirmDelete'))) return;
|
|
125
|
+
try {
|
|
126
|
+
await $moduleApi.delete($entityId);
|
|
127
|
+
navigate('..');
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('Failed to delete:', err);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (loading) {
|
|
134
|
+
return (
|
|
135
|
+
<div className="flex items-center justify-center py-12">
|
|
136
|
+
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (error || !$entity) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="p-6">
|
|
144
|
+
<p className="text-[var(--error-text)]">{error || t('common:errors.notFound')}</p>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="space-y-6">
|
|
151
|
+
<Breadcrumb
|
|
152
|
+
items={[
|
|
153
|
+
{ label: t('$module:title'), href: '/$APPLICATION_KEBAB/$MODULE_KEBAB' },
|
|
154
|
+
{ label: $entity.name },
|
|
155
|
+
]}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
<div className="flex items-center justify-between">
|
|
159
|
+
<div className="flex items-center gap-3">
|
|
160
|
+
<button onClick={() => navigate('..')} className="p-2 hover:bg-[var(--bg-hover)]" style={{ borderRadius: 'var(--radius-button)' }}>
|
|
161
|
+
<ArrowLeft className="w-5 h-5" />
|
|
162
|
+
</button>
|
|
163
|
+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">{$entity.name}</h1>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
166
|
+
<button
|
|
167
|
+
onClick={() => navigate('edit')}
|
|
168
|
+
className="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white font-medium transition-colors"
|
|
169
|
+
style={{ borderRadius: 'var(--radius-button)' }}
|
|
170
|
+
>
|
|
171
|
+
<Pencil className="w-4 h-4" />
|
|
172
|
+
{t('common:actions.edit')}
|
|
173
|
+
</button>
|
|
174
|
+
<button
|
|
175
|
+
onClick={handleDelete}
|
|
176
|
+
className="flex items-center gap-2 px-4 py-2 bg-[var(--error-bg)] hover:bg-[var(--error-border)] text-[var(--error-text)] font-medium transition-colors"
|
|
177
|
+
style={{ borderRadius: 'var(--radius-button)' }}
|
|
178
|
+
>
|
|
179
|
+
<Trash2 className="w-4 h-4" />
|
|
180
|
+
{t('common:actions.delete')}
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Detail content — adapt to your entity */}
|
|
186
|
+
<div className="bg-[var(--bg-card)] border border-[var(--item-color-border)] p-6" style={{ borderRadius: 'var(--radius-card)' }}>
|
|
187
|
+
<dl className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
188
|
+
<div>
|
|
189
|
+
<dt className="text-sm font-medium text-[var(--text-secondary)]">{t('$module:columns.name')}</dt>
|
|
190
|
+
<dd className="mt-1 text-[var(--text-primary)]">{$entity.name}</dd>
|
|
191
|
+
</div>
|
|
192
|
+
<div>
|
|
193
|
+
<dt className="text-sm font-medium text-[var(--text-secondary)]">{t('$module:columns.description')}</dt>
|
|
194
|
+
<dd className="mt-1 text-[var(--text-primary)]">{$entity.description || '-'}</dd>
|
|
195
|
+
</div>
|
|
196
|
+
</dl>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## TEMPLATE: EDIT PAGE
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
// pages/$APPLICATION/$MODULE/$MODULE_PASCALEditPage.tsx
|
|
209
|
+
|
|
210
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
211
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
212
|
+
import { useTranslation } from 'react-i18next';
|
|
213
|
+
import { ArrowLeft, Save, Loader2 } from 'lucide-react';
|
|
214
|
+
import { Breadcrumb } from '@/components/ui/Breadcrumb';
|
|
215
|
+
import { $moduleApi, type $ENTITY_PASCALDto, type Update$ENTITY_PASCALRequest } from '@/services/api/$moduleApi';
|
|
216
|
+
|
|
217
|
+
export function $MODULE_PASCALEditPage() {
|
|
218
|
+
// ⚠️ CRITICAL: DynamicRouter generates /:id/edit — use destructuring rename
|
|
219
|
+
const { id: $entityId } = useParams<{ id: string }>();
|
|
220
|
+
const navigate = useNavigate();
|
|
221
|
+
const { t } = useTranslation(['$module', 'common']);
|
|
222
|
+
|
|
223
|
+
const [data, setData] = useState<$ENTITY_PASCALDto | null>(null);
|
|
224
|
+
const [form, setForm] = useState<Update$ENTITY_PASCALRequest>({ name: '', description: '' });
|
|
225
|
+
const [loading, setLoading] = useState(true);
|
|
226
|
+
const [saving, setSaving] = useState(false);
|
|
227
|
+
const [error, setError] = useState<string | null>(null);
|
|
228
|
+
|
|
229
|
+
const loadData = useCallback(async () => {
|
|
230
|
+
if (!$entityId) return;
|
|
231
|
+
try {
|
|
232
|
+
setLoading(true);
|
|
233
|
+
const result = await $moduleApi.getById($entityId);
|
|
234
|
+
setData(result);
|
|
235
|
+
setForm({ name: result.name, description: result.description || '' });
|
|
236
|
+
} catch (err) {
|
|
237
|
+
setError(t('common:errors.loadFailed'));
|
|
238
|
+
console.error('Failed to load $entity:', err);
|
|
239
|
+
} finally {
|
|
240
|
+
setLoading(false);
|
|
241
|
+
}
|
|
242
|
+
}, [$entityId, t]);
|
|
243
|
+
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
loadData();
|
|
246
|
+
}, [loadData]);
|
|
247
|
+
|
|
248
|
+
const handleSave = async () => {
|
|
249
|
+
if (!$entityId) return;
|
|
250
|
+
try {
|
|
251
|
+
setSaving(true);
|
|
252
|
+
setError(null);
|
|
253
|
+
await $moduleApi.update($entityId, form);
|
|
254
|
+
navigate('..');
|
|
255
|
+
} catch (err) {
|
|
256
|
+
setError(t('common:errors.saveFailed'));
|
|
257
|
+
console.error('Failed to save:', err);
|
|
258
|
+
} finally {
|
|
259
|
+
setSaving(false);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (loading) {
|
|
264
|
+
return (
|
|
265
|
+
<div className="flex items-center justify-center py-12">
|
|
266
|
+
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="space-y-6">
|
|
273
|
+
<Breadcrumb
|
|
274
|
+
items={[
|
|
275
|
+
{ label: t('$module:title'), href: '/$APPLICATION_KEBAB/$MODULE_KEBAB' },
|
|
276
|
+
{ label: data?.name || '...', href: '..' },
|
|
277
|
+
{ label: t('common:actions.edit') },
|
|
278
|
+
]}
|
|
279
|
+
/>
|
|
280
|
+
|
|
281
|
+
<div className="flex items-center gap-3">
|
|
282
|
+
<button onClick={() => navigate('..')} className="p-2 hover:bg-[var(--bg-hover)]" style={{ borderRadius: 'var(--radius-button)' }}>
|
|
283
|
+
<ArrowLeft className="w-5 h-5" />
|
|
284
|
+
</button>
|
|
285
|
+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
|
286
|
+
{t('common:actions.edit')} — {data?.name}
|
|
287
|
+
</h1>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{error && (
|
|
291
|
+
<div className="p-4 bg-[var(--error-bg)] border border-[var(--error-border)]" style={{ borderRadius: 'var(--radius-card)' }}>
|
|
292
|
+
<span className="text-[var(--error-text)]">{error}</span>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
<div className="bg-[var(--bg-card)] border border-[var(--item-color-border)] p-6" style={{ borderRadius: 'var(--radius-card)' }}>
|
|
297
|
+
<div className="space-y-4 max-w-lg">
|
|
298
|
+
<div>
|
|
299
|
+
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
300
|
+
{t('$module:columns.name')}
|
|
301
|
+
</label>
|
|
302
|
+
<input
|
|
303
|
+
type="text"
|
|
304
|
+
value={form.name}
|
|
305
|
+
onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
|
|
306
|
+
className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] text-sm focus:outline-none focus:border-[var(--color-accent-500)]"
|
|
307
|
+
style={{ borderRadius: 'var(--radius-input)' }}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
<div>
|
|
311
|
+
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
|
|
312
|
+
{t('$module:columns.description')}
|
|
313
|
+
</label>
|
|
314
|
+
<textarea
|
|
315
|
+
value={form.description || ''}
|
|
316
|
+
onChange={(e) => setForm(prev => ({ ...prev, description: e.target.value }))}
|
|
317
|
+
rows={3}
|
|
318
|
+
className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] text-sm focus:outline-none focus:border-[var(--color-accent-500)]"
|
|
319
|
+
style={{ borderRadius: 'var(--radius-input)' }}
|
|
320
|
+
/>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-[var(--border-color)]">
|
|
325
|
+
<button
|
|
326
|
+
onClick={() => navigate('..')}
|
|
327
|
+
className="px-4 py-2 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)] font-medium transition-colors"
|
|
328
|
+
style={{ borderRadius: 'var(--radius-button)' }}
|
|
329
|
+
>
|
|
330
|
+
{t('common:actions.cancel')}
|
|
331
|
+
</button>
|
|
332
|
+
<button
|
|
333
|
+
onClick={handleSave}
|
|
334
|
+
disabled={saving}
|
|
335
|
+
className="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white font-medium transition-colors disabled:opacity-50"
|
|
336
|
+
style={{ borderRadius: 'var(--radius-button)' }}
|
|
337
|
+
>
|
|
338
|
+
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
|
339
|
+
{t('common:actions.save')}
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
82
350
|
## TEMPLATE: LIST VIEW (Reusable component)
|
|
83
351
|
|
|
84
352
|
```tsx
|
|
@@ -507,117 +775,108 @@ export const $moduleApi = {
|
|
|
507
775
|
|
|
508
776
|
---
|
|
509
777
|
|
|
510
|
-
## TEMPLATE: ROUTES (
|
|
511
|
-
|
|
512
|
-
### Detect App.tsx Routing Pattern FIRST
|
|
778
|
+
## TEMPLATE: ROUTES (PageRegistry + DynamicRouter)
|
|
513
779
|
|
|
514
|
-
|
|
780
|
+
### Default Pattern (v3.7+): PageRegistry + DynamicRouter
|
|
515
781
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
782
|
+
> **This is the ONLY supported pattern** since v3.7.
|
|
783
|
+
> Pages are registered in `componentRegistry.generated.ts` via `PageRegistry.register()`.
|
|
784
|
+
> DynamicRouter resolves routes automatically from the navigation API + PageRegistry.
|
|
785
|
+
> **No App.tsx wiring needed.** No manual route duplication for tenant prefixes.
|
|
520
786
|
|
|
521
|
-
|
|
787
|
+
**Steps:**
|
|
522
788
|
|
|
523
|
-
|
|
524
|
-
|
|
789
|
+
1. Create your page component(s)
|
|
790
|
+
2. Register in `componentRegistry.generated.ts` (or run `scaffold_routes outputFormat="componentRegistry"`)
|
|
791
|
+
3. Ensure navigation seed data has matching `ComponentKey` values
|
|
792
|
+
4. DynamicRouter does the rest
|
|
525
793
|
|
|
526
794
|
```tsx
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
795
|
+
// 1. Create page component
|
|
796
|
+
// pages/$APPLICATION/$MODULE/$MODULE_PASCALPage.tsx
|
|
797
|
+
export function $MODULE_PASCALPage() { /* ... */ }
|
|
798
|
+
|
|
799
|
+
// 2. Register in componentRegistry.generated.ts
|
|
800
|
+
import { PageRegistry } from '@/extensions/PageRegistry';
|
|
801
|
+
|
|
802
|
+
PageRegistry.register(
|
|
803
|
+
'$APPLICATION_KEBAB.$MODULE_KEBAB',
|
|
804
|
+
lazy(() => import('@/pages/$APPLICATION/$MODULE/$MODULE_PASCALPage')
|
|
805
|
+
.then(m => ({ default: m.$MODULE_PASCALPage })))
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
// For CRUD sub-pages (detail, create):
|
|
809
|
+
PageRegistry.register(
|
|
810
|
+
'$APPLICATION_KEBAB.$MODULE_KEBAB.detail',
|
|
811
|
+
lazy(() => import('@/pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage')
|
|
812
|
+
.then(m => ({ default: m.$MODULE_PASCALDetailPage })))
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
PageRegistry.register(
|
|
816
|
+
'$APPLICATION_KEBAB.$MODULE_KEBAB.create',
|
|
817
|
+
lazy(() => import('@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage')
|
|
818
|
+
.then(m => ({ default: m.Create$MODULE_PASCALPage })))
|
|
819
|
+
);
|
|
542
820
|
```
|
|
543
821
|
|
|
544
|
-
|
|
822
|
+
```sql
|
|
823
|
+
-- 3. Navigation seed data (ComponentKey must match PageRegistry key)
|
|
824
|
+
INSERT INTO core.nav_Sections (...)
|
|
825
|
+
VALUES (..., ComponentKey = '$APPLICATION_KEBAB.$MODULE_KEBAB', ...);
|
|
826
|
+
```
|
|
545
827
|
|
|
546
|
-
|
|
828
|
+
### ⚠️ IMPLICIT ROUTE PARAM CONVENTION
|
|
547
829
|
|
|
548
|
-
|
|
549
|
-
|-----------------|---------------------|---------------|-------------|
|
|
550
|
-
| `administration` | `AppLayout` | `/administration/...` | `/t/:slug/administration/...` |
|
|
551
|
-
| `{application}` | `AppLayout` | `/{application}/...` | `/t/:slug/{application}/...` |
|
|
552
|
-
| `myspace` | `AppLayout` | `/myspace/...` | `/t/:slug/myspace/...` |
|
|
830
|
+
DynamicRouter generates implicit sub-routes for `.detail`, `.create`, and `.edit` page keys:
|
|
553
831
|
|
|
554
|
-
|
|
832
|
+
| Suffix | URL pattern | Route param |
|
|
833
|
+
|--------|-------------|-------------|
|
|
834
|
+
| `.detail` | `/:id` | `id` |
|
|
835
|
+
| `.create` | `/create` | — |
|
|
836
|
+
| `.edit` | `/:id/edit` | `id` |
|
|
555
837
|
|
|
556
|
-
|
|
838
|
+
**The route param is ALWAYS `id`** — never `ticketId`, `userId`, or any entity-specific name.
|
|
557
839
|
|
|
558
|
-
|
|
840
|
+
In detail/edit pages, use destructuring rename to keep a readable local variable:
|
|
559
841
|
|
|
560
|
-
|
|
842
|
+
```tsx
|
|
843
|
+
// ✅ CORRECT — matches DynamicRouter's /:id pattern
|
|
844
|
+
const { id: $entityId } = useParams<{ id: string }>();
|
|
561
845
|
|
|
562
|
-
|
|
846
|
+
// ❌ WRONG — will be undefined because the route param is :id, not :$entityId
|
|
847
|
+
const { $entityId } = useParams<{ $entityId: string }>();
|
|
848
|
+
```
|
|
563
849
|
|
|
564
|
-
|
|
565
|
-
2. Find the existing layout route for the target application:
|
|
566
|
-
- `administration` → `<Route path="/administration" element={<AppLayout />}>`
|
|
567
|
-
- `{application}` → `<Route path="/{application}" element={<AppLayout />}>`
|
|
568
|
-
- `myspace` → `<Route path="/myspace" element={<AppLayout />}>`
|
|
569
|
-
3. Add the new routes **INSIDE** that `<Route>` block
|
|
570
|
-
4. If a tenant-prefixed block exists (`/t/:slug/...`), add the routes there too
|
|
850
|
+
### How DynamicRouter resolves routes
|
|
571
851
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
import { Create$MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage';
|
|
578
|
-
|
|
579
|
-
// Find the EXISTING layout route and add routes INSIDE it:
|
|
580
|
-
<Route path="/$APPLICATION" element={<$APPLICATION_Layout />}>
|
|
581
|
-
{/* ... existing routes stay here ... */}
|
|
582
|
-
|
|
583
|
-
{/* NEW: $MODULE routes - added as children of the layout */}
|
|
584
|
-
<Route path="$MODULE_KEBAB">
|
|
585
|
-
<Route index element={<Navigate to="." replace />} />
|
|
586
|
-
<Route index element={<$MODULE_PASCALPage />} />
|
|
587
|
-
<Route path="create" element={<Create$MODULE_PASCALPage />} />
|
|
588
|
-
<Route path=":id" element={<$MODULE_PASCALDetailPage />} />
|
|
589
|
-
<Route path=":id/edit" element={<Create$MODULE_PASCALPage />} />
|
|
590
|
-
</Route>
|
|
591
|
-
</Route>
|
|
592
|
-
```
|
|
852
|
+
1. Fetches menu from `GET /api/navigation/menu`
|
|
853
|
+
2. For each Application → Module → Section → Resource, builds `<Route>` elements
|
|
854
|
+
3. Looks up each item's `ComponentKey` in `PageRegistry` to get the React component
|
|
855
|
+
4. Wraps routes in `<ProtectedRoute>` (API-driven permission check via `isOpen` flag)
|
|
856
|
+
5. Generates both standard (`/$APPLICATION/$MODULE`) and tenant-prefixed (`/t/:slug/$APPLICATION/$MODULE`) routes automatically
|
|
593
857
|
|
|
594
|
-
### ⚠️ CRITICAL RULES
|
|
858
|
+
### ⚠️ CRITICAL RULES
|
|
595
859
|
|
|
596
|
-
**FORBIDDEN —
|
|
860
|
+
**FORBIDDEN — Flat routes outside DynamicRouter:**
|
|
597
861
|
```tsx
|
|
598
|
-
// ❌ WRONG — bypasses layout
|
|
599
|
-
|
|
600
|
-
{ path: '/$APPLICATION_KEBAB/$MODULE_KEBAB', element: <$MODULE_PASCALPage /> },
|
|
601
|
-
];
|
|
862
|
+
// ❌ WRONG — bypasses layout, permission checks, and tenant handling
|
|
863
|
+
<Route path="/$APPLICATION_KEBAB/$MODULE_KEBAB" element={<$MODULE_PASCALPage />} />
|
|
602
864
|
```
|
|
603
865
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
**FORBIDDEN — Flat routes outside layout:**
|
|
866
|
+
**FORBIDDEN — Registering pages without navigation seed data:**
|
|
607
867
|
```tsx
|
|
608
|
-
// ❌ WRONG (
|
|
609
|
-
|
|
868
|
+
// ❌ WRONG — PageRegistry.register() alone does nothing without a matching ComponentKey in the DB
|
|
869
|
+
PageRegistry.register('my-app.my-module', MyPage);
|
|
870
|
+
// Must also have INSERT INTO core.nav_Sections with ComponentKey = 'my-app.my-module'
|
|
610
871
|
```
|
|
611
872
|
|
|
612
|
-
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
### Legacy Patterns (DEPRECATED — removed in v3.7)
|
|
613
876
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
| AvatarMenu visible | No | Yes |
|
|
618
|
-
| Tenant prefix | Manual duplication | Automatic (mergeRoutes) |
|
|
619
|
-
| Auto-redirects | None | Generated for intermediate paths |
|
|
620
|
-
| Permission check | Bypassed (no RouteGuard) | Enforced by RouteGuard |
|
|
877
|
+
> **Pattern A (mergeRoutes)** and **Pattern B (JSX Routes)** were removed in v3.7.
|
|
878
|
+
> `smartstackRoutes.tsx`, `RouteGuard.tsx`, and `mergeRoutes()` no longer exist.
|
|
879
|
+
> If you encounter a pre-v3.7 project, upgrade to PageRegistry + DynamicRouter.
|
|
621
880
|
|
|
622
881
|
---
|
|
623
882
|
|
|
@@ -649,11 +908,13 @@ These patterns are **strictly prohibited** in generated frontend code:
|
|
|
649
908
|
| Check | Status |
|
|
650
909
|
|-------|--------|
|
|
651
910
|
| ☐ Main page created (`$MODULE_PASCALPage.tsx`) | |
|
|
911
|
+
| ☐ Detail page created (`$MODULE_PASCALDetailPage.tsx`) with `useParams<{ id: string }>` | |
|
|
912
|
+
| ☐ Edit page created (`$MODULE_PASCALEditPage.tsx`) with `useParams<{ id: string }>` | |
|
|
652
913
|
| ☐ ListView component created (`$MODULE_PASCALListView.tsx`) | |
|
|
653
914
|
| ☐ Preferences hook created (`use$MODULE_PASCALPreferences.ts`) | |
|
|
654
915
|
| ☐ API service created (uses `apiClient`, NOT raw axios) | |
|
|
655
|
-
| ☐
|
|
656
|
-
| ☐ Route
|
|
916
|
+
| ☐ PageRegistry keys registered (`.detail`, `.create`, `.edit`) in `componentRegistry.generated.ts` | |
|
|
917
|
+
| ☐ Route param uses `{ id: entityId }` destructuring (NOT `{ entityId }`) | |
|
|
657
918
|
|
|
658
919
|
### Theme Compliance
|
|
659
920
|
| Check | Status |
|
|
@@ -805,7 +805,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
805
805
|
else
|
|
806
806
|
{
|
|
807
807
|
app = NavigationApplication.Create(
|
|
808
|
-
"{app_code}", "{app_label_en}",
|
|
808
|
+
ApplicationZone.Business, "{app_code}", "{app_label_en}",
|
|
809
809
|
"{app_desc_en}", "{app_icon}", IconType.Lucide,
|
|
810
810
|
"/{app_code}", {display_order});
|
|
811
811
|
context.NavigationApplications.Add(app);
|
|
@@ -822,17 +822,23 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
822
822
|
|
|
823
823
|
// Module translations — IDEMPOTENT (unique index IX_nav_Translations_EntityType_EntityId_LanguageCode)
|
|
824
824
|
// CRITICAL: Always check before inserting to avoid duplicate key errors on re-runs
|
|
825
|
+
// CRITICAL: Use modEntity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
|
|
825
826
|
// if (!await context.NavigationTranslations.AnyAsync(
|
|
826
|
-
// t => t.EntityId ==
|
|
827
|
-
// { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
|
|
827
|
+
// t => t.EntityId == mod1Entity.Id && t.EntityType == NavigationEntityType.Module, ct))
|
|
828
|
+
// { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
|
|
829
|
+
// { context.NavigationTranslations.Add(
|
|
830
|
+
// NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description)); } }
|
|
828
831
|
|
|
829
832
|
// Sections (idempotent per-section — check each before inserting)
|
|
830
833
|
// var secExists = await context.NavigationSections.AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == ..., ct);
|
|
831
834
|
// Use NavigationSection.Create(moduleId, code, label, description, icon, iconType, route, displayOrder)
|
|
832
835
|
|
|
833
836
|
// Section translations — IDEMPOTENT (same guard pattern as module translations)
|
|
837
|
+
// CRITICAL: Resolve actual section from DB (secEntry.Id is seed-time GUID ≠ DB ID):
|
|
838
|
+
// var actualSection = await context.NavigationSections
|
|
839
|
+
// .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);
|
|
834
840
|
// if (!await context.NavigationTranslations.AnyAsync(
|
|
835
|
-
// t => t.EntityId ==
|
|
841
|
+
// t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
|
|
836
842
|
|
|
837
843
|
// Resources (idempotent per-resource — check each before inserting)
|
|
838
844
|
// var resExists = await context.NavigationResources.AnyAsync(r => r.Code == resEntry.Code && r.SectionId == ..., ct);
|
|
@@ -841,20 +847,26 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
841
847
|
|
|
842
848
|
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
843
849
|
{
|
|
844
|
-
//
|
|
850
|
+
// Resolve application from DB by Code — NOT seed-time GUID
|
|
851
|
+
var app = await context.NavigationApplications
|
|
852
|
+
.FirstAsync(a => a.Code == "{app_code}", ct);
|
|
853
|
+
|
|
854
|
+
// Check idempotence — scoped by actual DB ApplicationId
|
|
845
855
|
var exists = await context.Roles
|
|
846
|
-
.AnyAsync(r => r.ApplicationId ==
|
|
856
|
+
.AnyAsync(r => r.ApplicationId == app.Id, ct);
|
|
847
857
|
if (exists) return;
|
|
848
858
|
|
|
849
859
|
// Create application-scoped roles (Admin, Manager, Contributor, Viewer)
|
|
850
860
|
foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
|
|
851
861
|
{
|
|
852
862
|
var role = Role.Create(
|
|
853
|
-
entry.
|
|
854
|
-
entry.
|
|
855
|
-
|
|
856
|
-
entry.
|
|
857
|
-
entry.IsSystem
|
|
863
|
+
name: entry.Name,
|
|
864
|
+
shortName: entry.Code,
|
|
865
|
+
category: RoleCategory.Application,
|
|
866
|
+
description: entry.Description,
|
|
867
|
+
isSystem: entry.IsSystem,
|
|
868
|
+
applicationId: app.Id,
|
|
869
|
+
code: entry.Code);
|
|
858
870
|
context.Roles.Add(role);
|
|
859
871
|
}
|
|
860
872
|
await ((DbContext)context).SaveChangesAsync(ct);
|