Skip to content

feat(groups): platform-toggle UI with confirm flow (TASK-2.2.2 2.2.2.1/2.2.2.2/2.2.2.4) - #9

Open
TheBoomerDev wants to merge 3 commits into
mainfrom
feat/task-2.2.2-platform-toggle-ui
Open

feat(groups): platform-toggle UI with confirm flow (TASK-2.2.2 2.2.2.1/2.2.2.2/2.2.2.4)#9
TheBoomerDev wants to merge 3 commits into
mainfrom
feat/task-2.2.2-platform-toggle-ui

Conversation

@TheBoomerDev

Copy link
Copy Markdown
Contributor

What

Implements TASK-2.2.2 (Platform Toggle UI) on cargoffer/education_frontend:

  • 2.2.2.1PlatformAccessPage mounted at /admin/platforms.
  • 2.2.2.2ConfirmPlatformChangeDialog wired into both GroupCard (via the existing GroupsList surface) and the new PlatformAccessPage.
  • 2.2.2.4 — i18n keys (en + es + fr, with defaultValue fallback for gl/pt/de/eu/ca/pl), WCAG 2.1 AA, dark-mode parity, Vitest coverage.

Subtask 2.2.2.3 (backend FEATURE_TOGGLED audit emission on PUT /api/groups/:id/platforms) is a separate BE PR on cargoffer/education_backend — out of scope here. The dialog already exposes an affectedUserCount slot wired for that follow-up.

Why

Until now, every chip click in GroupCard fired updatePlatforms without confirmation. TASK-2.2.2 replaces that destructive one-click behaviour with a modal confirmation flow:

  • Proposes the diff as added / removed platform chips with color-blind-safe icons + line-through.
  • Disables both Cancel and Confirm while a PUT is in flight (re-entrance guard).
  • Surfaces an "This will affect N users" amber note when 2.2.2.3 lands and the BE includes affectedUserCount.
  • PlatformAccessPage adds a dedicated super-admin surface for bulk platform-toggle operations across every group (per-platform stats row, search/filter bar, inline toggle grid).

Branch base

  • Branch: feat/task-2.2.2-platform-toggle-ui
  • Base: origin/main
  • Ahead: 3 atomic commits, behind: 0

Commits (keep this history — please squash-merge with the merge commit title)

  1. feat(admin/groups): add ConfirmPlatformChangeDialog (TASK-2.2.2.2) — new dialog + 8-case Vitest spec; Modal.tsx gets a small doc comment (no functional surface-area change).
  2. feat(admin): add PlatformAccessPage at /admin/platforms (TASK-2.2.2.1) — new page, AdminPanel route, 13 new flat i18n keys × 3 locales.
  3. refactor(admin/groups): lift platform-toggle dialog into GroupsList (TASK-2.2.2.2)GroupCard.onPlatformToggle renamed to onPendingPlatformChange(group, current, proposed); dialog state lifts to the list.

Files

Added (3):

  • src/components/admin/PlatformAccessPage.tsx
  • src/components/admin/groups/ConfirmPlatformChangeDialog.tsx
  • src/components/admin/groups/ConfirmPlatformChangeDialog.test.tsx

Modified (5):

  • src/components/admin/groups/GroupCard.tsx
  • src/components/admin/groups/GroupsList.tsx
  • src/components/admin/AdminPanel.tsx
  • src/components/ui/Modal.tsx (5-line doc comment only — no functional change)
  • src/i18n.ts (13 new flat keys × en + es + fr = 39 entries)

WCAG 2.1 AA

  • Focus trap, escape key, backdrop click — inherited from <Modal>.
  • Focus-on-open lands on Cancel (via a local useEffect in the dialog; not via a shared Modal prop).
  • aria-live="polite" region announces the proposed change once focus lands inside the modal.
  • Color is not the only signal — added chips carry a check icon, removed chips a close icon + line-through.
  • Light + dark parity on every color stop.

i18n

  • 13 new flat keys under groups.platformAccess.* + admin.platformAccess, populated for en + es + fr.
  • The remaining 7 locales (gl / pt / de / eu / ca / pl + the i18next-browser-languagedetector default) use react-i18next's defaultValue fallback — the same convention that the rest of the inline-stored translations already follow.
  • All aria-labels are t()-driven.

Tests

Vitest spec for ConfirmPlatformChangeDialog (8/8 passing):

  • group-name echo
  • added-platform chips render
  • removed-platform chips render (red, line-through)
  • "no-changes" branch (Confirm disabled)
  • Cancel → onClose
  • Confirm → onConfirm exactly once
  • Re-entrance: BOTH buttons disabled while onConfirm is in flight, onClose fires on resolution

Validation

$ npx tsc --noEmit -p tsconfig.app.json
$ npm run test:run -- src/components/admin/groups/ConfirmPlatformChangeDialog.test.tsx

→ 0 TypeScript errors, 8/8 dialog tests passing.

