feat: add Cases by Type widget to dashboard#169
Conversation
- Add aggregateByType() helper to dashboardHelpers.js — aggregates open cases by case type name, sorted by count descending - Add typeData state, typeBarWidth method, and cases-by-type widget definition and layout entry to Dashboard.vue - Clicking a bar navigates to Cases view filtered by case type - Fix duplicate DEFAULT_LAYOUT entries (merge artefact) - Fix missing </template> closing tag for #widget-case-map slot
Update spec status to pr-created after draft PR #169 was opened.
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ❌ | ❌ | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ |
Quality workflow — 2026-04-16 10:37 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ❌ | ❌ | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-16 10:38 UTC
Download the full PDF report from the workflow artifacts.
… vulns (#135) Ran npm install --legacy-peer-deps to sync package-lock.json with the updated package.json (apexcharts/vue-apexcharts were missing from lockfile), then ran npm audit fix --legacy-peer-deps to patch: - [CRITICAL] axios 1.0.0-1.14.0 → 1.15.0+ (GHSA-fvcv-3m26-pcqx, GHSA-3p68-rc4w-qgx5) - [HIGH] fast-xml-parser (GHSA-8gc5-j5rx-235r, GHSA-jp2q-39xq-3w4g) - [HIGH] flatted ≤3.4.1 (GHSA-25h7-pfq9-p65f, GHSA-rf6f-7fwh-wjgh) - [HIGH] picomatch (multiple advisories) - [MODERATE] brace-expansion, dompurify, follow-redirects Remaining low/moderate issues are all Vue 2 ecosystem (vue, @nextcloud/vue v8) and require a breaking Vue 3 migration; they are out-of-scope for this fix. npm audit --audit-level=critical now exits 0.
|
Hydra Builder — Quality Fix (npm-audit) The Root cause: Fix applied (
Remaining (not fixed — all require breaking changes): The remaining 10 low + 4 moderate vulnerabilities are all in the Vue 2 ecosystem ( Verification: The quality gate ( |
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ❌ | ❌ | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-16 10:52 UTC
Download the full PDF report from the workflow artifacts.
Code Review — Juan Claude van DammeResult: PASS (0 critical, 0 warning, 2 suggestions) Clean PR. The new Cases by Type widget follows the established bar-chart pattern from Cases by Status, the helper function is well-structured, and the duplicate layout / missing SUGGESTIONVariable shadowing: SUGGESTIONSame-named case types would merge with only one |
| v-for="(item, index) in typeData" | ||
| :key="item.name" | ||
| class="status-bar-row status-bar-row--clickable" | ||
| @click="$router.push({ name: 'Cases', query: { caseType: item.typeId } })"> |
There was a problem hiding this comment.
[unfixed: suggestion only] Rule: logic/navigation — When item.typeId is null (cases with no matching case type, bucketed as 'Unknown'), $router.push({ query: { caseType: null } }) serialises to ?caseType=null in vue-router v3, which will match nothing in the Cases view. Consider guarding: item.typeId ? $router.push(...) : null or disabling the .--clickable class when typeId is null.
| for (const c of openCases) { | ||
| const name = typeMap.get(c.caseType) || 'Unknown' | ||
| countMap.set(name, (countMap.get(name) || 0) + 1) | ||
| if (!idMap.has(name)) { |
There was a problem hiding this comment.
[unfixed: suggestion only] Rule: correctness/edge-case — idMap stores the first caseType ID encountered for each display name. If two distinct case types share the same title/name, they are merged into one chart bar and clicking navigates to only the first type's ID. In practice display names should be unique, but a comment noting this tie-breaking assumption would aid future maintainers.
| * @return {string} CSS width value | ||
| */ | ||
| typeBarWidth(count) { | ||
| const max = Math.max(1, ...this.typeData.map(t => t.count)) |
There was a problem hiding this comment.
[unfixed: suggestion only] Rule: readability — The arrow function parameter t in this.typeData.map(t => t.count) shadows the Nextcloud global t() i18n function within this method's scope. While it causes no runtime or lint error (the Nextcloud ESLint config does not declare t as a module-scope global), renaming to td or typeItem eliminates the visual ambiguity for readers. Compare with the existing barWidth method which uses s for the same pattern.
Code Review — Juan Claude van DammeResult: PASS (0 fixed, 3 unfixed, 0 blocking)
Findings
Notes
See inline comments for per-finding detail. |
Security Review — Clyde BarcodeResult: PASS (0 fixed, 0 unfixed, 0 blocking) Scan coverage
New code assessment (
|
| Check | Result |
|---|---|
XSS — template interpolations use {{ }} (Vue-escaped), no v-html |
✓ Safe |
CSS injection — typeBarWidth() returns computed float, barColor() uses hardcoded array |
✓ Safe |
| Open redirect — router navigation uses server-provided type ID through Vue Router encoding | ✓ Safe |
| Data exposure — widget renders case type names and counts, no PII fields | ✓ Safe |
Auth gate — widget reads from already-filtered openCases (server-side filtered) |
✓ Safe |
Dependency scan (package-lock.json)
npm audit reports 4 moderate findings (postcss <8.4.31, vue ReDoS GHSA-5j4c-8p2g-v4jx, vue-template-compiler GHSA-g3ch-rx76-35fx, vue-apexcharts→vue transitive). All are pre-existing in the Vue 2 ecosystem; none were introduced by this PR's logic changes. Breaking-change upgrades (Vue 2→3) are out of scope for this widget PR. The quality phase already addressed critical/high CVEs in commit 097949e.
Informational (pre-existing, not in diff — no action required)
window._oc_isadmininDashboard.vue:349— cosmetic use only (controls welcome message text, not any privilege gate); no backend privilege escalation possible. ADR-005 backend check requirement is met at the API/controller layer.err.messagerendered via{{ error }}inDashboard.vue:219— Vue text interpolation escapes HTML; no XSS risk. Minor info disclosure to authenticated users only.
Verdict
- Total findings: 0
- Fixed: 0
- Unfixed: 0
- Verdict: pass
No blocking issues. Cleared for Applier review.
| * Aggregate open cases by case type name for the type chart. | ||
| * Sorted by count descending. | ||
| * | ||
| * @see openspec/changes/dashboard/tasks.md#task-1 |
There was a problem hiding this comment.
| /** | ||
| * Compute bar width percentage for the Cases by Type chart. | ||
| * | ||
| * @see openspec/changes/dashboard/tasks.md#task-1 |
| { id: 8, widgetId: 'deadline-alerts', gridX: 0, gridY: 6, gridWidth: 4, gridHeight: 4 }, | ||
| { id: 9, widgetId: 'task-due-reminders', gridX: 4, gridY: 6, gridWidth: 4, gridHeight: 4 }, | ||
| { id: 10, widgetId: 'stalled-cases', gridX: 8, gridY: 6, gridWidth: 4, gridHeight: 4 }, | ||
| { id: 8, widgetId: 'cases-by-type', gridX: 0, gridY: 6, gridWidth: 6, gridHeight: 4 }, |
There was a problem hiding this comment.
[unfixed: design/product decision] SUGGESTION: case-map widget is defined in widgetDefs and has a working template slot, but has no entry in DEFAULT_LAYOUT. Row 3 (gridY 6) only fills the left half (cols 0–5); the right half is empty by default. If the Case Map widget should appear in the default dashboard, add { id: 12, widgetId: 'case-map', gridX: 6, gridY: 6, gridWidth: 6, gridHeight: 4 } here. No fix applied — layout is a product decision.
| typeBarWidth(count) { | ||
| const max = Math.max(1, ...this.typeData.map(t => t.count)) | ||
| const pct = (count / max) * 100 | ||
| return `max(20px, ${pct}%)` |
There was a problem hiding this comment.
[unfixed: out of fix scope] SUGGESTION: typeBarWidth is a near-exact copy of barWidth — only the data source differs (this.typeData vs this.statusData). Consider refactoring into a single private barWidthFor(count, data) helper to avoid the duplication growing as more bar-chart widgets are added. Not fixed in-container as it would change the barWidth call site (existing code) and is beyond the 1–3 line bounded fix rule.
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ❌ | ❌ | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-18 11:14 UTC
Download the full PDF report from the workflow artifacts.
Code Review — Juan Claude van DammeResult: PASS (2 fixed, 2 unfixed suggestions, 0 blocking)
ADR compliance
Notes on
|
Security Review — Clyde BarcodeResult: PASS (0 fixed, 0 unfixed, 0 blocking) ScopeChanged files reviewed: SAST — SemgrepRulesets: Manual OWASP Review
Informational note (pre-existing, not in diff)
|
…ng global t() - src/views/Dashboard.vue:492 — .map(t => t.count) → .map(tw => tw.count) The parameter name `t` shadowed the Nextcloud translation global `t()`. Follows the pattern of the existing barWidth method which uses `s => s.count`. Co-fixed-by: Juan Claude van Damme <hydra-reviewer@conduction.nl>
| * @return {string} CSS width value | ||
| */ | ||
| typeBarWidth(count) { | ||
| const max = Math.max(1, ...this.typeData.map(tw => tw.count)) |
There was a problem hiding this comment.
[fixed: renamed arrow param t→tw to avoid shadowing global t()] Rule: no-shadow — In typeBarWidth, the arrow function .map(t => t.count) used t as a parameter name, which shadows the Nextcloud translation global t(). Renamed to tw following the existing barWidth pattern which uses s => s.count.
| v-for="(item, index) in typeData" | ||
| :key="item.name" | ||
| class="status-bar-row status-bar-row--clickable" | ||
| @click="$router.push({ name: 'Cases', query: { caseType: item.typeId } })"> |
There was a problem hiding this comment.
[unfixed: product judgment required] Rule: REQ-DASH-003 navigation — When item.typeId is null (cases whose caseType field is absent or unresolvable map to 'Unknown' with typeId: null), clicking the bar navigates with { caseType: null }. Vue Router serialises null query values; whether the Cases view treats ?caseType= as a no-op or a filter depends on CnIndexPage internals. Guard the click or strip null from the query before navigation to make intent explicit.
| for (const c of openCases) { | ||
| const name = typeMap.get(c.caseType) || 'Unknown' | ||
| countMap.set(name, (countMap.get(name) || 0) + 1) | ||
| if (!idMap.has(name)) { |
There was a problem hiding this comment.
[unfixed: edge case, product judgment required] Rule: aggregateByType data accuracy — idMap stores the first resolved c.caseType ID for each name bucket. If multiple cases have unresolvable type IDs they all land in the 'Unknown' bucket, but only the first one's ID is used for navigation. Clicking 'Unknown' filters by one specific (unresolved) type ID rather than showing all typeless cases. Consider using typeId: null explicitly for the Unknown bucket and handling null in the navigation guard.
| { id: 8, widgetId: 'deadline-alerts', gridX: 0, gridY: 6, gridWidth: 4, gridHeight: 4 }, | ||
| { id: 9, widgetId: 'task-due-reminders', gridX: 4, gridY: 6, gridWidth: 4, gridHeight: 4 }, | ||
| { id: 10, widgetId: 'stalled-cases', gridX: 8, gridY: 6, gridWidth: 4, gridHeight: 4 }, | ||
| { id: 8, widgetId: 'cases-by-type', gridX: 0, gridY: 6, gridWidth: 6, gridHeight: 4 }, |
There was a problem hiding this comment.
[unfixed: product/UX decision] Rule: DEFAULT_LAYOUT completeness — The case-map widget was removed from DEFAULT_LAYOUT as part of fixing the duplicate-layout merge artefact. It remains in widgetDefs so users can add it manually, but it is no longer shown by default. The right half of the third row (gridX 6–11, gridY 6) is now empty in the default layout. If case-map should remain in the default dashboard, add it back at gridX 6, gridY 6, gridWidth 6, gridHeight 4.
Code Review — Juan Claude van DammeResult: PASS (1 fixed, 3 unfixed suggestions, 0 blocking)
Fixed
Unfixed (all SUGGESTION — not blocking)
Architecture compliance (ADRs 001/003/004/005/015)
See inline comments for per-finding detail. |
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ❌ | ❌ | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-18 11:41 UTC
Download the full PDF report from the workflow artifacts.
Security Review — Clyde BarcodeResult: PASS (0 fixed, 0 unfixed, 0 blocking) SAST ScanSemgrep scanned all 4 changed files with Changed-code manual review
Pre-existing informational note (not a blocking finding)
Verdict
PASS — no security issues introduced by this PR. |
|
Closing — will be rebuilt from scratch under the new 4-stage fire-once architecture (PR ConductionNL/hydra#15). |
Restores `npm ci`, `npm run lint`, and `npm run build` on `development`. Adds the missing peer-dependency chain for `@nextcloud/eslint-config@^8.4.1` and `@nextcloud/webpack-vue-config@^6.0.1`. `.npmrc` sets `legacy-peer-deps=true` so CI's `npm ci` (which doesn't take a flag) accepts the same resolution npm install produced. devDependencies added: - eslint-plugin-import ^2.32.0 - eslint-plugin-n ^16.6.2 - eslint-plugin-jsdoc ^46.10.1 - eslint-plugin-promise ^6.6.0 - eslint-plugin-vue ^9.33.0 - eslint-config-standard ^17.1.0 - @nextcloud/eslint-plugin ^2.2.1 - @vue/eslint-config-typescript ^13.0.0 - @babel/eslint-parser ^7.28.6 - @babel/core ^7.29.0 - eslint-import-resolver-exports ^1.0.0-beta.5 - eslint-import-resolver-typescript ^3.10.1 - node-polyfill-webpack-plugin ^3.0.0 - typescript ^5.9.3 dependencies added: - @nextcloud/auth ^2.6.0 (was used in 5 .vue files but never declared) Vue files in src/ contain only `npm run lint-fix` auto-fix output (vue/order-in-components reordering, attribute wrapping). No manual code edits. Mirror of pattern landed in decidesk #169, pipelinq #333, softwarecatalog #223. Gates after change: - `npm ci` succeeds - `npm run lint` — 7 errors / 13 warnings remain; these are pre-existing source bugs (parsing errors, v-slot scoping, unused vars, duplicate v-else-if) tracked separately in #323 — not caused by this baseline fix - `npm run build` — succeeds (webpack 5.105.2, 9 warnings)
Closes #135
Summary
Adds a Cases by Type horizontal bar chart widget to the Procest Dashboard. The widget aggregates open cases by case type name, sorted by count descending, using the same CSS bar chart pattern as the existing Cases by Status widget. Clicking any bar navigates to the Cases view filtered by that case type. This closes the gap identified in REQ-DASH-003. The PR also fixes a duplicate DEFAULT_LAYOUT array (a merge artefact) and a missing
</template>closing tag for the Case Map slot.Spec Reference
openspec/changes/dashboard/design.mdChanges
src/utils/dashboardHelpers.js— addedaggregateByType()function: groups open cases by case type name, resolves type IDs for navigation, returns array sorted by count descendingsrc/views/Dashboard.vue— addedtypeDatareactive state,typeBarWidth()method,cases-by-typewidget definition inwidgetDefs, layout entry inDEFAULT_LAYOUT(gridX 0, gridY 6, 6×4), and template slot#widget-cases-by-typewith clickable bar rows navigating to{ name: 'Cases', query: { caseType: item.typeId } }; fixed duplicate layout entries and missing</template>closing tagTest Coverage
tests/Unit/Dashboard/SignaleringWidgetsTest.php— existing tests cover CasesOverviewWidget (T02/T03 already implemented); the new helperaggregateByTypeis a pure JavaScript function with no PHP surface, covered by the spec acceptance criteria inopenspec/changes/dashboard/specs/dashboard/spec.md(REQ-DASH-003)