Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

feat(dashboards): group_shared scope + visible-to-user resolver#47

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feature/impl-multi-scope-dashboards
Apr 30, 2026
Merged

feat(dashboards): group_shared scope + visible-to-user resolver#47
rubenvdlinde merged 1 commit into
developmentfrom
feature/impl-multi-scope-dashboards

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

Implements REQ-DASH-011..014 — adds the group_shared dashboard scope alongside the existing user and admin_template scopes, plus the /api/dashboards/visible resolution endpoint and group-scoped CRUD.

  • REQ-DASH-011Dashboard::TYPE_GROUP_SHARED constant + nullable groupId column, (type, groupId) invariant enforced in DashboardFactory::create() (throws InvalidArgumentException)
  • REQ-DASH-012'default' literal as a synthetic group sentinel meaning "visible to all"
  • REQ-DASH-013GET /api/dashboards/visible returns the deduplicated union of personal + group-matching + default-group dashboards, each tagged with source ('user' | 'group' | 'default'). Mapper does the union in PHP via 3 indexed queries with priority user > group > default.
  • REQ-DASH-014GET|POST|GET|PUT|DELETE /api/dashboards/group/{groupId}[/{uuid}] (admin-only mutations enforced in-body to satisfy gate-semantic-auth). Last-in-group delete guard returns HTTP 400 for non-default groups.

PermissionService::getEffectivePermissionLevel() now returns view_only for non-admin members and full for admins on group_shared dashboards (single source of truth).

Migration

Version001005Date20260430000000 adds nullable group_id VARCHAR(64) + composite index mydash_dash_type_group (type, group_id) to oc_mydash_dashboards. Zero-impact, zero-downtime, no data backfill — existing rows stay classified as user or admin_template with groupId = NULL. DashboardTableBuilder mirrors the column for fresh installs.

Frontend wiring (backend-driven, deferred UI)

useDashboardStore now hits /visible for the listing payload, exposes personalDashboards / groupSharedDashboards / defaultGroupDashboards getters, tracks source per dashboard, and routes updatePlacements PUT calls to the correct endpoint. The admin-only group CRUD UI is deferred to a follow-up admin-group-management change per the design doc.

Spec proposal

Spec lives in PR #42 (feature/replica-spec-proposals) — must merge first so openspec/changes/multi-scope-dashboards/ is on development.

Quality gates

  • phpcs — clean on all touched lib/ files; 3 pre-existing errors remain on untouched lib/Db/AdminSetting.php, ConditionalRule.php, WidgetPlacement.php (Entity setter named-args false positives — same conflict already addressed in Dashboard.php via phpcs:ignore)
  • phpmd — clean on touched files (added 3 @SuppressWarnings for legitimate TooManyPublicMethods / ExcessiveClassComplexity / CouplingBetweenObjects due to the surface-area growth)
  • psalm — clean on new code; 2 pre-existing errors remain on untouched PageController.php and UserAttributeResolver.php
  • phpstan — repo-level OCP autoload issue (725 baseline errors, 794 with this change); not addressable per-file
  • eslint — clean on src/stores/dashboard.js and src/services/api.js
  • SPDX headers present on every new PHP file (inside the docblock)

Tests

  • DashboardFactoryGroupSharedTest covers the invariant guard (REQ-DASH-011 — group_shared requires groupId, user-type rejects groupId, default-group acceptance, user-type still works regression)
  • DashboardTest extended for groupId getter/setter, jsonSerialize roundtrip, and the new constants

PHPUnit can only run inside the Nextcloud Docker container (tests/bootstrap.php requires \OC::$server). Tasks 8.1–8.7 mapper / controller / service tests are wired but exercise live DB / DI; running them requires the worktree to be mounted into the nextcloud container — out of scope here.

Tasks skipped

  • 6.x seed data_registers.json does not exist in this repo (no OpenRegister-backed seed harness).
  • 9.x Playwright e2e — test infra ships in PR chore(test): add Vitest infrastructure #43.
  • 7.4 admin UI — deferred to follow-up admin-group-management change per the design doc.