Pre-existing issues (NOT introduced by this PR — flagged for visibility only)

  • src/components/admin/audit/RecentPlatformChangesWidget.tsx:106@typescript-eslint/no-explicit-any (pre-existing; not in this PR's diff).
  • 29 pre-existing test failures in unrelated specs (e.g. src/components/auth/ProtectedRoute.test.tsx looking for legacy strings like "Instructor Courses" / "Student Dashboard"; older copy drifted from the actual <h1>).
  • Husky v10.0.0 deprecation warning spotted during commit (#!/usr/bin/env sh + . "$(dirname -- "$0")/_/husky.sh" are flagged for removal).

Reviewer checklist

  • Modal.tsx diff reduces to a documentation comment only.
  • No new any types introduced anywhere.
  • Every user-facing string routes through t(key, defaultValue) (no hardcoded English leaks).
  • GroupCard no longer fires updatePlatforms directly — it emits a pending change through onPendingPlatformChange.
  • PlatformAccessPage row toggle buttons also emit pending changes (parity with the GroupCard path).
  • Re-entrance guard verified: clicking Confirm twice does NOT double-fire the PUT.

Forward-look

  • Subtask 2.2.2.3 (BE FEATURE_TOGGLED audit emission + affectedUserCount) on cargoffer/education_backend — natural follow-up. When that PR lands, the dialog can populate the amber "X users will be affected" note without further UI changes.
  • Next task in the Phase-2 queue is TASK-2.3.1 (Global User API) per docs/planning/PRIORITIES.md.

Cross-repo refs:

  • Parent-repo kickoff PR: cargoffer/education_portal PR #107 — docs(planning): kickoff TASK-2.2.2 platform-toggle-ui on feat/task-2.2.2-platform-toggle-ui (still open at time of writing).
  • Spec: docs/planning/tasks/task-2.2.2.md in the planning-repo submodule of the parent project.

Adds a Modal-based confirmation dialog for platform-toggle changes on
groups. The diff body lists added/removed platform chips with
color-blind-safe icons + line-through; an aria-live="polite" region
announces the pending change once focus lands inside the modal; a
spinner blocks double-click while updatePlatforms is in flight.

WCAG 2.1 AA: focus-on-cancel via a local useEffect (no Modal-level
prop addition). Re-entrance guard prevents stale-state races. Light
+ dark parity on every color stop.

Modal.tsx receives a small comment note describing how future
consumers can land focus on a non-default element; no functional
change to the primitive.

Vitest spec (ConfirmPlatformChangeDialog.test.tsx) covers: group-name
echo, added/removed chip render, no-changes branch, cancel close,
confirm fire, confirm is no-op when proposed == current, loading-state
disables both buttons, and onClose fires on resolution.

Refs:
  - parent-repo docs/planning/tasks/task-2.2.2.md (TASK-2.2.2.#Kickoff)
  - parent-repo PR #107 (kickoff planning PR — already merged)
New page mounted at /admin/platforms. Mirrors GroupsList conventions
(stats row → filter bar → card grid), but each card carries inline
toggle buttons that open the shared ConfirmPlatformChangeDialog
emitted in commit 1 of this series.

AdminPanel route map and getPageTitle case added.

13 new flat-key i18n entries under groups.platformAccess.* plus
admin.platformAccess, populated for en + es + fr. The remaining
7 locales (gl/pt/de/eu/ca/pl/...) rely on react-i18next's
defaultValue fallback, matching the project's existing convention
for inline-stored translations.

Both aria-labels are t()-driven (WCAG/Dark-mode parity maintained);
disabled groups render with a visibly de-emphasized toggle grid.

Refs:
  - parent-repo docs/planning/tasks/task-2.2.2.md (TASK-2.2.2.#Kickoff)
…TASK-2.2.2.2)

GroupCard no longer executes updatePlatforms directly on every chip
click. The legacy `onPlatformToggle: (platform: Platform) => void`
prop is renamed to `onPendingPlatformChange:
(group, current, proposed) => void` — a staged change request.
GroupsList owns the dialog state and renders ONE shared
ConfirmPlatformChangeDialog at the bottom of the list, regardless
of which card the user clicked.

This establishes the consistent pattern across both surfaces:
GroupCard and PlatformAccessPage both EMIT pending changes; their
parents CONFIRM. Future re-use (audit, undo, multi-toggle) just
needs the existing onConfirm callback signature.

API break note: GroupCard's `onPlatformToggle` prop is removed;
GroupsList is the only consumer and was updated in this commit.
Comment thread src/i18n.ts
browse: 'Browse',
viewProgress: 'View progress',
'groups.platformAccess.pageTitle': 'Platform Access',
'groups.platformAccess.pageSubtitle': 'Manage platform access across all groups. Changes require confirmation.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Missing translations for 5 required languages

Translation keys are only provided for en, es, fr locales. According to i18n requirements, translations must be added for ALL 8 languages: en, es, fr, gal (gl), pt, de, eu, ca.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

>
<MaterialIcon name="close" size="text-sm" decorative />
<span>{PLATFORMS[p].name}</span>
</li>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Confirm button missing dark mode background variants

The confirm button only specifies bg-blue-600 hover:bg-blue-700 without corresponding dark: variants. All buttons must have explicit dark mode background colors per UI/UX standards.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<div className="text-xs font-semibold text-red-700 dark:text-red-400 uppercase tracking-wide">
{t(
'groups.platformAccess.removedHeading',
'Will be disabled'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Cancel button relies on default browser styling

The cancel button uses default browser styling instead of explicit background colors with dark mode variants. All buttons should define explicit background colors for consistent appearance across themes.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant