@atlashub/smartstack-cli 3.22.0 → 3.24.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/mcp-entry.mjs +143 -174
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/component.tsx.hbs +21 -1
- package/templates/skills/apex/SKILL.md +21 -0
- package/templates/skills/apex/references/smartstack-api.md +507 -0
- package/templates/skills/apex/references/smartstack-frontend.md +1081 -0
- package/templates/skills/apex/references/smartstack-layers.md +166 -20
- package/templates/skills/apex/steps/step-00-init.md +27 -14
- package/templates/skills/apex/steps/step-01-analyze.md +45 -3
- package/templates/skills/apex/steps/step-02-plan.md +5 -1
- package/templates/skills/apex/steps/step-03-execute.md +51 -9
- package/templates/skills/apex/steps/step-04-validate.md +251 -0
- package/templates/skills/apex/steps/step-05-examine.md +7 -0
- package/templates/skills/apex/steps/step-07-tests.md +48 -5
- package/templates/skills/business-analyse/_shared.md +6 -6
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +1 -1
- package/templates/skills/business-analyse/questionnaire/07-ui.md +3 -3
- package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +1 -1
- package/templates/skills/business-analyse/references/entity-architecture-decision.md +3 -3
- package/templates/skills/business-analyse/references/handoff-file-templates.md +13 -5
- package/templates/skills/business-analyse/references/spec-auto-inference.md +14 -14
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +2 -2
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +1 -1
- package/templates/skills/business-analyse/steps/step-03a1-setup.md +2 -2
- package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -1
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +15 -4
- package/templates/skills/business-analyse/templates/tpl-frd.md +2 -2
- package/templates/skills/business-analyse/templates-frd.md +2 -2
- package/templates/skills/ralph-loop/references/category-rules.md +45 -7
- package/templates/skills/ralph-loop/references/compact-loop.md +2 -2
- package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
- package/templates/skills/ralph-loop/steps/step-02-execute.md +110 -1
- package/templates/skills/validate-feature/steps/step-05-db-validation.md +86 -1
|
@@ -126,6 +126,7 @@ sqlcmd -S "(localdb)\MSSQLLocalDB" -Q "IF DB_ID('$DB_NAME') IS NOT NULL BEGIN AL
|
|
|
126
126
|
|
|
127
127
|
| File | Checks |
|
|
128
128
|
|------|--------|
|
|
129
|
+
| NavigationApplicationSeedData | MUST be first, deterministic GUID, 4 lang translations |
|
|
129
130
|
| NavigationModuleSeedData | Deterministic GUIDs (SHA256), 4 languages, GetModuleEntry + GetTranslationEntries |
|
|
130
131
|
| PermissionsSeedData | MCP generate_permissions used, paths match Permissions.cs, wildcard + CRUD |
|
|
131
132
|
| RolesSeedData | Admin=wildcard, Manager=CRU, Contributor=CR, Viewer=R |
|
|
@@ -134,6 +135,248 @@ sqlcmd -S "(localdb)\MSSQLLocalDB" -Q "IF DB_ID('$DB_NAME') IS NOT NULL BEGIN AL
|
|
|
134
135
|
|
|
135
136
|
---
|
|
136
137
|
|
|
138
|
+
## 6b. BLOCKING POST-CHECKs (bash verification on real files)
|
|
139
|
+
|
|
140
|
+
> These checks run on the actual generated files. Model-interpreted checks are unreliable.
|
|
141
|
+
|
|
142
|
+
### POST-CHECK 1: Navigation routes must be full paths starting with /
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Find all seed data files and check route values
|
|
146
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
147
|
+
if [ -n "$SEED_FILES" ]; then
|
|
148
|
+
# Check for short routes (no leading /) in Create() calls for navigation entities
|
|
149
|
+
BAD_ROUTES=$(grep -Pn 'NavigationApplication\.Create\(|NavigationModule\.Create\(|NavigationSection\.Create\(|NavigationResource\.Create\(' $SEED_FILES | grep -v '"/[a-z]')
|
|
150
|
+
if [ -n "$BAD_ROUTES" ]; then
|
|
151
|
+
echo "BLOCKING: Navigation routes must be full paths starting with /"
|
|
152
|
+
echo "$BAD_ROUTES"
|
|
153
|
+
echo "Expected: \"/business/human-resources\" NOT \"humanresources\""
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
fi
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### POST-CHECK 2: All services must filter by TenantId (OWASP A01)
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Find all service implementation files
|
|
163
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
164
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
165
|
+
# Check each service file has TenantId reference (either _currentUser.TenantId or TenantId filter)
|
|
166
|
+
for f in $SERVICE_FILES; do
|
|
167
|
+
if ! grep -q "TenantId" "$f"; then
|
|
168
|
+
echo "BLOCKING (OWASP A01): Service missing TenantId filter: $f"
|
|
169
|
+
echo "Every service query MUST filter by _currentUser.TenantId"
|
|
170
|
+
exit 1
|
|
171
|
+
fi
|
|
172
|
+
if grep -q "Guid.Empty" "$f"; then
|
|
173
|
+
echo "BLOCKING (OWASP A01): Service uses Guid.Empty instead of _currentUser.TenantId: $f"
|
|
174
|
+
exit 1
|
|
175
|
+
fi
|
|
176
|
+
done
|
|
177
|
+
fi
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### POST-CHECK 3: Controllers must use [RequirePermission], not just [Authorize]
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Find all controller files
|
|
184
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
185
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
186
|
+
for f in $CTRL_FILES; do
|
|
187
|
+
# Check controller has at least one RequirePermission attribute
|
|
188
|
+
if grep -q "\[Authorize\]" "$f" && ! grep -q "\[RequirePermission" "$f"; then
|
|
189
|
+
echo "WARNING: Controller uses [Authorize] without [RequirePermission]: $f"
|
|
190
|
+
echo "Use [RequirePermission(Permissions.{Module}.{Action})] on each endpoint"
|
|
191
|
+
fi
|
|
192
|
+
done
|
|
193
|
+
fi
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### POST-CHECK 4: Seed data must not use Guid.NewGuid()
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
SEED_FILES=$(find src/ -path "*/Seeding/Data/*" -name "*.cs" 2>/dev/null)
|
|
200
|
+
if [ -n "$SEED_FILES" ]; then
|
|
201
|
+
BAD_GUIDS=$(grep -n "Guid.NewGuid()" $SEED_FILES 2>/dev/null)
|
|
202
|
+
if [ -n "$BAD_GUIDS" ]; then
|
|
203
|
+
echo "BLOCKING: Seed data must use deterministic GUIDs (SHA256), not Guid.NewGuid()"
|
|
204
|
+
echo "$BAD_GUIDS"
|
|
205
|
+
exit 1
|
|
206
|
+
fi
|
|
207
|
+
fi
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### POST-CHECK 5: Services must inject ICurrentUser
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
SERVICE_FILES=$(find src/ -path "*/Services/*" -name "*Service.cs" ! -name "I*Service.cs" 2>/dev/null)
|
|
214
|
+
if [ -n "$SERVICE_FILES" ]; then
|
|
215
|
+
for f in $SERVICE_FILES; do
|
|
216
|
+
if ! grep -q "ICurrentUser" "$f"; then
|
|
217
|
+
echo "BLOCKING: Service missing ICurrentUser injection: $f"
|
|
218
|
+
echo "All services MUST inject ICurrentUser for tenant isolation"
|
|
219
|
+
exit 1
|
|
220
|
+
fi
|
|
221
|
+
done
|
|
222
|
+
fi
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### POST-CHECK 6: Translation files must exist for all 4 languages (if frontend)
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Find all i18n namespaces used in tsx files
|
|
229
|
+
TSX_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
230
|
+
if [ -n "$TSX_FILES" ]; then
|
|
231
|
+
NAMESPACES=$(grep -ohP "useTranslation\(\[?'([^']+)" $TSX_FILES | sed "s/.*'//" | sort -u)
|
|
232
|
+
for NS in $NAMESPACES; do
|
|
233
|
+
for LANG in fr en it de; do
|
|
234
|
+
if [ ! -f "src/i18n/locales/$LANG/$NS.json" ]; then
|
|
235
|
+
echo "BLOCKING: Missing translation file: src/i18n/locales/$LANG/$NS.json"
|
|
236
|
+
exit 1
|
|
237
|
+
fi
|
|
238
|
+
done
|
|
239
|
+
done
|
|
240
|
+
fi
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### POST-CHECK 7: Pages must use lazy loading (no static page imports in routes)
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
|
|
247
|
+
if [ -n "$ROUTE_FILES" ]; then
|
|
248
|
+
STATIC_PAGE_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" $ROUTE_FILES 2>/dev/null)
|
|
249
|
+
if [ -n "$STATIC_PAGE_IMPORTS" ]; then
|
|
250
|
+
echo "BLOCKING: Route files must use React.lazy() for page imports, not static imports"
|
|
251
|
+
echo "$STATIC_PAGE_IMPORTS"
|
|
252
|
+
echo "Fix: const Page = lazy(() => import('@/pages/...').then(m => ({ default: m.Page })));"
|
|
253
|
+
exit 1
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### POST-CHECK 8: Forms must be full pages with routes — ZERO modals/popups/drawers
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Check for modal/dialog/drawer imports in page files (create/edit forms)
|
|
262
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
263
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
264
|
+
MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet)" $PAGE_FILES 2>/dev/null)
|
|
265
|
+
if [ -n "$MODAL_IMPORTS" ]; then
|
|
266
|
+
echo "BLOCKING: Form pages must NOT use Modal/Dialog/Drawer/Popup components"
|
|
267
|
+
echo "Create/Edit forms MUST be full pages with their own URL routes"
|
|
268
|
+
echo "Found modal imports in page files:"
|
|
269
|
+
echo "$MODAL_IMPORTS"
|
|
270
|
+
echo "Fix: Create EntityCreatePage.tsx with route /create and EntityEditPage.tsx with route /:id/edit"
|
|
271
|
+
exit 1
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### POST-CHECK 9: Create/Edit pages must exist as separate route pages
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# For each module with a list page, verify create and edit pages exist
|
|
280
|
+
LIST_PAGES=$(find src/pages/ -name "*ListPage.tsx" -o -name "*sPage.tsx" 2>/dev/null | grep -v test)
|
|
281
|
+
if [ -n "$LIST_PAGES" ]; then
|
|
282
|
+
for LIST_PAGE in $LIST_PAGES; do
|
|
283
|
+
PAGE_DIR=$(dirname "$LIST_PAGE")
|
|
284
|
+
MODULE_NAME=$(basename "$PAGE_DIR")
|
|
285
|
+
# Check for create page
|
|
286
|
+
CREATE_PAGE=$(find "$PAGE_DIR" -name "*CreatePage.tsx" 2>/dev/null)
|
|
287
|
+
if [ -z "$CREATE_PAGE" ]; then
|
|
288
|
+
echo "WARNING: Module $MODULE_NAME has a list page but no CreatePage — expected EntityCreatePage.tsx"
|
|
289
|
+
fi
|
|
290
|
+
# Check for edit page
|
|
291
|
+
EDIT_PAGE=$(find "$PAGE_DIR" -name "*EditPage.tsx" 2>/dev/null)
|
|
292
|
+
if [ -z "$EDIT_PAGE" ]; then
|
|
293
|
+
echo "WARNING: Module $MODULE_NAME has a list page but no EditPage — expected EntityEditPage.tsx"
|
|
294
|
+
fi
|
|
295
|
+
done
|
|
296
|
+
fi
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### POST-CHECK 10: Form pages must have companion test files
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Every CreatePage and EditPage must have a .test.tsx file
|
|
303
|
+
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test)
|
|
304
|
+
if [ -n "$FORM_PAGES" ]; then
|
|
305
|
+
for FORM_PAGE in $FORM_PAGES; do
|
|
306
|
+
TEST_FILE="${FORM_PAGE%.tsx}.test.tsx"
|
|
307
|
+
if [ ! -f "$TEST_FILE" ]; then
|
|
308
|
+
echo "BLOCKING: Form page missing test file: $FORM_PAGE"
|
|
309
|
+
echo "Expected: $TEST_FILE"
|
|
310
|
+
echo "All form pages MUST have companion test files (rendering, validation, submit, navigation)"
|
|
311
|
+
exit 1
|
|
312
|
+
fi
|
|
313
|
+
done
|
|
314
|
+
fi
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### POST-CHECK 11: FK fields must NOT be plain text inputs (EntityLookup required)
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
# Check for FK fields rendered as plain text inputs in form pages
|
|
321
|
+
FORM_PAGES=$(find src/pages/ -name "*CreatePage.tsx" -o -name "*EditPage.tsx" 2>/dev/null | grep -v test | grep -v node_modules)
|
|
322
|
+
if [ -n "$FORM_PAGES" ]; then
|
|
323
|
+
# Detect pattern: input with value containing "Id" (e.g., employeeId, departmentId)
|
|
324
|
+
FK_TEXT_INPUTS=$(grep -Pn 'type=["\x27]text["\x27].*[a-z]+Id|value=\{[^}]*\.[a-z]+Id\}.*type=["\x27]text["\x27]' $FORM_PAGES 2>/dev/null)
|
|
325
|
+
if [ -n "$FK_TEXT_INPUTS" ]; then
|
|
326
|
+
echo "BLOCKING: FK fields rendered as plain text inputs — MUST use EntityLookup component"
|
|
327
|
+
echo "Users cannot type GUIDs manually. Use <EntityLookup /> from @/components/ui/EntityLookup"
|
|
328
|
+
echo "See smartstack-frontend.md section 6 for the EntityLookup pattern"
|
|
329
|
+
echo "$FK_TEXT_INPUTS"
|
|
330
|
+
exit 1
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# Check for input placeholders mentioning "ID" or "id" or "Enter...Id"
|
|
334
|
+
FK_PLACEHOLDER=$(grep -Pn 'placeholder=["\x27].*[Ee]nter.*[Ii][Dd]|placeholder=["\x27].*[Gg][Uu][Ii][Dd]' $FORM_PAGES 2>/dev/null)
|
|
335
|
+
if [ -n "$FK_PLACEHOLDER" ]; then
|
|
336
|
+
echo "BLOCKING: Form has placeholder asking user to enter an ID/GUID — use EntityLookup instead"
|
|
337
|
+
echo "$FK_PLACEHOLDER"
|
|
338
|
+
exit 1
|
|
339
|
+
fi
|
|
340
|
+
fi
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### POST-CHECK 12: Backend APIs must support search parameter for EntityLookup
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
# Check that controller GetAll methods accept search parameter
|
|
347
|
+
CTRL_FILES=$(find src/ -path "*/Controllers/*" -name "*Controller.cs" 2>/dev/null)
|
|
348
|
+
if [ -n "$CTRL_FILES" ]; then
|
|
349
|
+
for f in $CTRL_FILES; do
|
|
350
|
+
# Check if controller has GetAll but no search parameter
|
|
351
|
+
if grep -q "\[HttpGet\]" "$f" && grep -q "GetAll" "$f"; then
|
|
352
|
+
if ! grep -q "search" "$f"; then
|
|
353
|
+
echo "WARNING: Controller missing search parameter on GetAll: $f"
|
|
354
|
+
echo "GetAll endpoints MUST accept ?search= to enable EntityLookup on frontend"
|
|
355
|
+
echo "Fix: Add [FromQuery] string? search parameter to GetAll action"
|
|
356
|
+
fi
|
|
357
|
+
fi
|
|
358
|
+
done
|
|
359
|
+
fi
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### POST-CHECK 13: No hardcoded Tailwind colors in generated pages
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
NEW_PAGES=$(git diff --name-only HEAD 2>/dev/null | grep "src/pages/.*\.tsx$")
|
|
366
|
+
if [ -n "$NEW_PAGES" ]; then
|
|
367
|
+
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $NEW_PAGES 2>/dev/null)
|
|
368
|
+
if [ -n "$HARDCODED" ]; then
|
|
369
|
+
echo "WARNING: Pages should use CSS variables instead of hardcoded Tailwind colors"
|
|
370
|
+
echo "Fix: bg-[var(--bg-card)] instead of bg-white, text-[var(--text-primary)] instead of text-gray-900"
|
|
371
|
+
echo "$HARDCODED"
|
|
372
|
+
fi
|
|
373
|
+
fi
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**If ANY POST-CHECK fails → fix in step-03, re-validate.**
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
137
380
|
## 7. Acceptance Criteria POST-CHECK
|
|
138
381
|
|
|
139
382
|
For each AC inferred in step-01:
|
|
@@ -163,6 +406,14 @@ AC2: {criterion} → PASS / FAIL (evidence: {file:line or test})
|
|
|
163
406
|
| dotnet build | PASS |
|
|
164
407
|
| npm typecheck | PASS / N/A |
|
|
165
408
|
| Seed data | PASS / N/A |
|
|
409
|
+
| I18n: 4 languages per namespace | PASS / N/A |
|
|
410
|
+
| Lazy loading: no static page imports | PASS / N/A |
|
|
411
|
+
| Forms: full pages, zero modals | PASS / N/A |
|
|
412
|
+
| Forms: create/edit pages exist | PASS / N/A |
|
|
413
|
+
| Forms: test files exist | PASS / N/A |
|
|
414
|
+
| FK fields: EntityLookup, no plain text | PASS / N/A |
|
|
415
|
+
| APIs: search parameter on GetAll | PASS / N/A |
|
|
416
|
+
| CSS variables: no hardcoded colors | PASS / N/A |
|
|
166
417
|
| Acceptance criteria | {X}/{Y} PASS |
|
|
167
418
|
```
|
|
168
419
|
|
|
@@ -60,6 +60,13 @@ For each changed file, check:
|
|
|
60
60
|
- [ ] SmartTable/SmartForm (not raw HTML tables/forms)
|
|
61
61
|
- [ ] Correct Layout wrapper per context
|
|
62
62
|
|
|
63
|
+
**FK Fields & Forms:**
|
|
64
|
+
- [ ] FK Guid fields use `EntityLookup` component (NEVER plain text `<input>`)
|
|
65
|
+
- [ ] No placeholder text asking user to "Enter ID" or "Enter GUID"
|
|
66
|
+
- [ ] Create/Edit forms are full pages with own routes (ZERO modals/dialogs)
|
|
67
|
+
- [ ] Backend GetAll endpoints support `?search=` param for EntityLookup
|
|
68
|
+
- [ ] Each EntityLookup has `apiEndpoint`, `mapOption`, `label` props
|
|
69
|
+
|
|
63
70
|
---
|
|
64
71
|
|
|
65
72
|
## 4. Produce Findings
|
|
@@ -12,14 +12,15 @@ next_step: steps/step-08-run-tests.md
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
## 1. Ensure Test
|
|
15
|
+
## 1. Ensure Test Projects Exist
|
|
16
|
+
|
|
17
|
+
### 1a. Unit Test Project
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
|
-
# Check for existing test project
|
|
19
|
-
|
|
20
|
+
# Check for existing unit test project
|
|
21
|
+
UNIT_PROJECT=$(find tests/ -name "*.Tests.Unit.csproj" 2>/dev/null | head -1)
|
|
20
22
|
|
|
21
|
-
if [ -z "$
|
|
22
|
-
# Create test project
|
|
23
|
+
if [ -z "$UNIT_PROJECT" ]; then
|
|
23
24
|
PROJECT_NAME=$(basename *.sln .sln)
|
|
24
25
|
dotnet new xunit -n "${PROJECT_NAME}.Tests.Unit" -o "tests/${PROJECT_NAME}.Tests.Unit"
|
|
25
26
|
dotnet add "tests/${PROJECT_NAME}.Tests.Unit" package Moq
|
|
@@ -31,6 +32,29 @@ if [ -z "$TEST_PROJECT" ]; then
|
|
|
31
32
|
fi
|
|
32
33
|
```
|
|
33
34
|
|
|
35
|
+
### 1b. Integration Test Project (for step-04 DB validation)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Check for existing integration test project
|
|
39
|
+
INT_PROJECT=$(find tests/ -name "*.Tests.Integration.csproj" 2>/dev/null | head -1)
|
|
40
|
+
|
|
41
|
+
if [ -z "$INT_PROJECT" ]; then
|
|
42
|
+
PROJECT_NAME=$(basename *.sln .sln)
|
|
43
|
+
dotnet new xunit -n "${PROJECT_NAME}.Tests.Integration" -o "tests/${PROJECT_NAME}.Tests.Integration"
|
|
44
|
+
dotnet add "tests/${PROJECT_NAME}.Tests.Integration" package Moq
|
|
45
|
+
dotnet add "tests/${PROJECT_NAME}.Tests.Integration" package FluentAssertions
|
|
46
|
+
dotnet add "tests/${PROJECT_NAME}.Tests.Integration" package Respawn
|
|
47
|
+
dotnet add "tests/${PROJECT_NAME}.Tests.Integration" package Microsoft.EntityFrameworkCore.SqlServer
|
|
48
|
+
for proj in src/*/*.csproj; do
|
|
49
|
+
dotnet add "tests/${PROJECT_NAME}.Tests.Integration" reference "$proj"
|
|
50
|
+
done
|
|
51
|
+
dotnet sln add "tests/${PROJECT_NAME}.Tests.Integration/${PROJECT_NAME}.Tests.Integration.csproj"
|
|
52
|
+
fi
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> **Note:** Integration tests use `DatabaseFixture` + `Respawn` with SQL Server LocalDB.
|
|
56
|
+
> See step-04 section 5c for details.
|
|
57
|
+
|
|
34
58
|
---
|
|
35
59
|
|
|
36
60
|
## 2. Scaffold Tests via MCP
|
|
@@ -76,6 +100,24 @@ Call: mcp__smartstack__scaffold_tests
|
|
|
76
100
|
Tests: RequirePermission enforcement, multi-tenant isolation
|
|
77
101
|
```
|
|
78
102
|
|
|
103
|
+
### Frontend Form Tests
|
|
104
|
+
```
|
|
105
|
+
For each module with create/edit pages:
|
|
106
|
+
→ Generate EntityCreatePage.test.tsx and EntityEditPage.test.tsx
|
|
107
|
+
→ Co-located next to the page component (NOT in __tests__/ folder)
|
|
108
|
+
|
|
109
|
+
Required test coverage:
|
|
110
|
+
- Rendering: form renders with all expected fields
|
|
111
|
+
- Validation: required fields show errors on empty submit
|
|
112
|
+
- Submission: successful submit calls API and navigates back
|
|
113
|
+
- Pre-fill (edit only): entity data loaded into fields
|
|
114
|
+
- Navigation: back/cancel button calls navigate(-1)
|
|
115
|
+
- Error handling: API error displays error message
|
|
116
|
+
|
|
117
|
+
Tools: Vitest + React Testing Library + @testing-library/user-event
|
|
118
|
+
Mock API: vi.mock() or MSW — NEVER real API calls
|
|
119
|
+
```
|
|
120
|
+
|
|
79
121
|
---
|
|
80
122
|
|
|
81
123
|
## 3. Suggest Additional Scenarios
|
|
@@ -112,6 +154,7 @@ If under 80%: identify uncovered paths, scaffold additional tests
|
|
|
112
154
|
| Application | {count} | {%} |
|
|
113
155
|
| API | {count} | {%} |
|
|
114
156
|
| Security | {count} | {%} |
|
|
157
|
+
| Frontend Forms | {count} | {%} |
|
|
115
158
|
|
|
116
159
|
**Total tests:** {count}
|
|
117
160
|
**Estimated coverage:** {%}
|
|
@@ -98,13 +98,13 @@ Level 1: Context (business)
|
|
|
98
98
|
### Standard Sections per Module
|
|
99
99
|
|
|
100
100
|
> **RULE: Sections are functional zones, NOT CRUD operations.**
|
|
101
|
-
> `create` and `edit` are
|
|
101
|
+
> `create` and `edit` are SEPARATE PAGES with their own URL routes (`/create` and `/:id/edit`).
|
|
102
102
|
> `detail` is a page with tabs accessible by clicking a row in `list` (not a standalone nav section).
|
|
103
103
|
|
|
104
104
|
| Section | Purpose | Typical Components |
|
|
105
105
|
|---------|---------|-------------------|
|
|
106
|
-
| `list` | **Main page**: entity grid + create
|
|
107
|
-
| `detail` | **Reached from list** (hidden nav, route `:id`): tabbed view of entity details + edit
|
|
106
|
+
| `list` | **Main page**: entity grid + create button (navigates to `/create` page) + click to detail | DataGrid, FilterBar, Pagination, CreateButton |
|
|
107
|
+
| `detail` | **Reached from list** (hidden nav, route `:id`): tabbed view of entity details + edit button (navigates to `/:id/edit` page) | BackButton, DetailHeader, StatusBadge, TabPanel, DetailCard, SmartTable (relations), Timeline (history) |
|
|
108
108
|
| `dashboard` | Module overview/KPIs (if analytics needed) | Charts, StatCards, RecentActivity |
|
|
109
109
|
| `approve` | Approval workflow queue (if validation workflow) | ApprovalPanel, StatusTimeline |
|
|
110
110
|
| `import` | Bulk data import (if volume > 100) | FileUpload, MappingTable, Preview |
|
|
@@ -115,9 +115,9 @@ Level 1: Context (business)
|
|
|
115
115
|
|
|
116
116
|
| NOT a sidebar section | What it really is |
|
|
117
117
|
|---------------|-------------------|
|
|
118
|
-
| `create` | Action button in `list` page →
|
|
119
|
-
| `edit` | Action in detail page →
|
|
120
|
-
| `delete` | Action with confirmation
|
|
118
|
+
| `create` | Action button in `list` page → navigates to `/create` page with form |
|
|
119
|
+
| `edit` | Action in list/detail page → navigates to `/:id/edit` page with pre-filled form |
|
|
120
|
+
| `delete` | Action with inline confirmation in `list` or detail |
|
|
121
121
|
| `search` | FilterBar component integrated in the `list` section |
|
|
122
122
|
|
|
123
123
|
> **Note on `detail`:** The detail page IS a `specification.sections[]` entry (with `navigation: "hidden"`) so it gets a wireframe, resources, and route. But it is NOT shown in the sidebar — it is reached by clicking a row in `list`. Every module with a `list` section MUST have a companion `detail` section.
|
|
@@ -489,7 +489,7 @@ Auto-added:
|
|
|
489
489
|
| Produits/Products | Commerce, Inventaire | Catalogue avec variantes, tarification, stock |
|
|
490
490
|
|
|
491
491
|
**Section pattern for module dedie:**
|
|
492
|
-
- Section `list` (TOUJOURS) : grille + create
|
|
492
|
+
- Section `list` (TOUJOURS) : grille + bouton create (navigue vers page `/create`) + clic vers detail
|
|
493
493
|
- Page detail avec onglets : Infos + onglets relationnels
|
|
494
494
|
- Sections additionnelles UNIQUEMENT si : dashboard, approve, import, rapport
|
|
495
495
|
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
| Need | Component | Détail |
|
|
40
40
|
|------|-----------|--------|
|
|
41
41
|
| List with pagination | DataTable | Tri, pagination, sélection, actions inline |
|
|
42
|
-
| Creation form | Form
|
|
43
|
-
| Edit form | Form
|
|
42
|
+
| Creation form | Form page (`/create` route) | Validation FluentValidation, i18n des erreurs |
|
|
43
|
+
| Edit form | Form page (`/:id/edit` route) | Pré-remplissage, dirty check, optimistic locking |
|
|
44
44
|
| Filters | FilterBar | Filtres combinables, persistance URL params |
|
|
45
45
|
| Export | ExportButton | CSV/Excel, respecte les filtres actifs |
|
|
46
46
|
| Cards/Grid | CardGrid | Affichage visuel alternatif au tableau |
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
| Question | If answer is vague/insufficient | Probe |
|
|
59
59
|
|----------|-------------------------------|-------|
|
|
60
60
|
| Q7.1 (devices) | "Desktop uniquement" | "Aucun accès mobile ? Même pas en consultation ? Tablette en atelier/terrain ?" |
|
|
61
|
-
| Q7.5 (screens) | "Un écran de liste" | "Avec
|
|
61
|
+
| Q7.5 (screens) | "Un écran de liste" | "Avec pages dédiées pour création et édition ? Détail en page avec onglets ?" |
|
|
62
62
|
| Q7.5 (screens) | Pas de mention dashboard | "L'utilisateur a-t-il besoin d'un tableau de bord avec des KPIs avant de plonger dans les données ?" |
|
|
63
63
|
| Q7.7 (key info) | "Toutes les colonnes" | "Sur mobile/petit écran, quelles 3-4 colonnes sont indispensables ? Le reste = détail secondaire" |
|
|
64
64
|
| Q7.8 (actions) | "CRUD classique" | "Actions métier spécifiques ? (valider, dupliquer, archiver, changer statut, assigner)" |
|
|
@@ -84,7 +84,7 @@ For each detected module, anticipate sections (Level 4) and resources (Level 5)
|
|
|
84
84
|
| integration | list | sync-status-grid, config-form, log-viewer | Infos, Config, Logs |
|
|
85
85
|
|
|
86
86
|
> **RULE:** Sections = functional zones only (`list`, `dashboard`, `approve`, `import`, `rapport`, `planning`).
|
|
87
|
-
> `create`/`edit` =
|
|
87
|
+
> `create`/`edit` = separate pages with own URL routes (`/create` and `/:id/edit`). `detail` = tabbed page reached by clicking a row in `list`.
|
|
88
88
|
|
|
89
89
|
**Always check:** Does the user's description imply additional functional sections? (e.g., "reports" → `dashboard`, "calendar view" → `planning`, "approval workflow" → `approve`)
|
|
90
90
|
|
|
@@ -121,7 +121,7 @@ L'entite {E} est-elle CORE pour cette application ?
|
|
|
121
121
|
Module
|
|
122
122
|
├── Section: list
|
|
123
123
|
│ └── Page principale avec grille/table
|
|
124
|
-
│ └── Action "Creer" (
|
|
124
|
+
│ └── Action "Creer" (navigue vers la page /create avec formulaire)
|
|
125
125
|
│ └── Clic sur une ligne → navigation vers la page detail
|
|
126
126
|
│ └── Resources : {entity}-grid, {entity}-filters, {entity}-form
|
|
127
127
|
│
|
|
@@ -152,8 +152,8 @@ Module
|
|
|
152
152
|
|
|
153
153
|
| PAS une section | C'est quoi en realite |
|
|
154
154
|
|-----------------|----------------------|
|
|
155
|
-
| `create` | Action (bouton) dans la page `list`,
|
|
156
|
-
| `edit` | Action dans la page detail,
|
|
155
|
+
| `create` | Action (bouton) dans la page `list`, navigue vers la page `/create` avec formulaire |
|
|
156
|
+
| `edit` | Action dans la page list/detail, navigue vers la page `/:id/edit` avec formulaire pre-rempli |
|
|
157
157
|
| `detail` | Page de detail accessible par navigation depuis `list` (pas une section menu) |
|
|
158
158
|
| `delete` | Action avec confirmation dans la page `list` ou `detail` |
|
|
159
159
|
| `search` | Composant de filtrage integre dans la section `list` |
|
|
@@ -70,14 +70,22 @@ From `specification.uiWireframes[]`, `specification.dashboards[]` and `analysis.
|
|
|
70
70
|
|
|
71
71
|
```json
|
|
72
72
|
"frontend": [
|
|
73
|
-
{ "path": "src/pages/{ModuleName}/{PageName}Page.tsx", "type": "Page", "linkedUCs": [], "linkedWireframes": ["{module}-list"], "module": "{moduleCode}", "wireframeAcceptanceCriteria": "Layout MUST match wireframe..." },
|
|
74
|
-
{ "path": "src/pages/{ModuleName}/{PageName}DetailPage.tsx", "type": "Page", "linkedUCs": [], "linkedWireframes": ["{module}-detail"], "module": "{moduleCode}" },
|
|
75
|
-
{ "path": "src/pages/{ModuleName}/{DashboardName}DashboardPage.tsx", "type": "DashboardPage", "linkedUCs": [], "linkedWireframes": ["{module}-dashboard"], "module": "{moduleCode}", "dashboardRef": "{module}-dashboard", "instructions": "Use Recharts library..." },
|
|
76
|
-
{ "path": "src/components/{ModuleName}/{ComponentName}.tsx", "type": "Component", "linkedUCs": [], "linkedWireframes": [], "module": "{moduleCode}" },
|
|
77
|
-
{ "path": "src/hooks/use{ModuleName}{Hook}.ts", "type": "Hook", "linkedUCs": [], "module": "{moduleCode}" }
|
|
73
|
+
{ "path": "src/pages/{ContextPascal}/{ApplicationName}/{ModuleName}/{PageName}Page.tsx", "type": "Page", "linkedUCs": [], "linkedWireframes": ["{module}-list"], "module": "{moduleCode}", "wireframeAcceptanceCriteria": "Layout MUST match wireframe...", "skill": "/ui-components" },
|
|
74
|
+
{ "path": "src/pages/{ContextPascal}/{ApplicationName}/{ModuleName}/{PageName}DetailPage.tsx", "type": "Page", "linkedUCs": [], "linkedWireframes": ["{module}-detail"], "module": "{moduleCode}", "skill": "/ui-components" },
|
|
75
|
+
{ "path": "src/pages/{ContextPascal}/{ApplicationName}/{ModuleName}/{DashboardName}DashboardPage.tsx", "type": "DashboardPage", "linkedUCs": [], "linkedWireframes": ["{module}-dashboard"], "module": "{moduleCode}", "dashboardRef": "{module}-dashboard", "instructions": "Use Recharts library...", "skill": "/ui-components" },
|
|
76
|
+
{ "path": "src/components/{ModuleName}/{ComponentName}.tsx", "type": "Component", "linkedUCs": [], "linkedWireframes": [], "module": "{moduleCode}", "skill": "/ui-components" },
|
|
77
|
+
{ "path": "src/hooks/use{ModuleName}{Hook}.ts", "type": "Hook", "linkedUCs": [], "module": "{moduleCode}" },
|
|
78
|
+
{ "path": "src/i18n/locales/fr/{moduleLower}.json", "type": "I18n", "language": "fr", "module": "{moduleCode}" },
|
|
79
|
+
{ "path": "src/i18n/locales/en/{moduleLower}.json", "type": "I18n", "language": "en", "module": "{moduleCode}" },
|
|
80
|
+
{ "path": "src/i18n/locales/it/{moduleLower}.json", "type": "I18n", "language": "it", "module": "{moduleCode}" },
|
|
81
|
+
{ "path": "src/i18n/locales/de/{moduleLower}.json", "type": "I18n", "language": "de", "module": "{moduleCode}" }
|
|
78
82
|
]
|
|
79
83
|
```
|
|
80
84
|
|
|
85
|
+
**Page generation:** ALL pages and components MUST be generated via `/ui-components` skill (entity lists, grids, tables, dashboards, charts). The `"skill"` field indicates which skill to invoke.
|
|
86
|
+
|
|
87
|
+
**I18n generation:** 4 JSON files per module (fr, en, it, de) with identical key structures. `{moduleLower}` = lowercase module code. See `smartstack-frontend.md` for JSON template (keys: actions, labels, columns, form, errors, validation, messages, empty).
|
|
88
|
+
|
|
81
89
|
**Dashboard acceptance criteria:** Chart library (Recharts), chart types matching spec, KPI cards, filters, CSS variables, responsive layout, wireframe-matching positions.
|
|
82
90
|
|
|
83
91
|
## 4.6 SeedData Files
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| string + unique | text | yes | yes | clickAction: navigate:detail |
|
|
10
10
|
| string | text | yes | yes | — |
|
|
11
11
|
| enum / status | badge | yes | yes (multi-select) | colorMap from lifeCycles if exists |
|
|
12
|
-
| FK:Entity | text (join .name) | yes | yes (
|
|
12
|
+
| FK:Entity | text (join .name) | yes | yes (EntityLookup) | Display related entity name, filter via searchable EntityLookup |
|
|
13
13
|
| decimal | currency | yes | no | — |
|
|
14
14
|
| int | number | yes | no | — |
|
|
15
15
|
| datetime | date-relative | yes | yes (date-range) | — |
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| string | Input | entity.required | — |
|
|
24
24
|
| string (multiline) | TextArea | entity.required | rows: 4 |
|
|
25
25
|
| enum | Select | entity.required | source: enum name |
|
|
26
|
-
| FK:Entity |
|
|
26
|
+
| FK:Entity | EntityLookup | entity.required | source: target entity, searchable. Component: `@/components/ui/EntityLookup`. NEVER plain text input for FK Guid fields. Backend API MUST support `?search=` param. See `smartstack-frontend.md` section 6. |
|
|
27
27
|
| decimal | NumberInput | entity.required | — |
|
|
28
28
|
| int | NumberInput | entity.required | — |
|
|
29
29
|
| datetime | DatePicker | entity.required | — |
|
|
@@ -31,22 +31,22 @@
|
|
|
31
31
|
|
|
32
32
|
## Auto-Generated UI Components by featureType
|
|
33
33
|
|
|
34
|
-
> **RULE:** Sections = functional zones only. `create`/`edit` are
|
|
34
|
+
> **RULE:** Sections = functional zones only. `create`/`edit` are separate pages with own URL routes (`/create` and `/:id/edit`). `detail` is a tabbed page reached from `list`.
|
|
35
35
|
|
|
36
|
-
| featureType | Sections (functional zones) | List page includes | Detail page tabs |
|
|
37
|
-
|
|
38
|
-
| data-centric | list | grid, filters, create
|
|
39
|
-
| workflow | list, approve | grid, filters, create
|
|
40
|
-
| integration | list | grid, filters, config
|
|
41
|
-
| reporting | dashboard | — | — |
|
|
42
|
-
| full-module | list, dashboard | grid, filters, create
|
|
36
|
+
| featureType | Sections (functional zones) | List page includes | Form pages | Detail page tabs |
|
|
37
|
+
|---|---|---|---|---|
|
|
38
|
+
| data-centric | list | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations} |
|
|
39
|
+
| workflow | list, approve | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
|
|
40
|
+
| integration | list | grid, filters, config button | `/create` page, `/:id/edit` page | Infos, Config, Logs |
|
|
41
|
+
| reporting | dashboard | — | — | — |
|
|
42
|
+
| full-module | list, dashboard | grid, filters, create button | `/create` page, `/:id/edit` page | Infos, {relations}, Historique |
|
|
43
43
|
|
|
44
44
|
## Component Generation Rules
|
|
45
45
|
|
|
46
|
-
1. **list section:** SmartTable with all non-long-text attributes as columns, actions = [view, edit, delete], defaultSort = { createdAt, desc }. Create action
|
|
47
|
-
2. **create
|
|
48
|
-
3. **
|
|
49
|
-
4. **
|
|
46
|
+
1. **list section:** SmartTable with all non-long-text attributes as columns, actions = [view, edit, delete], defaultSort = { createdAt, desc }. Create button navigates to `/create` page. Edit action navigates to `/:id/edit` page.
|
|
47
|
+
2. **create page (`/create` route):** SmartForm (full page) with all writable attributes as fields, component inferred from type. Back button + form + submit. NEVER a modal.
|
|
48
|
+
3. **edit page (`/:id/edit` route):** Same form as create but pre-filled with entity data. Full page with back button. NEVER a modal or inline toggle.
|
|
49
|
+
4. **detail page (from list click):** TabPanel with: Info tab (all attributes read-only + edit button navigating to `/:id/edit`), relation tabs (child SmartTable for each 1:N relationship), History tab (if auditable). See "Detail Page Tab Auto-Inference" below for exact rules.
|
|
50
50
|
5. **dashboard section:** Trigger 3d (dashboard specification)
|
|
51
51
|
|
|
52
52
|
## Detail Page Tab Auto-Inference
|
|
@@ -136,7 +136,7 @@ pre_analysis:
|
|
|
136
136
|
|
|
137
137
|
**Modules identifiés :**
|
|
138
138
|
- **{Module 1}** : {functional description}
|
|
139
|
-
- Section **list** : {entity}-grid, {entity}-filters
|
|
139
|
+
- Section **list** : {entity}-grid, {entity}-filters + pages `/create` et `/:id/edit`
|
|
140
140
|
- Page détail (onglets) : {Infos, {relation_tabs}...}
|
|
141
141
|
- {IF dashboard needed:} Section **dashboard** : {kpi-cards, charts...}
|
|
142
142
|
- **{Module 2}** : {functional description}
|
|
@@ -601,7 +601,7 @@ BEFORE transitioning to step-02:
|
|
|
601
601
|
f. **List detail page tabs** — for entities accessible via click from `list`
|
|
602
602
|
|
|
603
603
|
> **SECTION RULE:** Sections are functional zones (list, dashboard, approve, import, rapport...).
|
|
604
|
-
> `create` and `edit` are
|
|
604
|
+
> `create` and `edit` are SEPARATE PAGES with their own URL routes (`/create` and `/:id/edit`).
|
|
605
605
|
> `detail` is a page with tabs accessible by clicking a row in `list`, NOT a standalone section.
|
|
606
606
|
|
|
607
607
|
4. **OUTPUT the matrix as text** (do NOT put it inside AskUserQuestion — it won't render):
|
|
@@ -73,7 +73,7 @@ For each mustHave/shouldHave scope item:
|
|
|
73
73
|
> "entities": ["Anticipated entity names"],
|
|
74
74
|
> "estimatedComplexity": "simple|medium|complex",
|
|
75
75
|
> "anticipatedSections": [
|
|
76
|
-
> { "code": "list", "description": "Main page: grid, create
|
|
76
|
+
> { "code": "list", "description": "Main page: grid, create button (navigates to /create page), click to detail", "resources": ["entity-grid", "entity-filters", "entity-create-page", "entity-edit-page"] },
|
|
77
77
|
> { "code": "dashboard", "description": "Module KPIs and overview (if needed)", "resources": ["entity-kpi-cards", "entity-charts"] }
|
|
78
78
|
> ],
|
|
79
79
|
> "detailTabs": ["Informations", "RelatedEntity1", "Historique"]
|
|
@@ -200,7 +200,7 @@ Total: {N} requirements mapped to this module
|
|
|
200
200
|
For each module, propose standard sections based on module type:
|
|
201
201
|
|
|
202
202
|
> **RULE:** Sections = functional zones only. `list` is ALWAYS present (default).
|
|
203
|
-
> `create`/`edit` =
|
|
203
|
+
> `create`/`edit` = separate pages with own URL routes (`/create` and `/:id/edit`). `detail` = tabbed page from list click.
|
|
204
204
|
|
|
205
205
|
| Module Type | Sections (functional zones) | Detail page tabs (from list click) |
|
|
206
206
|
|-------------|---------------------------|-----------------------------------|
|
|
@@ -291,7 +291,7 @@ See [references/spec-auto-inference.md](../references/spec-auto-inference.md) fo
|
|
|
291
291
|
- Entity attribute → SmartTable column mapping (9 type rules)
|
|
292
292
|
- Entity attribute → SmartForm field mapping (8 type rules)
|
|
293
293
|
- Auto-generated sections per featureType (5 types)
|
|
294
|
-
- Component generation rules (list section, create form
|
|
294
|
+
- Component generation rules (list section, create/edit form pages with own routes, detail page with tabs, dashboard section)
|
|
295
295
|
- Status/lifecycle enhancement rules
|
|
296
296
|
|
|
297
297
|
Write auto-generated sections to `specification.sections[]` via ba-writer.enrichSection()
|
|
@@ -319,7 +319,8 @@ For each section, identify what resources/components are needed:
|
|
|
319
319
|
|
|
320
320
|
- List section → DataGrid, FilterBar, SearchInput, ExportButton, CreateButton
|
|
321
321
|
- Detail page (from list click) → BackButton, DetailHeader, StatusBadge, TabPanel, DetailCard, SmartForm (edit mode), SmartTable (relation tabs), Timeline (history tab)
|
|
322
|
-
- Create (
|
|
322
|
+
- Create page (`/create` route) → SmartForm (full page), ValidationMessages, SubmitButton, BackButton
|
|
323
|
+
- Edit page (`/:id/edit` route) → SmartForm (full page, pre-filled), ValidationMessages, SubmitButton, BackButton
|
|
323
324
|
- Approve section → StatusTransitionPanel, CommentBox, ApproveRejectButtons
|
|
324
325
|
- Dashboard section → StatCard, RechartsChart (Bar/Line/Pie/Area), DashboardGrid, FilterBar
|
|
325
326
|
|