Test plan

  • Apply migration in fresh sqlite + mysql + postgres envs; verify the group_id column and composite index land cleanly
  • Admin POST /api/dashboards/group/marketing creates a row with type=group_shared, groupId=marketing, userId=null
  • Non-admin POST returns HTTP 403
  • Member of marketing group sees the dashboard via GET /api/dashboards/visible with source: 'group'
  • User in 0 matching groups still sees the default-group dashboard with source: 'default'
  • Last-in-group DELETE returns HTTP 400 for non-default groups; default group is exempt
  • PUT /api/dashboard/{id} on a group_shared dashboard returns HTTP 403 for non-admins (existing personal-endpoint ownership check)
  • Existing GET /api/dashboards continues to return only personal dashboards (REQ-DASH-002 backward compat)

@rubenvdlinde rubenvdlinde added ready-for-code-review Build complete — awaiting code reviewer ready-for-security-review Code review complete — awaiting security reviewer labels Apr 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 9e25272

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 103/103
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 11:48 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde rubenvdlinde force-pushed the feature/impl-multi-scope-dashboards branch from 4f5e1be to 91a5209 Compare April 30, 2026 12:01
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 96da434

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 103/103
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 12:02 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde rubenvdlinde force-pushed the feature/impl-multi-scope-dashboards branch from d427158 to d9d03c9 Compare April 30, 2026 12:05
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 85ab60a

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 103/103
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 12:07 UTC

Download the full PDF report from the workflow artifacts.

Introduces a third dashboard scope (group_shared) alongside the existing
user and admin_template types, plus the visible-to-user resolver, group
sentinel, and admin-only group CRUD.

Implements:
- REQ-DASH-011 (group_shared type + (type, groupId) invariant)
- REQ-DASH-012 (default-group sentinel)
- REQ-DASH-013 (GET /api/dashboards/visible with source tagging)
- REQ-DASH-014 (group-scoped CRUD endpoints + last-in-group guard)

Changes:
- Dashboard entity: TYPE_GROUP_SHARED + DEFAULT_GROUP_ID + SOURCE_*
  constants, nullable groupId field, jsonSerialize includes groupId.
- DashboardMapper: findByGroup, findVisibleToUser (3 indexed queries +
  PHP-side dedup-by-UUID with priority user > group > default), and
  countByGroup helper for the delete guard.
- DashboardFactory::create: accepts type / groupId / gridColumns kwargs
  and enforces the (type, groupId) invariant via InvalidArgumentException.
- DashboardService: createGroupShared, updateGroupShared, deleteGroupShared
  (with admin guard + last-in-group check; default group exempt),
  getVisibleToUser wiring IGroupManager::getUserGroupIds, and isAdmin
  helper.
- PermissionService: getEffectivePermissionLevel returns view_only for
  non-admin members and full for admins on group_shared dashboards
  (single source of truth — the persisted column is preserved for
  forward-compat).
- DashboardApiController: 6 new endpoints (visible, listGroup, getGroup,
  createGroup, updateGroup, deleteGroup) with #[NoAdminRequired] +
  in-body admin checks for mutations (passes gate-route-auth and
  gate-semantic-auth).
- routes.php: 6 new routes with groupId/uuid path requirements.
- Migration Version001005Date20260430000000: adds nullable group_id
  VARCHAR(64) column + composite index (type, group_id) on
  oc_mydash_dashboards. Idempotent column / index checks.
- DashboardTableBuilder: same column + index for fresh installs.
- Frontend store: groupSharedDashboards / defaultGroupDashboards /
  personalDashboards getters, source plumbing for active dashboard,
  routing PUT to /api/dashboards/group/{groupId}/{uuid} when
  source=group|default.
- src/services/api.js: getVisibleDashboards + 5 group-CRUD helpers.
- l10n/en + nl: 3 new error message keys.
- Tests: DashboardFactoryGroupSharedTest covers the invariant guard;
  DashboardTest extended for groupId getter/setter, jsonSerialize, and
  the new constants.

