@atlashub/smartstack-cli 3.28.0 → 3.30.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 +6 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/templates/project/api.ts.template +4 -2
- package/templates/project/appsettings.json.template +1 -1
- package/templates/skills/apex/_shared.md +13 -0
- package/templates/skills/apex/references/post-checks.md +228 -6
- package/templates/skills/apex/references/smartstack-api.md +67 -17
- package/templates/skills/apex/references/smartstack-frontend.md +41 -1
- package/templates/skills/apex/references/smartstack-layers.md +40 -10
- package/templates/skills/apex/steps/step-02-plan.md +16 -11
- package/templates/skills/apex/steps/step-03-execute.md +6 -0
- package/templates/skills/apex/steps/step-04-examine.md +4 -2
- package/templates/skills/application/references/frontend-verification.md +26 -1
- package/templates/skills/application/steps/step-03-roles.md +1 -1
- package/templates/skills/application/steps/step-05-frontend.md +24 -8
- package/templates/skills/application/templates-frontend.md +41 -22
- package/templates/skills/application/templates-seed.md +53 -16
- package/templates/skills/business-analyse/SKILL.md +4 -2
- package/templates/skills/business-analyse/_shared.md +17 -4
- package/templates/skills/business-analyse/react/schema.md +1 -1
- package/templates/skills/business-analyse/references/agent-module-prompt.md +40 -11
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +4 -3
- package/templates/skills/business-analyse/references/deploy-modes.md +1 -1
- package/templates/skills/business-analyse/references/handoff-file-templates.md +4 -4
- package/templates/skills/business-analyse/references/robustness-checks.md +12 -9
- package/templates/skills/business-analyse/references/spec-auto-inference.md +3 -3
- package/templates/skills/business-analyse/references/team-orchestration.md +57 -23
- package/templates/skills/business-analyse/references/ui-resource-cards.md +3 -3
- package/templates/skills/business-analyse/references/validation-checklist.md +21 -3
- package/templates/skills/business-analyse/schemas/sections/specification-schema.json +33 -5
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +12 -0
- package/templates/skills/business-analyse/steps/step-03b-ui.md +14 -2
- package/templates/skills/business-analyse/steps/step-03c-compile.md +17 -9
- package/templates/skills/business-analyse/steps/step-03d-validate.md +42 -2
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +5 -3
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +23 -15
- package/templates/skills/business-analyse/templates/tpl-handoff.md +10 -8
- package/templates/skills/business-analyse/templates/tpl-progress.md +7 -6
- package/templates/skills/ralph-loop/references/category-rules.md +50 -6
- package/templates/skills/ralph-loop/references/compact-loop.md +16 -1
- package/templates/skills/ralph-loop/references/core-seed-data.md +158 -38
- package/templates/skills/ralph-loop/references/task-transform-legacy.md +3 -3
- package/templates/skills/ralph-loop/steps/step-02-execute.md +109 -1
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
**Rules:**
|
|
65
65
|
- **ALL services MUST inject `ICurrentUserService` + `ICurrentTenantService` and filter by `TenantId`** (see `smartstack-api.md` Service Pattern)
|
|
66
|
-
- **ALL services MUST use guard clause:** `var tenantId = _currentTenant.TenantId ?? throw new
|
|
66
|
+
- **ALL services MUST use guard clause:** `var tenantId = _currentTenant.TenantId ?? throw new TenantContextRequiredException();` (returns 400, NOT 401 — a missing tenant is a bad request, not an auth failure)
|
|
67
67
|
- **ALL services MUST inject `ILogger<T>`** for production diagnostics
|
|
68
68
|
- CQRS with MediatR
|
|
69
69
|
- FluentValidation for all commands
|
|
@@ -112,17 +112,26 @@
|
|
|
112
112
|
| Roles seed | Templates below |
|
|
113
113
|
| Provider | Templates below |
|
|
114
114
|
|
|
115
|
-
### Seed Data Chain (
|
|
115
|
+
### Seed Data Chain (9 files minimum)
|
|
116
116
|
|
|
117
|
+
**Application-level (created ONCE, shared across modules):**
|
|
117
118
|
1. **NavigationApplicationSeedData.cs** — Application-level navigation entry (MUST be first)
|
|
118
|
-
2. **
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
2. **ApplicationRolesSeedData.cs** — Application-scoped roles (admin, manager, contributor, viewer) with deterministic GUIDs. Provides role entries for SeedRolesAsync().
|
|
120
|
+
|
|
121
|
+
**Per-module:**
|
|
122
|
+
3. **NavigationModuleSeedData.cs** — Deterministic GUIDs (SHA256), 4 languages (fr, en, it, de)
|
|
123
|
+
4. **NavigationSectionSeedData.cs** — Section-level navigation (if sections defined)
|
|
124
|
+
5. **NavigationResourceSeedData.cs** — Resource-level navigation (if resources defined)
|
|
125
|
+
6. **Permissions.cs** — Static permission constants: `public static class {Module} { public const string Read = "..."; }`. Referenced by `[RequirePermission(Permissions.{Module}.Read)]`.
|
|
126
|
+
7. **PermissionsSeedData.cs** — MCP `generate_permissions` first, fallback template
|
|
127
|
+
8. **RolesSeedData.cs** — Code-based role mapping: Admin=wildcard(*), Manager=CRU, Contributor=CR, Viewer=R. **NEVER use deterministic GUIDs for roles** — look up by Code at runtime.
|
|
128
|
+
|
|
129
|
+
**Infrastructure (created ONCE per application):**
|
|
130
|
+
9. **{App}SeedDataProvider.cs** — Implements IClientSeedDataProvider
|
|
124
131
|
- `SeedNavigationAsync()` — seeds Application → Module → Section → Resource + translations
|
|
125
|
-
- `
|
|
132
|
+
- `SeedRolesAsync()` — seeds application-scoped roles from ApplicationRolesSeedData
|
|
133
|
+
- `SeedPermissionsAsync()` — seeds permission entries from PermissionsSeedData
|
|
134
|
+
- `SeedRolePermissionsAsync()` — maps roles to permissions (roles resolved by Code, NOT by GUID)
|
|
126
135
|
- DI: `services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()`
|
|
127
136
|
|
|
128
137
|
### Deterministic GUID Pattern
|
|
@@ -177,7 +186,7 @@ private static Guid GenerateDeterministicGuid(string input)
|
|
|
177
186
|
|
|
178
187
|
```csharp
|
|
179
188
|
private static string ToKebabCase(string value)
|
|
180
|
-
=> System.Text.RegularExpressions.Regex.Replace(value, "(
|
|
189
|
+
=> System.Text.RegularExpressions.Regex.Replace(value, "([a-z])([A-Z])", "$1-$2").ToLowerInvariant();
|
|
181
190
|
|
|
182
191
|
// Route construction:
|
|
183
192
|
var appRoute = $"/{ToKebabCase(contextCode)}/{ToKebabCase(appCode)}";
|
|
@@ -190,6 +199,13 @@ var sectionRoute = $"{moduleRoute}/{ToKebabCase(sectionCode)}";
|
|
|
190
199
|
// → "/business/human-resources/employees/departments"
|
|
191
200
|
```
|
|
192
201
|
|
|
202
|
+
**ROUTE SPECIAL CASES (list and detail sections):**
|
|
203
|
+
> `list` and `detail` are view modes of the module, NOT functional sub-areas.
|
|
204
|
+
> - `list` section route = module route (e.g., `/business/human-resources/employees`) — NO `/list` suffix
|
|
205
|
+
> - `detail` section route = module route + `/:id` (e.g., `/business/human-resources/employees/:id`) — NOT `/detail/:id`
|
|
206
|
+
> - FORBIDDEN: `/employees/list`, `/employees/detail/:id`
|
|
207
|
+
> - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (normal)
|
|
208
|
+
|
|
193
209
|
**FORBIDDEN:**
|
|
194
210
|
- `Guid.NewGuid()` → use deterministic GUIDs (SHA256)
|
|
195
211
|
- `DeterministicGuid("nav:business")` for ContextId → context IDs are pre-seeded by SmartStack core, look up by code at runtime
|
|
@@ -259,6 +275,20 @@ const [loading, setLoading] = useState(true);
|
|
|
259
275
|
**CSS:** Variables only → `bg-[var(--bg-card)]`, `text-[var(--text-primary)]`, `border-[var(--border-color)]`
|
|
260
276
|
**Loader:** `Loader2` from `lucide-react` for spinners, `PageLoader` for Suspense fallback
|
|
261
277
|
|
|
278
|
+
### Section Routes (when module has sections)
|
|
279
|
+
|
|
280
|
+
If the module defines `{sections}`, generate frontend routes for EACH section as children of the module route:
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
// Section routes are nested inside the module's children array
|
|
284
|
+
{ path: '{section-kebab}', element: <Suspense fallback={<PageLoader />}><{Section}Page /></Suspense> },
|
|
285
|
+
{ path: '{section-kebab}/create', element: <Suspense fallback={<PageLoader />}><Create{Section}Page /></Suspense> },
|
|
286
|
+
{ path: '{section-kebab}/:id', element: <Suspense fallback={<PageLoader />}><{Section}DetailPage /></Suspense> },
|
|
287
|
+
{ path: '{section-kebab}/:id/edit', element: <Suspense fallback={<PageLoader />}><Edit{Section}Page /></Suspense> },
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Section pages live in `src/pages/{ContextPascal}/{AppPascal}/{Module}/{Section}/`.
|
|
291
|
+
|
|
262
292
|
### FK Fields in Forms (CRITICAL)
|
|
263
293
|
|
|
264
294
|
Any entity property that is a FK Guid (e.g., `EmployeeId`, `DepartmentId`) MUST use the `EntityLookup` component — NEVER a plain text input. Users cannot type GUIDs manually.
|
|
@@ -54,20 +54,25 @@ For EACH file in the plan, specify HOW it will be created/modified:
|
|
|
54
54
|
| 4 | Application/Services/.../MyService.cs | create | MCP scaffold_extension |
|
|
55
55
|
| 5 | Application/DTOs/.../MyDto.cs | create | MCP scaffold_extension |
|
|
56
56
|
| 6 | Api/Controllers/.../MyController.cs | create | /controller skill |
|
|
57
|
-
| 7 | Seeding/Data
|
|
58
|
-
|
|
|
59
|
-
|
|
|
57
|
+
| 7 | Seeding/Data/NavigationApplicationSeedData.cs | create | ref smartstack-layers.md (once per app) |
|
|
58
|
+
| 7b | Seeding/Data/ApplicationRolesSeedData.cs | create | ref smartstack-layers.md (once per app) |
|
|
59
|
+
| 8 | Seeding/Data/.../NavigationModuleSeedData.cs | create | ref smartstack-layers.md |
|
|
60
|
+
| 8b | Application/Authorization/Permissions.cs | create | MCP generate_permissions → static constants |
|
|
61
|
+
| 9 | Seeding/Data/.../PermissionsSeedData.cs | create | MCP generate_permissions |
|
|
62
|
+
| 10 | Seeding/Data/.../RolesSeedData.cs | create | ref smartstack-layers.md |
|
|
60
63
|
|
|
61
64
|
### Layer 2 — Frontend + I18n (parallel)
|
|
62
65
|
|
|
63
66
|
| # | File | Action | Tool |
|
|
64
67
|
|---|------|--------|------|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
68
|
+
| 11 | src/pages/{Ctx}/{App}/{Mod}/ListPage.tsx | create | /ui-components skill |
|
|
69
|
+
| 11b | src/pages/{Ctx}/{App}/{Mod}/CreatePage.tsx | create | /ui-components skill (EntityLookup for FK fields) |
|
|
70
|
+
| 11c | src/pages/{Ctx}/{App}/{Mod}/EditPage.tsx | create | /ui-components skill (EntityLookup for FK fields) |
|
|
71
|
+
| 11d | src/pages/{Ctx}/{App}/{Mod}/{Section}Page.tsx | create | /ui-components skill (per section in `{sections}`) |
|
|
72
|
+
| 11e | src/pages/{Ctx}/{App}/{Mod}/{Section}DetailPage.tsx | create | /ui-components skill (per section in `{sections}`) |
|
|
73
|
+
| 12 | src/services/api/{module}Api.ts | create | MCP scaffold_api_client |
|
|
74
|
+
| 13 | src/routes/{module}.tsx | create | MCP scaffold_routes |
|
|
75
|
+
| 14 | src/i18n/locales/{lang}/{module}.json | create | ref smartstack-frontend.md (4 languages: fr, en, it, de) |
|
|
71
76
|
|
|
72
77
|
**FK Field Guidance:** If step-01 identified `fkFields[]`, every Create/Edit page MUST use `EntityLookup` for those fields (see `smartstack-frontend.md` section 6). The corresponding backend GetAll endpoints (Layer 1) MUST support `?search=` parameter.
|
|
73
78
|
|
|
@@ -75,8 +80,8 @@ For EACH file in the plan, specify HOW it will be created/modified:
|
|
|
75
80
|
|
|
76
81
|
| # | File | Action | Tool |
|
|
77
82
|
|---|------|--------|------|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
83
|
+
| 15 | tests/.../MyEntityTests.cs | create | MCP scaffold_tests |
|
|
84
|
+
| 16 | tests/.../MyServiceTests.cs | create | MCP scaffold_tests |
|
|
80
85
|
```
|
|
81
86
|
|
|
82
87
|
---
|
|
@@ -77,6 +77,8 @@ Execute each item from the plan sequentially using skills and MCP.
|
|
|
77
77
|
- **FK FIELDS (CRITICAL):** Any Guid FK property (e.g., EmployeeId, DepartmentId) MUST use `EntityLookup` component — NEVER a plain text input. See `smartstack-frontend.md` section 6.
|
|
78
78
|
- **ZERO modals/popups/drawers for forms — ALL forms are full pages with their own URL**
|
|
79
79
|
- **Form tests: Generate `EntityCreatePage.test.tsx` and `EntityEditPage.test.tsx` (co-located)**
|
|
80
|
+
- **SECTION PERMISSIONS:** After calling `MCP generate_permissions` for the module navRoute (3 segments: `{context}.{app}.{module}`), also call it for EACH section navRoute (4 segments: `{context}.{app}.{module}.{section}`)
|
|
81
|
+
- **SECTION ROUTES:** After generating module routes, add section child routes to the module's `children` array. Wire `PermissionGuard` for section routes with section-level permissions.
|
|
80
82
|
- Read `references/smartstack-frontend.md` for mandatory patterns (sections 3b + 8)
|
|
81
83
|
- Generate i18n JSON files for all 4 languages (fr, en, it, de) — `src/i18n/locales/{lang}/{module}.json`
|
|
82
84
|
- All pages must follow loading → error → content pattern with CSS variables
|
|
@@ -134,6 +136,10 @@ Spawn 2 teammates (Opus, full tools):
|
|
|
134
136
|
→ EntityEditPage.test.tsx (co-located next to page)
|
|
135
137
|
→ Cover: rendering, validation, submit, pre-fill, navigation, errors
|
|
136
138
|
→ See smartstack-frontend.md section 8 for test templates
|
|
139
|
+
- **SECTION PERMISSIONS:** After MCP generate_permissions for the module navRoute (3 segments),
|
|
140
|
+
also call MCP generate_permissions for EACH section navRoute (4 segments: {context}.{app}.{module}.{section})
|
|
141
|
+
- **SECTION ROUTES:** Add section child routes to the module's children array.
|
|
142
|
+
Wire PermissionGuard for section routes with section-level permissions.
|
|
137
143
|
- I18n: Generate 4 JSON files per module namespace (fr, en, it, de)
|
|
138
144
|
→ Follow JSON template from smartstack-frontend.md
|
|
139
145
|
→ Keys: actions, labels, errors, validation, columns, form, messages, empty
|
|
@@ -132,10 +132,12 @@ sqlcmd -S "(localdb)\MSSQLLocalDB" -Q "IF DB_ID('$DB_NAME') IS NOT NULL BEGIN AL
|
|
|
132
132
|
| File | Checks |
|
|
133
133
|
|------|--------|
|
|
134
134
|
| NavigationApplicationSeedData | MUST be first, deterministic GUID for application (NOT for context — context is looked up by code), 4 lang translations |
|
|
135
|
+
| ApplicationRolesSeedData | 4 roles (admin, manager, contributor, viewer), deterministic GUIDs, references NavigationApplicationSeedData.ApplicationId |
|
|
135
136
|
| NavigationModuleSeedData | Deterministic GUIDs (SHA256), 4 languages, GetModuleEntry + GetTranslationEntries |
|
|
137
|
+
| Permissions.cs | Static constants class with `public static class {Module} { Read, Create, Update, Delete }`, paths match PermissionsSeedData |
|
|
136
138
|
| PermissionsSeedData | MCP generate_permissions used, paths match Permissions.cs, wildcard + CRUD |
|
|
137
|
-
| RolesSeedData | Admin=wildcard, Manager=CRU, Contributor=CR, Viewer=R |
|
|
138
|
-
| IClientSeedDataProvider |
|
|
139
|
+
| RolesSeedData | Admin=wildcard(*), Manager=CRU, Contributor=CR, Viewer=R. Role-permission entries reference permission paths from PermissionsSeedData |
|
|
140
|
+
| IClientSeedDataProvider | 4 Seed methods (Navigation, Roles, Permissions, RolePermissions), idempotent, factory methods, SaveChanges per group |
|
|
139
141
|
| DI Registration | `services.AddScoped<IClientSeedDataProvider, {App}SeedDataProvider>()` |
|
|
140
142
|
|
|
141
143
|
---
|
|
@@ -76,7 +76,7 @@ Verify exactly **4 language files** exist with identical key structures:
|
|
|
76
76
|
Verify routes are:
|
|
77
77
|
- **INSIDE** the Layout wrapper (`BusinessLayout`, `AdminLayout`, or `UserLayout`)
|
|
78
78
|
- **NESTED** (not flat)
|
|
79
|
-
- Following path convention: `/{context}/{
|
|
79
|
+
- Following path convention: `/{context}/{application_kebab}/{module_kebab}` (kebab-case)
|
|
80
80
|
|
|
81
81
|
### 5b. Route Wiring Check (BLOCKING)
|
|
82
82
|
|
|
@@ -98,6 +98,31 @@ Args:
|
|
|
98
98
|
|
|
99
99
|
If `appWiring.issues` is not empty, fix the wiring before proceeding.
|
|
100
100
|
|
|
101
|
+
### 5c. Route Kebab-Case Check (BLOCKING)
|
|
102
|
+
|
|
103
|
+
Scan App.tsx route paths for non-kebab-case segments:
|
|
104
|
+
|
|
105
|
+
**FORBIDDEN** — concatenated multi-word segments without hyphens:
|
|
106
|
+
- `humanresources`, `timemanagement`, `mymodule` (when representing multi-word names)
|
|
107
|
+
|
|
108
|
+
**REQUIRED** — proper kebab-case for multi-word segments:
|
|
109
|
+
- `human-resources`, `time-management`
|
|
110
|
+
|
|
111
|
+
**Verification steps:**
|
|
112
|
+
|
|
113
|
+
1. Extract all route path strings from App.tsx (both `contextRoutes` and `<Route path="...">`)
|
|
114
|
+
2. For each multi-word path segment, verify it uses hyphens (kebab-case)
|
|
115
|
+
3. Cross-reference route paths with navigation seed data routes to ensure exact match
|
|
116
|
+
4. Single-word segments are fine as-is (e.g., `employees`, `products`)
|
|
117
|
+
|
|
118
|
+
**Grep check** — verify no multi-word segments without hyphens in route paths:
|
|
119
|
+
```bash
|
|
120
|
+
# Should return NO matches for multi-word segments without hyphens
|
|
121
|
+
grep -E "path:\s*['\"]([a-z]+[A-Z]|[a-z]{2,}[a-z]{2,}/)" App.tsx
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If ANY non-kebab-case multi-word route segment is found → **FIX IT** before continuing.
|
|
125
|
+
|
|
101
126
|
### 6. States Check
|
|
102
127
|
|
|
103
128
|
Verify the list page includes:
|
|
@@ -251,7 +251,7 @@ rolePermissions.Add(new { RoleId = platformAdminRoleId, PermissionId = {permissi
|
|
|
251
251
|
// Application-scoped: Admin → wildcard
|
|
252
252
|
rolePermissions.Add(new { RoleId = appAdminRoleId, PermissionId = {permission_guids.wildcard}, AssignedAt = seedDate });
|
|
253
253
|
|
|
254
|
-
// Application-scoped: Manager →
|
|
254
|
+
// Application-scoped: Manager → CRU (read + create + update — no delete)
|
|
255
255
|
rolePermissions.Add(new { RoleId = appManagerRoleId, PermissionId = {permission_guids.read}, AssignedAt = seedDate });
|
|
256
256
|
rolePermissions.Add(new { RoleId = appManagerRoleId, PermissionId = {permission_guids.create}, AssignedAt = seedDate });
|
|
257
257
|
rolePermissions.Add(new { RoleId = appManagerRoleId, PermissionId = {permission_guids.update}, AssignedAt = seedDate });
|
|
@@ -35,6 +35,22 @@ From previous steps:
|
|
|
35
35
|
| `{entity_code}` | kebab-case code |
|
|
36
36
|
| `{labels}` | Object with fr, en, it, de |
|
|
37
37
|
| `{api_route}` | API endpoint path |
|
|
38
|
+
| `{application_kebab}` | kebab-case route segment for application (PascalCase → kebab-case, e.g., `HumanResources` → `human-resources`) |
|
|
39
|
+
| `{module_kebab}` | kebab-case route segment for module (PascalCase → kebab-case, e.g., `TimeManagement` → `time-management`) |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ROUTE NAMING RULE
|
|
44
|
+
|
|
45
|
+
> **BLOCKING:** All route paths MUST use **kebab-case** to match navigation seed data exactly.
|
|
46
|
+
>
|
|
47
|
+
> - Transformation: `PascalCase` → `kebab-case` (e.g., `HumanResources` → `human-resources`)
|
|
48
|
+
> - Single words stay lowercase: `Employees` → `employees`
|
|
49
|
+
> - Multi-word segments MUST have hyphens: `TimeManagement` → `time-management`
|
|
50
|
+
> - **FORBIDDEN:** `humanresources`, `timemanagement` (concatenated words without hyphens)
|
|
51
|
+
> - **REQUIRED:** `human-resources`, `time-management` (proper kebab-case)
|
|
52
|
+
>
|
|
53
|
+
> Route paths MUST match the navigation seed data routes (which use `ToKebabCase()`).
|
|
38
54
|
|
|
39
55
|
---
|
|
40
56
|
|
|
@@ -123,10 +139,10 @@ Read App.tsx and detect which pattern is used:
|
|
|
123
139
|
const contextRoutes: ContextRouteExtensions = {
|
|
124
140
|
business: [
|
|
125
141
|
// existing routes...
|
|
126
|
-
{ path: '{
|
|
127
|
-
{ path: '{
|
|
128
|
-
{ path: '{
|
|
129
|
-
{ path: '{
|
|
142
|
+
{ path: '{application_kebab}/{module_kebab}', element: <{EntityName}ListPage /> },
|
|
143
|
+
{ path: '{application_kebab}/{module_kebab}/new', element: <Create{EntityName}Page /> },
|
|
144
|
+
{ path: '{application_kebab}/{module_kebab}/:id', element: <{EntityName}DetailPage /> },
|
|
145
|
+
{ path: '{application_kebab}/{module_kebab}/:id/edit', element: <Create{EntityName}Page /> },
|
|
130
146
|
],
|
|
131
147
|
};
|
|
132
148
|
```
|
|
@@ -139,7 +155,7 @@ Read App.tsx and detect which pattern is used:
|
|
|
139
155
|
```tsx
|
|
140
156
|
<Route path="/business" element={<BusinessLayout />}>
|
|
141
157
|
{/* ... existing routes ... */}
|
|
142
|
-
<Route path="{
|
|
158
|
+
<Route path="{application_kebab}/{module_kebab}" element={<{EntityName}Page />} />
|
|
143
159
|
</Route>
|
|
144
160
|
```
|
|
145
161
|
|
|
@@ -211,9 +227,9 @@ If the generated translations need customization (e.g., replacing generic title
|
|
|
211
227
|
### Route Configuration
|
|
212
228
|
```tsx
|
|
213
229
|
// In routes.tsx - NESTED routes (not flat!)
|
|
214
|
-
<Route path="{
|
|
215
|
-
<Route index element={<Navigate to="{
|
|
216
|
-
<Route path="{
|
|
230
|
+
<Route path="{application_kebab}">
|
|
231
|
+
<Route index element={<Navigate to="{module_kebab}" replace />} />
|
|
232
|
+
<Route path="{module_kebab}" element={<{EntityName}Page />} />
|
|
217
233
|
</Route>
|
|
218
234
|
```
|
|
219
235
|
```
|
|
@@ -32,6 +32,26 @@ web/smartstack-web/src/
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
+
## ROUTE PATH VARIABLES
|
|
36
|
+
|
|
37
|
+
> **All route path segments MUST use kebab-case to match the navigation seed data.**
|
|
38
|
+
|
|
39
|
+
| Variable | Description | Example |
|
|
40
|
+
|----------|-------------|---------|
|
|
41
|
+
| `$APPLICATION_KEBAB` | kebab-case of application code | `human-resources` |
|
|
42
|
+
| `$MODULE_KEBAB` | kebab-case of module code | `time-management` |
|
|
43
|
+
|
|
44
|
+
**Transformation rule:** `PascalCase` → `kebab-case` via regex `([a-z])([A-Z])` → `$1-$2` + `toLowerCase()`
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
- `HumanResources` → `human-resources`
|
|
48
|
+
- `TimeManagement` → `time-management`
|
|
49
|
+
- `Employees` → `employees` (single word, no change needed)
|
|
50
|
+
|
|
51
|
+
> **FORBIDDEN:** Using raw `$APPLICATION` or `$MODULE` in route paths — they may produce non-kebab-case URLs (e.g., `humanresources` instead of `human-resources`). Always use `$APPLICATION_KEBAB` and `$MODULE_KEBAB` for route paths.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
35
55
|
## TEMPLATE: MAIN PAGE
|
|
36
56
|
|
|
37
57
|
```tsx
|
|
@@ -47,8 +67,7 @@ export function $MODULE_PASCALPage() {
|
|
|
47
67
|
<$MODULE_PASCALListView
|
|
48
68
|
title={t('$module:title')}
|
|
49
69
|
subtitle={t('$module:subtitle')}
|
|
50
|
-
|
|
51
|
-
createPath="/$CONTEXT/$APPLICATION/$MODULE/new"
|
|
70
|
+
createPath="new"
|
|
52
71
|
/>
|
|
53
72
|
);
|
|
54
73
|
}
|
|
@@ -91,7 +110,6 @@ interface PagedResult {
|
|
|
91
110
|
interface $MODULE_PASCALListViewProps {
|
|
92
111
|
title: string;
|
|
93
112
|
subtitle?: string;
|
|
94
|
-
basePath: string;
|
|
95
113
|
createPath?: string;
|
|
96
114
|
}
|
|
97
115
|
|
|
@@ -100,7 +118,6 @@ const DEFAULT_COLUMNS = ['name', 'description', 'isActive', 'createdAt'];
|
|
|
100
118
|
export function $MODULE_PASCALListView({
|
|
101
119
|
title,
|
|
102
120
|
subtitle,
|
|
103
|
-
basePath,
|
|
104
121
|
createPath,
|
|
105
122
|
}: $MODULE_PASCALListViewProps) {
|
|
106
123
|
const { t } = useTranslation(['$module', 'common']);
|
|
@@ -270,7 +287,7 @@ export function $MODULE_PASCALListView({
|
|
|
270
287
|
<tr
|
|
271
288
|
key={item.id}
|
|
272
289
|
className="border-b border-[var(--item-color-border)] hover:bg-[var(--bg-hover)] cursor-pointer"
|
|
273
|
-
onClick={() => navigate(
|
|
290
|
+
onClick={() => navigate(item.id)}
|
|
274
291
|
>
|
|
275
292
|
{visibleColumns.includes('name') && (
|
|
276
293
|
<td className="px-4 py-3 text-sm text-[var(--text-primary)] font-medium">
|
|
@@ -306,7 +323,7 @@ export function $MODULE_PASCALListView({
|
|
|
306
323
|
<button
|
|
307
324
|
onClick={(e) => {
|
|
308
325
|
e.stopPropagation();
|
|
309
|
-
navigate(`${
|
|
326
|
+
navigate(`${item.id}/edit`);
|
|
310
327
|
}}
|
|
311
328
|
className="p-1 hover:bg-[var(--bg-tertiary)]"
|
|
312
329
|
style={{ borderRadius: 'var(--radius-button)' }}
|
|
@@ -346,9 +363,9 @@ export function $MODULE_PASCALListView({
|
|
|
346
363
|
color: 'var(--success-text)'
|
|
347
364
|
} : undefined}
|
|
348
365
|
stats={`${t('common:createdAt')}: ${new Date(item.createdAt).toLocaleDateString()}`}
|
|
349
|
-
onClick={() => navigate(
|
|
366
|
+
onClick={() => navigate(item.id)}
|
|
350
367
|
actions={[
|
|
351
|
-
{ label: t('common:actions.view'), onClick: () => navigate(
|
|
368
|
+
{ label: t('common:actions.view'), onClick: () => navigate(item.id), variant: 'primary' },
|
|
352
369
|
]}
|
|
353
370
|
/>
|
|
354
371
|
))}
|
|
@@ -512,15 +529,15 @@ import { Create$MODULE_PASCALPage } from '@/pages/$CONTEXT/$APPLICATION/$MODULE/
|
|
|
512
529
|
const contextRoutes: ContextRouteExtensions = {
|
|
513
530
|
$CONTEXT: [
|
|
514
531
|
// ... existing routes ...
|
|
515
|
-
{ path: '$
|
|
516
|
-
{ path: '$
|
|
517
|
-
{ path: '$
|
|
518
|
-
{ path: '$
|
|
532
|
+
{ path: '$APPLICATION_KEBAB/$MODULE_KEBAB', element: <$MODULE_PASCALPage /> },
|
|
533
|
+
{ path: '$APPLICATION_KEBAB/$MODULE_KEBAB/new', element: <Create$MODULE_PASCALPage /> },
|
|
534
|
+
{ path: '$APPLICATION_KEBAB/$MODULE_KEBAB/:id', element: <$MODULE_PASCALDetailPage /> },
|
|
535
|
+
{ path: '$APPLICATION_KEBAB/$MODULE_KEBAB/:id/edit', element: <Create$MODULE_PASCALPage /> },
|
|
519
536
|
],
|
|
520
537
|
};
|
|
521
538
|
```
|
|
522
539
|
|
|
523
|
-
**mergeRoutes auto-generates redirects** for intermediate paths (e.g., `$
|
|
540
|
+
**mergeRoutes auto-generates redirects** for intermediate paths (e.g., `$APPLICATION_KEBAB` → `$APPLICATION_KEBAB/$DEFAULT_MODULE_KEBAB`) so you don't need to add index redirects manually.
|
|
524
541
|
|
|
525
542
|
**Context-to-Layout mapping (automatic via mergeRoutes):**
|
|
526
543
|
|
|
@@ -560,12 +577,12 @@ import { Create$MODULE_PASCALPage } from '@/pages/$CONTEXT/$APPLICATION/$MODULE/
|
|
|
560
577
|
{/* ... existing routes stay here ... */}
|
|
561
578
|
|
|
562
579
|
{/* NEW: $APPLICATION routes - added as children of the layout */}
|
|
563
|
-
<Route path="$
|
|
564
|
-
<Route index element={<Navigate to="$
|
|
565
|
-
<Route path="$
|
|
566
|
-
<Route path="$
|
|
567
|
-
<Route path="$
|
|
568
|
-
<Route path="$
|
|
580
|
+
<Route path="$APPLICATION_KEBAB">
|
|
581
|
+
<Route index element={<Navigate to="$DEFAULT_MODULE_KEBAB" replace />} />
|
|
582
|
+
<Route path="$MODULE_KEBAB" element={<$MODULE_PASCALPage />} />
|
|
583
|
+
<Route path="$MODULE_KEBAB/new" element={<Create$MODULE_PASCALPage />} />
|
|
584
|
+
<Route path="$MODULE_KEBAB/:id" element={<$MODULE_PASCALDetailPage />} />
|
|
585
|
+
<Route path="$MODULE_KEBAB/:id/edit" element={<Create$MODULE_PASCALPage />} />
|
|
569
586
|
</Route>
|
|
570
587
|
</Route>
|
|
571
588
|
```
|
|
@@ -576,7 +593,7 @@ import { Create$MODULE_PASCALPage } from '@/pages/$CONTEXT/$APPLICATION/$MODULE/
|
|
|
576
593
|
```tsx
|
|
577
594
|
// ❌ WRONG — bypasses layout entirely, shell will NOT render
|
|
578
595
|
const clientRoutes: RouteConfig[] = [
|
|
579
|
-
{ path: '/business/$
|
|
596
|
+
{ path: '/business/$APPLICATION_KEBAB/$MODULE_KEBAB', element: <$MODULE_PASCALPage /> },
|
|
580
597
|
];
|
|
581
598
|
```
|
|
582
599
|
|
|
@@ -585,7 +602,7 @@ const clientRoutes: RouteConfig[] = [
|
|
|
585
602
|
**FORBIDDEN — Flat routes outside layout:**
|
|
586
603
|
```tsx
|
|
587
604
|
// ❌ WRONG (Pattern B only) — flat route bypasses layout
|
|
588
|
-
<Route path="/$CONTEXT/$
|
|
605
|
+
<Route path="/$CONTEXT/$APPLICATION_KEBAB/$MODULE_KEBAB" element={<$MODULE_PASCALPage />} />
|
|
589
606
|
```
|
|
590
607
|
|
|
591
608
|
### Why nested/context routes?
|
|
@@ -612,6 +629,8 @@ These patterns are **strictly prohibited** in generated frontend code:
|
|
|
612
629
|
| `rounded-lg`, `rounded-md` | `rounded-[var(--radius-card)]`, `rounded-[var(--radius-button)]` |
|
|
613
630
|
| Custom `<div>` cards for entity lists | `<EntityCard>` component (MANDATORY) |
|
|
614
631
|
| Flat routes outside Layout wrapper | Nested routes inside `<BusinessLayout>` / `<AdminLayout>` |
|
|
632
|
+
| Route paths with PascalCase or concatenated words (e.g., `humanresources`, `timeManagement`) | Kebab-case route paths (e.g., `human-resources`, `time-management`) |
|
|
633
|
+
| `navigate(\`${basePath}/${item.id}\`)` in list row click | `navigate(item.id)` (relative navigation, no path duplication) |
|
|
615
634
|
| Only 2 languages (fr/en) | All 4 languages (fr/en/it/de) |
|
|
616
635
|
| `[Authorize]` without permissions | `[RequirePermission("navRoute.action")]` per endpoint |
|
|
617
636
|
| Missing error state | Error state with retry button (AlertCircle + underline) |
|
|
@@ -630,7 +649,7 @@ These patterns are **strictly prohibited** in generated frontend code:
|
|
|
630
649
|
| ☐ Preferences hook created (`use$MODULE_PASCALPreferences.ts`) | |
|
|
631
650
|
| ☐ API service created (uses `apiClient`, NOT raw axios) | |
|
|
632
651
|
| ☐ Routes added inside Layout wrapper in App.tsx | |
|
|
633
|
-
| ☐ Route path follows `/{context}/{
|
|
652
|
+
| ☐ Route path follows `/{context}/{application_kebab}/{module_kebab}` (kebab-case) | |
|
|
634
653
|
|
|
635
654
|
### Theme Compliance
|
|
636
655
|
| Check | Status |
|
|
@@ -843,7 +843,7 @@ services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
|
|
|
843
843
|
InitializeSmartStackAsync()
|
|
844
844
|
1. MigrateAsync() -> Core schema created
|
|
845
845
|
2. IDatabaseSeeder.SeedAsync() -> System users, tenant
|
|
846
|
-
3. IClientSeedDataProvider.Seed*() -> Navigation + Permissions +
|
|
846
|
+
3. IClientSeedDataProvider.Seed*() -> Navigation + Roles + Permissions + RolePermissions CLIENT
|
|
847
847
|
4. IDevDataSeeder.SeedAsync() -> Dev data
|
|
848
848
|
5. InitializeNavigationRoutingAsync -> Routes loaded (including client routes)
|
|
849
849
|
```
|
|
@@ -864,21 +864,37 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
864
864
|
|
|
865
865
|
public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct)
|
|
866
866
|
{
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
867
|
+
// NOTE: Idempotence is at MODULE level (not application level).
|
|
868
|
+
// If the application already exists, we reuse it and seed any missing modules.
|
|
869
|
+
// This allows adding Module 2+ to an existing application.
|
|
870
|
+
var existingApp = await context.NavigationApplications
|
|
871
|
+
.FirstOrDefaultAsync(a => a.Code == "{app_code}", ct);
|
|
872
|
+
|
|
873
|
+
NavigationApplication app;
|
|
874
|
+
if (existingApp != null)
|
|
875
|
+
{
|
|
876
|
+
app = existingApp; // Application already seeded — reuse for module seeding below
|
|
877
|
+
}
|
|
878
|
+
else
|
|
879
|
+
{
|
|
880
|
+
var parentContext = await context.NavigationContexts
|
|
881
|
+
.FirstAsync(c => c.Code == "{context_code}", ct);
|
|
882
|
+
|
|
883
|
+
app = NavigationApplication.Create(
|
|
884
|
+
parentContext.Id, "{app_code}", "{app_label_en}",
|
|
885
|
+
"{app_desc_en}", "{app_icon}", IconType.Lucide,
|
|
886
|
+
"/{context_code}/{app_code}", {display_order});
|
|
887
|
+
context.NavigationApplications.Add(app);
|
|
888
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
889
|
+
|
|
890
|
+
// Application translations (only when creating new application)
|
|
891
|
+
// foreach (var t in NavigationApplicationSeedData.GetTranslationEntries()) { ... }
|
|
892
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
893
|
+
}
|
|
880
894
|
|
|
881
|
-
// Create modules
|
|
895
|
+
// Create modules (idempotent per-module — check each before inserting)
|
|
896
|
+
// var mod1Exists = await context.NavigationModules.AnyAsync(m => m.Code == "{module_code}" && m.ApplicationId == app.Id, ct);
|
|
897
|
+
// if (!mod1Exists) { var mod1 = NavigationModule.Create(...); context.NavigationModules.Add(mod1); }
|
|
882
898
|
// Create module translations from {Module}NavigationSeedData.GetTranslationEntries()
|
|
883
899
|
|
|
884
900
|
// Create sections from {Module}NavigationSeedData.GetSectionEntries(moduleId)
|
|
@@ -889,6 +905,27 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
889
905
|
// Use NavigationResource.Create(sectionId, code, label, entityType, route, displayOrder)
|
|
890
906
|
}
|
|
891
907
|
|
|
908
|
+
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
909
|
+
{
|
|
910
|
+
// Check idempotence
|
|
911
|
+
var exists = await context.Roles
|
|
912
|
+
.AnyAsync(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId, ct);
|
|
913
|
+
if (exists) return;
|
|
914
|
+
|
|
915
|
+
// Create application-scoped roles (Admin, Manager, Contributor, Viewer)
|
|
916
|
+
foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
|
|
917
|
+
{
|
|
918
|
+
var role = Role.Create(
|
|
919
|
+
entry.Code,
|
|
920
|
+
entry.Name,
|
|
921
|
+
entry.Description,
|
|
922
|
+
entry.ApplicationId,
|
|
923
|
+
entry.IsSystem);
|
|
924
|
+
context.Roles.Add(role);
|
|
925
|
+
}
|
|
926
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
927
|
+
}
|
|
928
|
+
|
|
892
929
|
public async Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct)
|
|
893
930
|
{
|
|
894
931
|
var exists = await context.Permissions
|
|
@@ -917,7 +954,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
917
954
|
|------|-------------|
|
|
918
955
|
| Factory methods | `NavigationModule.Create(...)`, `NavigationSection.Create(...)`, `NavigationResource.Create(...)`, `Permission.CreateForModule(...)` - NEVER `new Entity()` |
|
|
919
956
|
| Idempotence | Each Seed method checks existence before inserting |
|
|
920
|
-
| SaveChanges per group | Navigation
|
|
957
|
+
| SaveChanges per group | Navigation -> save -> Roles -> save -> Permissions -> save -> RolePermissions -> save |
|
|
921
958
|
| Deterministic GUIDs | Use IDs from SeedData classes (not `Guid.NewGuid()`) |
|
|
922
959
|
| FK resolution by Code | Parent modules found by `Code`, not hardcoded GUID |
|
|
923
960
|
| Section/Resource conditionality | Only generate if `seedDataCore.navigationSections` / `seedDataCore.navigationResources` exist in feature.json |
|
|
@@ -94,15 +94,17 @@ docs/business/
|
|
|
94
94
|
|
|
95
95
|
**SeedData generation (step-03c):**
|
|
96
96
|
|
|
97
|
-
The `specification.seedDataCore` contains **
|
|
97
|
+
The `specification.seedDataCore` contains **9 mandatory arrays** for database seeding:
|
|
98
98
|
|
|
99
99
|
| Array | Table | Source | Description |
|
|
100
100
|
|-------|-------|--------|-------------|
|
|
101
|
+
| `navigationApplications` | `core.nav_Applications` | Manual (step-03c) | Application entry (Level 2) — created ONCE per application |
|
|
102
|
+
| `applicationRoles` | `core.auth_Roles` | Manual (step-03c) | Application-scoped roles (admin, manager, contributor, viewer) |
|
|
101
103
|
| `navigationModules` | `core.nav_Modules` | Manual (step-03c) | Module entry (Level 3) |
|
|
102
104
|
| **`navigationSections`** | **`core.nav_Sections`** | **Derived from `specification.sections[]`** | **Section entries (Level 4)** |
|
|
103
105
|
| **`navigationResources`** | **`core.nav_Resources`** | **Derived from `specification.sections[].resources[]`** | **Resource entries (Level 5)** |
|
|
104
106
|
| `navigationTranslations` | `core.nav_Translations` | Manual (i18n) | Multi-language labels |
|
|
105
|
-
| `permissions` | `core.Permissions` | Manual (step-03c) | Permission definitions |
|
|
107
|
+
| `permissions` | `core.Permissions` | Manual (step-03c) | Permission definitions ({path, action, description}) |
|
|
106
108
|
| `rolePermissions` | `core.RolePermissions` | Manual (step-03c) | Role-permission mappings |
|
|
107
109
|
| `permissionConstants` | `PermissionConstants.cs` | Manual (step-03c) | C# permission constants |
|
|
108
110
|
|
|
@@ -84,17 +84,25 @@ Level 1: Context (business)
|
|
|
84
84
|
|
|
85
85
|
### Route patterns
|
|
86
86
|
```
|
|
87
|
-
/business/{application}/{module}
|
|
88
|
-
/business/{application}/{module}
|
|
87
|
+
/business/{application}/{module} ← list section (= module route, NO /list suffix)
|
|
88
|
+
/business/{application}/{module}/:id ← detail page (= module route + /:id, NO /detail prefix)
|
|
89
|
+
/business/{application}/{module}/{section} ← other sidebar sections (dashboard, approve, import, etc.)
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
**Detail page routing:**
|
|
92
93
|
- The detail page uses a parameterized route with `:id`
|
|
93
94
|
- It is NOT registered as a sidebar navigation entry (`navigation: "hidden"`)
|
|
94
95
|
- It is reached by clicking a row in the `list` section (action: `navigate:detail`)
|
|
95
|
-
- The back button navigates to `/business/{application}/{module}
|
|
96
|
+
- The back button navigates to `/business/{application}/{module}`
|
|
96
97
|
- Route MUST be registered in frontend routing (App.tsx) alongside the list route
|
|
97
98
|
|
|
99
|
+
> **ROUTE SPECIAL CASES (list and detail sections):**
|
|
100
|
+
> `list` and `detail` are view modes of the module, NOT functional sub-areas.
|
|
101
|
+
> - `list` route = module route (NO `/list` suffix) — React Router index route
|
|
102
|
+
> - `detail` route = module route + `/:id` (NO `/detail/` prefix) — React Router `:id` child
|
|
103
|
+
> - FORBIDDEN in navigation seed: `/module/list`, `/module/detail/:id`
|
|
104
|
+
> - Other sections (dashboard, approve, import) add `/{section-kebab}` normally
|
|
105
|
+
|
|
98
106
|
### Standard Sections per Module
|
|
99
107
|
|
|
100
108
|
> **RULE: Sections are functional zones, NOT CRUD operations.**
|
|
@@ -196,7 +204,12 @@ generate_feature_id():
|
|
|
196
204
|
|
|
197
205
|
## Permission Path Format
|
|
198
206
|
|
|
199
|
-
|
|
207
|
+
| Level | Format | Example |
|
|
208
|
+
|-------|--------|---------|
|
|
209
|
+
| Module | `{context}.{app}.{module}.{action}` | `business.sales.orders.read` |
|
|
210
|
+
| Section | `{context}.{app}.{module}.{section}.{action}` | `business.sales.orders.dashboard.read` |
|
|
211
|
+
|
|
212
|
+
> **Rule:** Use module-level permissions for standard CRUD. Use section-level permissions only when a section requires distinct access control (e.g., dashboard, approve, import).
|
|
200
213
|
|
|
201
214
|
| Action | Permission | Usage |
|
|
202
215
|
|--------|------------|-------|
|
|
@@ -489,7 +489,7 @@ export interface SeedDataNavTranslation {
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
export interface SeedDataPermission {
|
|
492
|
-
path: string; //
|
|
492
|
+
path: string; // Module-level: business.{app}.{module}.{action} or section-level: business.{app}.{module}.{section}.{action}
|
|
493
493
|
action: string;
|
|
494
494
|
description?: string;
|
|
495
495
|
}
|