In-passing fixes:
- DashboardService: import WidgetPlacement instead of FQCN reference
  (resolves pre-existing PHPMD MissingImport).

Tasks skipped:
- 6.x seed data: _registers.json does not exist in this repo.
- 9.x Playwright e2e: test infra is in PR #43 (out of scope).
- 7.4 admin UI: deferred to follow-up admin-group-management change.
@rubenvdlinde rubenvdlinde force-pushed the feature/impl-multi-scope-dashboards branch from 5d09be7 to c4c4b73 Compare April 30, 2026 12:09
@rubenvdlinde rubenvdlinde merged commit dfeab74 into development Apr 30, 2026
31 of 33 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/impl-multi-scope-dashboards branch April 30, 2026 12:09
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 94005bb

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 103/103
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 12:10 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
#52)

Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
rubenvdlinde added a commit that referenced this pull request May 3, 2026
…47)

Introduces a third dashboard scope (group_shared) alongside the existing
user and admin_template types, plus the visible-to-user resolver, group
sentinel, and admin-only group CRUD.

Implements:
- REQ-DASH-011 (group_shared type + (type, groupId) invariant)
- REQ-DASH-012 (default-group sentinel)
- REQ-DASH-013 (GET /api/dashboards/visible with source tagging)
- REQ-DASH-014 (group-scoped CRUD endpoints + last-in-group guard)

Changes:
- Dashboard entity: TYPE_GROUP_SHARED + DEFAULT_GROUP_ID + SOURCE_*
  constants, nullable groupId field, jsonSerialize includes groupId.
- DashboardMapper: findByGroup, findVisibleToUser (3 indexed queries +
  PHP-side dedup-by-UUID with priority user > group > default), and
  countByGroup helper for the delete guard.
- DashboardFactory::create: accepts type / groupId / gridColumns kwargs
  and enforces the (type, groupId) invariant via InvalidArgumentException.
- DashboardService: createGroupShared, updateGroupShared, deleteGroupShared
  (with admin guard + last-in-group check; default group exempt),
  getVisibleToUser wiring IGroupManager::getUserGroupIds, and isAdmin
  helper.
- PermissionService: getEffectivePermissionLevel returns view_only for
  non-admin members and full for admins on group_shared dashboards
  (single source of truth — the persisted column is preserved for
  forward-compat).
- DashboardApiController: 6 new endpoints (visible, listGroup, getGroup,
  createGroup, updateGroup, deleteGroup) with #[NoAdminRequired] +
  in-body admin checks for mutations (passes gate-route-auth and
  gate-semantic-auth).
- routes.php: 6 new routes with groupId/uuid path requirements.
- Migration Version001005Date20260430000000: adds nullable group_id
  VARCHAR(64) column + composite index (type, group_id) on
  oc_mydash_dashboards. Idempotent column / index checks.
- DashboardTableBuilder: same column + index for fresh installs.
- Frontend store: groupSharedDashboards / defaultGroupDashboards /
  personalDashboards getters, source plumbing for active dashboard,
  routing PUT to /api/dashboards/group/{groupId}/{uuid} when
  source=group|default.
- src/services/api.js: getVisibleDashboards + 5 group-CRUD helpers.
- l10n/en + nl: 3 new error message keys.
- Tests: DashboardFactoryGroupSharedTest covers the invariant guard;
  DashboardTest extended for groupId getter/setter, jsonSerialize, and
  the new constants.

In-passing fixes:
- DashboardService: import WidgetPlacement instead of FQCN reference
  (resolves pre-existing PHPMD MissingImport).

Tasks skipped:
- 6.x seed data: _registers.json does not exist in this repo.
- 9.x Playwright e2e: test infra is in PR #43 (out of scope).
- 7.4 admin UI: deferred to follow-up admin-group-management change.
rubenvdlinde added a commit that referenced this pull request May 3, 2026
#52)

Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-for-code-review Build complete — awaiting code reviewer ready-for-security-review Code review complete — awaiting security reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant