Skip to content

Phase 8: Collaboration & Department Workflow#160

Merged
NesiciCoding merged 7 commits into
mainfrom
claude/silly-shtern-3ca83d
Jun 22, 2026
Merged

Phase 8: Collaboration & Department Workflow#160
NesiciCoding merged 7 commits into
mainfrom
claude/silly-shtern-3ca83d

Conversation

@NesiciCoding

@NesiciCoding NesiciCoding commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

Implements Phase 8 of the roadmap (Collaboration & Department Workflow), per the wiki roadmap's themes:

  • 8.1 Co-grading & moderation — a second-marker grade reuses the peer-review data model end-to-end (StudentRubric with isPeerReview: true, gradedBy set to the colleague's name/email). peerReviewAggregator.ts's existing delta math needed no changes since it's already generic over reviewer identity. New src/utils/coGradingModerationQueue.ts flags disputes above a configurable point threshold; a "Co-grade" button on Grade Student opens the same Peer Review screen with a colleague as reviewer; a new /moderation page lists disputes with a per-criterion delta and keep/accept resolution.
  • 8.2 Department rubric/comment-bank libraries — a sharedWithSchool flag reused inside the existing rubric/comment-bank data jsonb column (no new table), with a new RLS SELECT policy (041_school_sharing.sql) mirroring the Phase 6 marketplace's school_members join pattern. Unlike the Marketplace (clone a frozen snapshot), this shares the live rubric read-only with every teacher in the same school.
  • 8.3 Grading task assignment — new GradingTask type/table (042_grading_tasks.sql). From the Activity Dashboard, batch-assign a class's ungraded submissions for a rubric to a specific colleague. Completion is derived (no separate "done" flag) — a task disappears once a matching grade exists.
  • 8.4 Manual reordering — shared src/utils/displayOrder.ts helper wired into RubricList, TestListPage, EssayListPage, and the Activity Dashboard via the existing @hello-pangea/dnd pattern already used in RubricBuilder for criteria/levels.

Also updates the wiki roadmap (pushed directly, separate repo) marking Phase 7 done and fleshing out 8.5 (cohort-based filtering) into a concrete "Proposed — not yet scheduled" plan, per CLAUDE.md's documentation-maintenance rule plus DocsPage.tsx, README.md, and LandingPage.tsx. i18n keys added across all 5 locales (en/nl/fr/de/es).

Test plan

  • npm run typecheck — clean
  • npm run lint — 0 errors (150 pre-existing no-explicit-any warnings, +1 from a new test file matching existing style)
  • npx prettier --check — clean on all touched/new files
  • npm test — full unit suite green (1788 tests), including new tests for displayOrder.ts and coGradingModerationQueue.ts
  • New Playwright e2e specs:
    • 23-co-grading-moderation.spec.ts (5 tests), 25-grading-task-assignment.spec.ts (3 tests), 26-list-reordering.spec.ts (3 tests, real @hello-pangea/dnd drag simulation) — all verified passing locally on chromium, firefox, and webkit, plus folded into the full existing chromium suite (99/99 passing) with no regressions
    • 24-department-sharing.spec.ts (2 tests) — requires a real Supabase stack (added a colleaguePage fixture to supabase.fixture.ts for a second distinct teacher in the same school). Not run locally (no Docker in this environment) — same constraint as the existing 14/15/16/17/18/20 specs; wired into playwright.config.ts's supabase project, runs in CI via npm run e2e:supabase
  • Manual browser-preview verification: drag-reorder on Rubrics/Activity Dashboard, grading-task assign→appear→delete flow end-to-end, Moderation queue empty/populated states, Comment Bank merged-list rendering — no console errors

Reviewer notes

  • 041_school_sharing.sql and 042_grading_tasks.sql have not been applied against a live Supabase instance in this environment (no Docker available) — recommend running npm run db:reset before merge to confirm they apply cleanly.
  • 24-department-sharing.spec.ts is similarly unverified pending a CI run with the local Supabase stack.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a co-grading & moderation workflow with a dispute moderation queue and configurable threshold.
    • Enabled department-wide read-only sharing for rubrics and comment-bank items.
    • Added manual drag-and-drop reordering for rubrics, tests, and essay lists (order is saved).
    • Added grading-task assignment from the Activity Dashboard, including a “pending grading tasks” panel with delete support.
    • Added a new Moderation page for resolving disputes.
  • Documentation
    • Updated docs and translations for co-grading/moderation, department sharing, and manual reordering.
  • Tests
    • Added end-to-end coverage for the new workflows and list reordering, plus related unit tests.

Adds the four Phase 8 roadmap items: co-grading/moderation (8.1, reuses the
peer-review data model end-to-end), department rubric/comment-bank sharing
(8.2, live read-only visibility via schools/school_members RLS), grading
task assignment (8.3, batch-assign ungraded submissions to a colleague from
the Activity Dashboard), and drag-to-reorder rubrics/tests/essays (8.4, via
the existing @hello-pangea/dnd pattern from RubricBuilder).

Includes Supabase migrations for school sharing and grading tasks, i18n
across all 5 locales, doc updates (DocsPage/README/LandingPage), and new
Playwright e2e coverage (23, 25, 26 verified locally across chromium/
firefox/webkit; 24 is Supabase-backed and CI-only, same constraint as the
existing 14/15/16/17/18/20 specs).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@NesiciCoding, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 59 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5336a082-a443-4205-8f26-1cf8584bc49d

📥 Commits

Reviewing files that changed from the base of the PR and between 5c748f2 and 2c3be87.

📒 Files selected for processing (2)
  • e2e/specs/24-department-sharing.spec.ts
  • src/services/database/SupabaseAdapter.ts
📝 Walkthrough

Walkthrough

This PR implements Phase 8 of RubricMaker, adding: (1) department-wide read-only rubric and comment-bank sharing via new Supabase RLS policies; (2) a co-grading second-marker workflow with a new ModerationQueuePage and configurable dispute threshold; (3) batch grading-task assignment from the Activity Dashboard with cloud-synced persistence; and (4) drag-and-drop manual reordering for rubric, test, and essay lists backed by a new displayOrder field and shared utility helpers. Five locales, two DB migrations, and four new E2E specs are included.

Changes

Phase 8: Co-grading, Department Sharing, Grading Tasks & Reordering

Layer / File(s) Summary
Domain types, display order utilities, and unit tests
src/types/index.ts, src/utils/displayOrder.ts, src/utils/displayOrder.test.ts
Rubric, CommentBankItem, EssayAssignment, and Test gain optional displayOrder/sharedWithSchool fields; new GradingTask interface added; sortByDisplayOrder and reorderDisplayOrder helpers introduced and unit tested.
Supabase migrations: school sharing RLS and grading_tasks table
supabase/migrations/041_school_sharing.sql, supabase/migrations/042_grading_tasks.sql
Migration 041 adds FOR SELECT RLS policies on rubrics and comment_bank gated on the data.sharedWithSchool JSONB flag plus school membership lookup. Migration 042 creates the grading_tasks table with owner-only RLS.
GradingTask storage, context actions, and cloud sync
src/store/storage.ts, src/context/AppContext.tsx, src/services/database/StorageSync.ts, src/services/database/SupabaseAdapter.ts
Adds saveGradingTasks and the gradingTasks slice to localStorage; wires ADD_GRADING_TASKS/DELETE_GRADING_TASK reducer actions with cloud push in AppContext; extends StorageSyncService hydration, pushAll, and pushOne; adds SupabaseAdapter CRUD for grading tasks and school-shared fetchers for comment bank and rubrics.
Co-grading moderation queue logic and ModerationQueuePage
src/utils/coGradingModerationQueue.ts, src/utils/coGradingModerationQueue.test.ts, src/pages/ModerationQueuePage.tsx, src/App.tsx, src/components/Layout/Sidebar.tsx
Adds isSecondMarkerEntry and getModerationQueue with unit tests; new ModerationQueuePage computes a threshold-filtered queue and provides keep/accept resolution; route /moderation registered and nav item added.
Co-grade entry point in GradeStudent
src/pages/GradeStudent.tsx
Adds a conditional "Co-grade" Topbar button (visible when a graded rubric record exists), a modal to collect a colleague name, and navigation to the peer-review route with reviewerId query param.
Department sharing UI: RubricList and CommentBankModal
src/pages/RubricList.tsx, src/components/Comments/CommentBankModal.tsx
RubricList fetches school-shared rubrics, adds toggleSharedWithSchool, renders a separate department section and a share-with-department checkbox. CommentBankModal merges owned and school-shared items, adds a share-with-department toggle and a department badge for non-owned items.
Drag-and-drop manual reordering: RubricList, TestListPage, EssayListPage, aggregator
src/pages/RubricList.tsx, src/pages/TestListPage.tsx, src/pages/EssayListPage.tsx, src/utils/activityDashboardAggregator.ts
All three list pages wrap their card grids with @hello-pangea/dnd primitives; handleDragEnd uses reorderDisplayOrder and persists via update* context methods; activityDashboardAggregator now orders all row kinds by displayOrder.
ActivityDashboard: grading task assignment panel and DnD row reordering
src/pages/ActivityDashboardPage.tsx, src/pages/__tests__/pages-phase4.a11y.test.tsx
Adds pendingTasks computation, an assign-task modal flow, a pending-tasks card UI with per-task delete, and DnD-wrapped activity matrix rows with a handleDragEnd that persists displayOrder for rubric/test/essay rows.
i18n strings, documentation, README, and LandingPage
src/locales/*.json, src/pages/DocsPage.tsx, src/pages/LandingPage.tsx, README.md
Adds moderation, drag_to_reorder, department-sharing, coGrading, and gradingTasks keys across five locales; expands DocsPage with new subsections; updates README routes and utility table; adds "Department Collaboration" to LandingPage features.
E2E test infrastructure: fixtures, page objects, and Playwright config
e2e/fixtures/supabase.fixture.ts, e2e/pages/BasePage.ts, e2e/pages/ActivityDashboardPage.ts, e2e/pages/EssayListPage.ts, e2e/pages/ModerationQueuePage.ts, e2e/pages/RubricListPage.ts, e2e/pages/TestListPage.ts, playwright.config.ts
Adds colleagueEmail/colleaguePage fixtures for a second teacher in the same school; introduces dragReorder on BasePage; creates ActivityDashboardPage, EssayListPage, ModerationQueuePage page objects; extends RubricListPage/TestListPage with cardTitles/dragHandle helpers; routes spec 24 to the supabase project only.
E2E specs 23–26
e2e/specs/23-co-grading-moderation.spec.ts, e2e/specs/24-department-sharing.spec.ts, e2e/specs/25-grading-task-assignment.spec.ts, e2e/specs/26-list-reordering.spec.ts
Covers co-grading moderation queue visibility/resolution, department rubric sharing (colleague sees/doesn't see), grading task assignment persistence and deletion, and drag-and-drop reordering persistence for rubrics, tests, and essays.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    Note over Teacher,ModerationQueuePage: Co-grading workflow
  end
  participant Teacher
  participant GradeStudent
  participant PeerReviewPage
  participant ModerationQueuePage
  participant getModerationQueue
  participant AppContext

  Teacher->>GradeStudent: click "Co-grade" (existingSR present)
  GradeStudent->>GradeStudent: open modal, enter colleague name
  GradeStudent->>PeerReviewPage: navigate /peer-review/?reviewerId=colleague
  PeerReviewPage->>AppContext: save peer-review StudentRubric (isPeerReview=true, gradedBy=colleague)

  Teacher->>ModerationQueuePage: open /moderation, set threshold
  ModerationQueuePage->>getModerationQueue: rubrics, peerReviews, students, threshold
  getModerationQueue-->>ModerationQueuePage: ModerationQueueItem[] sorted by totalAbsDelta desc
  Teacher->>ModerationQueuePage: keepOriginal
  ModerationQueuePage->>AppContext: deleteStudentRubric(secondMarkerId)
  Teacher->>ModerationQueuePage: acceptSecondMarker
  ModerationQueuePage->>AppContext: updateStudentRubric(baseline with second-marker data)
  ModerationQueuePage->>AppContext: deleteStudentRubric(secondMarkerId)
Loading
sequenceDiagram
  rect rgba(144, 238, 144, 0.5)
    Note over Teacher,SupabaseDB: Grading task assignment and persistence
  end
  participant Teacher
  participant ActivityDashboard
  participant AppContext
  participant StorageSync
  participant SupabaseDB

  Teacher->>ActivityDashboard: click Assign on activity row
  ActivityDashboard->>ActivityDashboard: openAssignTaskModal, fill teacher + due date
  Teacher->>ActivityDashboard: confirm assign
  ActivityDashboard->>AppContext: addGradingTasks(tasks[]) for ungraded students
  AppContext->>AppContext: dispatch ADD_GRADING_TASKS → localStorage
  AppContext->>StorageSync: pushOne('gradingTask', 'upsert', task) per task
  StorageSync->>SupabaseDB: upsert grading_tasks row
  ActivityDashboard->>ActivityDashboard: render pendingTasks card
  Teacher->>ActivityDashboard: delete pending task
  ActivityDashboard->>AppContext: deleteGradingTask(id)
  AppContext->>StorageSync: pushOne('gradingTask', 'delete', null, id)
  StorageSync->>SupabaseDB: delete grading_tasks row
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • NesiciCoding/RubricMaker#162: This issue proposes replacing the free-text colleague name input in the co-grade modal with a stable roster-based picker and UUID-based identity matching in src/pages/GradeStudent.tsx and src/utils/coGradingModerationQueue.ts — directly addressing the current implementation's reviewer identification approach.

Possibly related PRs

  • NesiciCoding/RubricMaker#84: Both PRs modify the same student-portal routing logic in src/App.tsx around linkedStudent lookup (null fallback/coalescing), establishing a shared dependency on routing entry conditions.
  • NesiciCoding/RubricMaker#93: This PR extends e2e/pages/BasePage.ts with dragReorder, building directly on the Playwright page-object infrastructure introduced in PR #93.
  • NesiciCoding/RubricMaker#145: Both PRs modify the Activity Dashboard at the code level—src/utils/activityDashboardAggregator.ts row aggregation and src/pages/ActivityDashboardPage.tsx UI/actions—establishing a shared feature dependency.

Poem

🐰 Hop, drag, and reorder with glee,
A second marker checks by decree.
The moderation queue lines them neat—
Keep or accept, the review's complete!
Department badges share with pride,
Tasks assigned and graded side-by-side. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main objective (Phase 8 collaboration & department workflow), which matches the substantive changes across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/silly-shtern-3ca83d

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 60.79% (🎯 52%) 5531 / 9098
🔵 Statements 59.43% (🎯 51%) 6299 / 10599
🔵 Functions 49.15% (🎯 42%) 1822 / 3707
🔵 Branches 50.8% (🎯 43%) 4433 / 8725
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/Comments/CommentBankModal.tsx 84.61% 82.5% 84.37% 86.3% 30, 40, 60, 83-87, 97, 188, 247-251
src/components/Layout/Sidebar.tsx 96% 86.3% 100% 95.65% 53
src/context/AppContext.tsx 55.28% 38.19% 49.77% 57.76% 145, 196-215, 244-260, 310, 348, 351, 363, 366, 399, 441, 449, 462, 469-475, 493-598, 603, 779-798, 864-865, 875-943, 951, 955-959, 960-961, 967-973, 989-1014, 1027-1055, 1085-1086, 1117-1118, 1137, 1165, 1279, 1291-1294, 1298, 1301, 1304, 1307, 1310, 1314, 1318, 1323, 1327-1328, 1331-1332, 1335-1336, 1339-1340, 1346-1365, 1372-1373, 1377, 1381-1385, 1391, 1395-1402, 1406, 1409, 1411, 1415, 1419, 1422, 1423, 1425, 1429, 1432, 1436-1437, 1441-1442, 1447-1449, 1455, 1459, 1463, 1467-1470, 1476-1490, 1501, 1503, 1506, 1509, 1512, 1513, 1515, 1519, 1524-1535, 1655
src/pages/ActivityDashboardPage.tsx 33.09% 24.6% 28.78% 35.71% 59-96, 107, 108, 135-157, 163-177, 183-192, 203-296, 443-447, 509-633, 649-684
src/pages/DocsPage.tsx 0% 0% 0% 0% 46-1360
src/pages/EssayListPage.tsx 0% 0% 0% 0% 13-191
src/pages/GradeStudent.tsx 39.52% 36.41% 28.37% 40.75% 67, 68, 78-81, 99-122, 172, 173-179, 227, 229, 244, 254, 258-270, 281, 283, 286, 300-309, 313-328, 335, 343-388, 398-400, 408-409, 414-419, 425, 431-433, 440-444, 451-459, 465-470, 477, 482-484, 490, 492, 493, 494, 499-500, 507-508, 518-719, 776-783, 858-977, 1141-1271, 1307-1311, 1341-1873
src/pages/LandingPage.tsx 69.69% 82.75% 84.61% 68.75% 152-159, 315, 344
src/pages/ModerationQueuePage.tsx 0% 0% 0% 0% 10-159
src/pages/RubricList.tsx 31.01% 28.18% 25.55% 32.98% 73-119, 128-173, 190-218, 223, 237-249, 271, 319, 362-466, 485-486, 550-576, 587, 590, 601-604, 655-739, 790-1095
src/pages/TestListPage.tsx 29.62% 21.15% 26.53% 31.86% 38-41, 54-85, 91, 105-110, 118-129, 161-164, 222-282, 298-473, 500, 501-513
src/store/storage.ts 85.18% 75.55% 88.46% 83.23% 247-248, 429, 433, 437, 441, 445, 455, 459, 463, 467, 471, 475, 479, 484, 488, 492, 499-501, 505, 509, 513, 520-525, 529, 536-538, 542, 559-563, 665
src/utils/activityDashboardAggregator.ts 100% 100% 100% 100%
src/utils/coGradingModerationQueue.ts 87.5% 85% 87.5% 100% 46, 53, 60, 88
src/utils/displayOrder.ts 100% 100% 100% 100%
Generated in workflow #564 for commit 2c3be87 by the Vitest Coverage Report Action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 22

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/fixtures/supabase.fixture.ts`:
- Line 365: The fixture callback in the supabase.fixture.ts file is triggering
lint violations due to the empty destructuring pattern and the parameter name
`use`. Rename the callback parameters: replace the empty destructuring pattern
`{}` with either an underscore `_` or a descriptive name, and rename the `use`
parameter to something like `useFixture` or `utilizeFixture` to avoid triggering
the React hooks lint rule. Apply this same fix to both occurrences in the
fixture file (lines 365 and 389).
- Around line 174-193: The Promise.all block containing the three fetch calls to
school_members, profiles, and user_settings endpoints does not validate the
response status codes. Add response validation for each fetch call by checking
the ok property or status code of each response, and throw an error if any
request fails so that fixture setup halts immediately rather than continuing
with potentially inconsistent backend state.

In `@src/components/Comments/CommentBankModal.tsx`:
- Around line 238-254: The button element in the share-with-department
functionality (the one that toggles sharedWithSchool and displays the Users2
icon) is missing an explicit type attribute. Add type="button" to this button
element to prevent accidental form submission and align with HTML best
practices, even if there is no form ancestor currently.

In `@src/context/AppContext.tsx`:
- Around line 571-574: In the ADD_GRADING_TASKS case, the current implementation
blindly appends incoming tasks to the existing gradingTasks array, which creates
duplicates when a task with the same id already exists in local state while the
cloud sync uses upsert semantics (one record per id). Instead of using
[...state.gradingTasks, ...action.payload], implement id-based upsert logic by
creating a map or filter-based approach that updates existing tasks by id or
adds new ones, ensuring local state maintains one record per task id and stays
consistent with the cloud sync behavior.

In `@src/pages/ActivityDashboardPage.tsx`:
- Around line 657-672: The label elements in the teacher assignment and due date
input fields are not programmatically associated with their corresponding
inputs, which impacts accessibility for screen readers. Add htmlFor attributes
to both labels (for the teacher name label with gradingTasks.teacher_label
translation and the due date label with gradingTasks.due_date_label translation)
and add matching id attributes to their corresponding input elements (the input
with assignTeacherName value and the input with assignDueDate value) to create
the proper association between labels and form controls.
- Around line 72-91: The handleAssignTasks function filters for ungraded
students but only excludes completed grades from studentRubrics, not
already-assigned pending tasks. When the modal reopens and assign is clicked
again, duplicate tasks are created with new IDs for the same student/rubric
pair. Expand the ungraded filter condition to also check for existing grading
tasks (pending assignments) for the same rubricId and studentId combination, in
addition to the existing studentRubrics check. This ensures that students with
both completed grades and already-pending assignments are excluded before
calling addGradingTasks.
- Around line 158-163: In the handleDragEnd function, the code derives the
destination kind from result.destination.droppableId but uses
result.source.index which refers to the item's position in the source section's
ordering. When a user drags across different sections (e.g., from rubric to
test), these indexes are misaligned, causing reorderDisplayOrder to corrupt the
operation. Extract the source kind from result.source.droppableId by applying
the same replace logic used for the destination, then compare both kinds to
ensure they match before proceeding with the reorder logic. If the kinds differ,
return early to reject the cross-section drop.

In `@src/pages/DocsPage.tsx`:
- Around line 80-81: The hardcoded English text in the `description` field and
similar user-visible strings in DocsPage.tsx (at the referenced line ranges)
must be moved to i18n keys. First, add appropriate translation keys to
src/locales/en.json for each hardcoded string. Then, ensure the useTranslation
hook is imported in DocsPage.tsx, and replace all hardcoded English strings with
t(key_name) calls referencing the new i18n keys. Apply this consistently across
all mentioned line ranges (267-276, 715-734, 869-889, 1127-1139) to maintain
multilingual behavior.

In `@src/pages/EssayListPage.tsx`:
- Around line 140-157: The button elements with className "btn btn-ghost
btn-icon btn-sm" containing the Edit2 icon (for navigation) and the Trash2 icon
(for handleDelete), as well as the additional button at line 185, are missing
explicit type attributes. Add type="button" to all three button elements to
prevent them from defaulting to type="submit" and triggering unintended form
submission behavior when used within form contexts.
- Around line 34-38: The handleDragEnd function needs an additional guard
condition to prevent unnecessary updateEssayGroup calls when an item is dropped
at the same position it was dragged from. After the existing check for missing
destinations, add a condition to return early if result.source.index equals
result.destination.index. This will prevent the reorderDisplayOrder function
from being called and avoid dispatching redundant updates when no actual
reordering has occurred.

In `@src/pages/GradeStudent.tsx`:
- Around line 1686-1693: The label with text coGrading.colleague_label is not
semantically linked to the input element that sets coGraderName, making it
inaccessible to screen readers. Add a unique id attribute to the input element
and then add a corresponding htmlFor attribute to the label element that matches
that id. This will ensure assistive technologies properly announce the field
name when the input is focused.
- Around line 1702-1705: The navigate call in GradeStudent.tsx uses free-text
coGraderName.trim() as the reviewerId parameter, which creates inconsistent
reviewer identities due to minor variations in how names/emails are entered.
Replace the coGraderName value with a stable, unique identifier for the reviewer
(such as a user ID or account identifier) that remains consistent regardless of
how the user's name or email is formatted. This ensures co-grade history and
moderation matching work correctly without fragmenting records based on text
variations.

In `@src/pages/LandingPage.tsx`:
- Around line 95-100: The feature card object containing the Department
Collaboration feature has hardcoded English text in the title and desc
properties, as well as a hardcoded hex color value. Extract the title
"Department Collaboration" and the description text to i18n translation keys
using the useTranslation hook pattern followed elsewhere in the file, replace
the hardcoded color '`#f59e0b`' with an appropriate global CSS custom property
instead of the hex value, and shorten the description to be concise and
user-focused rather than detailed and verbose.

In `@src/pages/ModerationQueuePage.tsx`:
- Around line 37-44: The label element displaying the threshold label text is
not programmatically associated with the input element of type number. Add a
unique id attribute to the number input element (the one with min={0},
step={0.5}, value={threshold}), and then add an htmlFor attribute to the label
element with the same id value to create the proper association. This will
ensure screen readers correctly announce the label when the input receives
focus.
- Around line 24-29: The resolveAcceptSecondMarker function is incomplete when
copying data from the second marker to the baseline. In the saveStudentRubric
call, you are spreading baseline properties and overriding entries and
overallComment from secondMarker, but you are missing the globalModifier
property. Add globalModifier from secondMarker to the object passed to
saveStudentRubric to ensure the accepted baseline grade remains consistent with
the accepted review, similar to how you are handling entries and overallComment.
- Around line 133-153: The three buttons in the ModerationQueuePage.tsx file are
missing explicit type attributes, which causes them to default to type="submit"
and can trigger accidental form submissions. Add type="button" to each of the
three buttons: the one with onClick navigating to the rubrics grading page, the
one calling resolveKeepBaseline, and the one calling resolveAcceptSecondMarker.
This ensures these action buttons do not trigger form submission behavior.

In `@src/pages/RubricList.tsx`:
- Around line 721-733: The clickable div element that navigates to
`/rubrics/${r.id}` on the onClick event lacks keyboard accessibility. Add the
`role="button"` attribute to semantically indicate this div is clickable, add
`tabIndex={0}` to make it keyboard focusable, and implement an `onKeyDown`
handler that triggers the same navigation logic when the Enter or Space key is
pressed. This ensures keyboard users can interact with the department-shared
rubric items the same way mouse users can.

In `@src/pages/TestListPage.tsx`:
- Around line 203-235: Add an explicit type="button" attribute to all button
elements in TestListPage.tsx that are currently missing it. This applies to the
action buttons including the duplicate button with the Copy icon, the edit
button with the Edit2 icon, and the delete button with the Trash2 icon, as well
as all other button elements mentioned in the file (lines 203, 214, 225, 275,
281, 288, 303, 309, 342, 439, 453). Without the explicit type attribute, buttons
default to type="submit" which can cause accidental form submission if these
controls are placed inside a form boundary.
- Around line 37-41: In the handleDragEnd function, add an early return check
after verifying the destination exists but before processing the reorder. Check
if the source index equals the destination index, and if they match, return
immediately since the item was dropped in its original position. This prevents
unnecessary iteration through the reordered list and avoids triggering
updateTest calls when the display order hasn't actually changed.

In `@src/store/storage.ts`:
- Around line 515-518: The validation for gradingTasks in the importFullBackup
function uses only isObjectArray() which checks if data is an object array but
does not validate the actual shape or required properties of each GradingTask
(such as required IDs and timestamps). Replace or enhance the isObjectArray
check with a more thorough validation function that verifies each item in the
array conforms to the GradingTask schema by checking for required fields. Only
call saveGradingTasks if all items pass this stricter shape validation,
otherwise log a warning and skip restoration as currently done.

In `@src/utils/activityDashboardAggregator.ts`:
- Line 23: The inline comment at line 23 in the activityDashboardAggregator.ts
file describes the implementation behavior (grouping essays by teacherKey and
using the first assignment's properties) rather than explaining the reasoning
behind this approach. Either remove the comment entirely since the code is
self-explanatory through its identifiers and logic, or rewrite it to explain the
business rationale or design decision for why this grouping and property
selection strategy is necessary.

In `@supabase/migrations/041_school_sharing.sql`:
- Around line 12-13: The RLS policies are using unsafe JSONB casting that can
cause query failures if malformed data exists. In both the rubrics_school_select
and comment_bank_school_select policies, replace the unsafe boolean cast
(data->>'sharedWithSchool')::boolean IS TRUE with a non-throwing JSONB predicate
that safely handles malformed values. Use the JSONB contains operator (@>) or a
CASE statement to check for the true value without throwing errors on invalid
data, ensuring the entire query doesn't fail just because one row has an invalid
sharedWithSchool value. Apply this same change at both locations mentioned in
the diff.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4b6ec5db-e424-4d47-a3e1-7285ba4e7f6d

📥 Commits

Reviewing files that changed from the base of the PR and between 4872e49 and 45bd3c0.

📒 Files selected for processing (43)
  • README.md
  • e2e/fixtures/supabase.fixture.ts
  • e2e/pages/ActivityDashboardPage.ts
  • e2e/pages/BasePage.ts
  • e2e/pages/EssayListPage.ts
  • e2e/pages/ModerationQueuePage.ts
  • e2e/pages/RubricListPage.ts
  • e2e/pages/TestListPage.ts
  • e2e/specs/23-co-grading-moderation.spec.ts
  • e2e/specs/24-department-sharing.spec.ts
  • e2e/specs/25-grading-task-assignment.spec.ts
  • e2e/specs/26-list-reordering.spec.ts
  • playwright.config.ts
  • src/App.tsx
  • src/components/Comments/CommentBankModal.tsx
  • src/components/Layout/Sidebar.tsx
  • src/components/__tests__/components.smoke.test.tsx
  • src/context/AppContext.tsx
  • src/locales/de.json
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/fr.json
  • src/locales/nl.json
  • src/pages/ActivityDashboardPage.tsx
  • src/pages/DocsPage.tsx
  • src/pages/EssayListPage.tsx
  • src/pages/GradeStudent.tsx
  • src/pages/LandingPage.tsx
  • src/pages/ModerationQueuePage.tsx
  • src/pages/RubricList.tsx
  • src/pages/TestListPage.tsx
  • src/pages/__tests__/pages-phase4.a11y.test.tsx
  • src/services/database/StorageSync.ts
  • src/services/database/SupabaseAdapter.ts
  • src/store/storage.ts
  • src/types/index.ts
  • src/utils/activityDashboardAggregator.ts
  • src/utils/coGradingModerationQueue.test.ts
  • src/utils/coGradingModerationQueue.ts
  • src/utils/displayOrder.test.ts
  • src/utils/displayOrder.ts
  • supabase/migrations/041_school_sharing.sql
  • supabase/migrations/042_grading_tasks.sql

Comment thread e2e/fixtures/supabase.fixture.ts Outdated
Comment thread e2e/fixtures/supabase.fixture.ts Outdated
Comment thread src/components/Comments/CommentBankModal.tsx
Comment thread src/context/AppContext.tsx
Comment thread src/pages/ActivityDashboardPage.tsx
Comment thread src/pages/TestListPage.tsx
Comment thread src/pages/TestListPage.tsx
Comment thread src/store/storage.ts
Comment thread src/utils/activityDashboardAggregator.ts Outdated
Comment thread supabase/migrations/041_school_sharing.sql Outdated
- e2e/fixtures/supabase.fixture.ts: validate the three school-linking write
  responses before returning the colleague magic link; fix no-empty-pattern/
  rules-of-hooks lint violations in the new fixture callbacks
- AppContext: ADD_GRADING_TASKS now upserts by id instead of blind-appending
- ActivityDashboardPage: exclude already-pending tasks (not just graded ones)
  when assigning; guard handleDragEnd against cross-section/no-op drops;
  associate modal labels with their inputs
- EssayListPage/TestListPage: guard handleDragEnd against no-op drops; add
  explicit type="button" to action buttons
- GradeStudent/ModerationQueuePage: associate labels with inputs; add
  type="button"; copy globalModifier when accepting a second marker's grade
- RubricList: keyboard support (role/tabIndex/onKeyDown) for the
  department-shared rubric list items
- CommentBankModal: add type="button" to the department-share toggle
- storage.ts: validate GradingTask shape (not just "is an object array") on
  backup restore
- 041_school_sharing.sql: use non-throwing jsonb equality instead of a
  boolean cast in the RLS policies
- activityDashboardAggregator.ts: drop a comment that narrated the code

Two flagged items left as-is with an explanation on the thread: the co-grader
identity stays free-text (matches the existing gradedBy heuristic, already
documented as a known ceiling; a stable-identity picker needs a teacher
directory that doesn't exist yet), and DocsPage/LandingPage hardcoded English
strings, which match the existing convention in those files everywhere else.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
NesiciCoding and others added 2 commits June 22, 2026 09:15
Playwright statically parses a fixture callback's first parameter to detect
declared dependencies, requiring it to be a destructuring pattern (even an
empty {}) — renaming it to a plain identifier (_fixtures) in the previous
commit broke fixture resolution at runtime ("First argument must use the
object destructuring pattern"), failing the whole supabase e2e project in CI.

Restored {} for colleagueEmail's first param and suppressed the no-empty-
pattern lint rule there instead, with a comment explaining why. The use->run
rename (second param) was unaffected and stays.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…rmat)

The colleaguePage fixture's user_settings only had {theme, language,
hasSeenTutorial} — missing defaultFormat (and a few other fields)
crashed the colleague's app on hydration whenever a page touching
rubric format rendered, which is exactly what 24-department-sharing.spec.ts
hits when the colleague navigates to /rubrics. createUserAndGetMagicLink's
own comment already warned about this; the new fixture just hadn't followed
it.

Extracted the settings payload into a shared FULL_TEST_SETTINGS constant
used by both createUserAndGetMagicLink and createColleagueAndGetMagicLink
so they can't drift apart again.

Verified via `playwright test --project=supabase --list` (parses cleanly)
and the full local chromium e2e suite (99/99); the supabase project itself
still can't run locally (no Docker), so CI is the next real signal.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/pages/RubricList.tsx (2)

721-741: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider using <button> instead of role="button" for native semantics.

The keyboard accessibility fix is correct. The static analysis tool suggests that <button> provides more reliable semantics for screen readers than a <div> with role="button". This is optional since the current implementation works correctly.

♿ Optional: native button approach
 {schoolShared.map((r) => (
-    <div
+    <button
         key={r.id}
-        role="button"
-        tabIndex={0}
         style={{
             padding: '12px 14px',
             background: 'var(--bg-elevated)',
             borderRadius: 10,
             border: '1px solid var(--border)',
             display: 'flex',
             alignItems: 'center',
             gap: 12,
             cursor: 'pointer',
+            width: '100%',
+            textAlign: 'left',
         }}
         onClick={() => navigate(`/rubrics/${r.id}`)}
-        onKeyDown={(e) => {
-            if (e.key === 'Enter' || e.key === ' ') {
-                e.preventDefault();
-                navigate(`/rubrics/${r.id}`);
-            }
-        }}
     >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/RubricList.tsx` around lines 721 - 741, Replace the `<div>` element
with `role="button"` with a native `<button>` element to improve screen reader
semantics. Remove the `role="button"` attribute since it is now redundant, and
keep all the styling applied to the button. The onKeyDown handler can be
simplified since native buttons automatically handle Enter and Space key presses
to trigger onClick, though keeping it does not hurt. Make sure the button still
receives the same onClick handler that calls navigate and retains the
tabIndex={0} if needed for focus management.

Source: Linters/SAST tools


266-266: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hardcoded English strings violate i18n guidelines.

Multiple user-visible strings in the topbar actions and button titles are hardcoded instead of using t():

  • Line 266: "Import from code"
  • Line 423: "Copy share code (for other teachers)"
  • Line 443: "Share preview with students (copy link)"

Similar issues exist in the code import modal (lines 901, 912–913, 922, 948, 955) and differentiate modal (line 876).

As per coding guidelines: "All user-visible strings must use the useTranslation hook and a key from src/locales/en.json. Never hardcode English text in JSX."

Also applies to: 423-423, 443-443

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/RubricList.tsx` at line 266, Replace all hardcoded English strings
with translation keys using the t() function from the useTranslation hook in
RubricList.tsx. Specifically, wrap the strings "Import from code", "Copy share
code (for other teachers)", and "Share preview with students (copy link)" with
t() calls, and also apply this to the hardcoded strings in the code import modal
and differentiate modal sections. For each hardcoded string, add a corresponding
translation key to the locales configuration file following the naming
convention used in the existing keys. Ensure the useTranslation hook is already
imported and available in the component before making these changes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/pages/RubricList.tsx`:
- Around line 721-741: Replace the `<div>` element with `role="button"` with a
native `<button>` element to improve screen reader semantics. Remove the
`role="button"` attribute since it is now redundant, and keep all the styling
applied to the button. The onKeyDown handler can be simplified since native
buttons automatically handle Enter and Space key presses to trigger onClick,
though keeping it does not hurt. Make sure the button still receives the same
onClick handler that calls navigate and retains the tabIndex={0} if needed for
focus management.
- Line 266: Replace all hardcoded English strings with translation keys using
the t() function from the useTranslation hook in RubricList.tsx. Specifically,
wrap the strings "Import from code", "Copy share code (for other teachers)", and
"Share preview with students (copy link)" with t() calls, and also apply this to
the hardcoded strings in the code import modal and differentiate modal sections.
For each hardcoded string, add a corresponding translation key to the locales
configuration file following the naming convention used in the existing keys.
Ensure the useTranslation hook is already imported and available in the
component before making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c085d818-96dc-4ad5-bc86-76ee195eae7d

📥 Commits

Reviewing files that changed from the base of the PR and between 45bd3c0 and 1581e18.

📒 Files selected for processing (12)
  • e2e/fixtures/supabase.fixture.ts
  • src/components/Comments/CommentBankModal.tsx
  • src/context/AppContext.tsx
  • src/pages/ActivityDashboardPage.tsx
  • src/pages/EssayListPage.tsx
  • src/pages/GradeStudent.tsx
  • src/pages/ModerationQueuePage.tsx
  • src/pages/RubricList.tsx
  • src/pages/TestListPage.tsx
  • src/store/storage.ts
  • src/utils/activityDashboardAggregator.ts
  • supabase/migrations/041_school_sharing.sql
💤 Files with no reviewable changes (1)
  • src/utils/activityDashboardAggregator.ts

NesiciCoding and others added 3 commits June 22, 2026 10:00
Array.prototype.find() returns undefined (not null) when nothing matches,
but the gate checked `linkedStudent !== null`. For any non-admin,
non-student teacher with no linked student record, this evaluated to
`undefined !== null && role !== 'admin'` = true, incorrectly routing them
into the "No student account linked to this email" dead end instead of
the normal teacher app.

This was dormant in every existing e2e-supabase test because they only
ever exercise an admin-role account (the first signup in a fresh DB
always becomes admin) or the same account on a second device — never a
genuine non-admin teacher session. The new colleaguePage fixture
(24-department-sharing.spec.ts) is the first to do that, which is how
this surfaced: the colleague got role='teacher' correctly, but then
hit this gate and got stuck on the student-portal screen instead of
/rubrics.

Fix: coalesce the find() result to null so the check means what it says.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… row

The previous policy joined two school_members rows (the colleague's own +
the rubric owner's) to confirm they're in the same school — mirroring
marketplace_listings_select's join shape. But that policy only ever reads
the QUERYING user's own school_members row; a second school_members row
for someone else (the owner, looked up by a non-admin colleague) is
exactly what school_members_select (migration 016) hides via RLS. So the
EXISTS always evaluated false for anyone but the owner themselves —
the colleague's fetchSchoolSharedRubrics() call kept returning [] even
though the rubric was genuinely shared (confirmed via the CI trace: the
owner's upsert with sharedWithSchool:true landed ~1s before the
colleague's read, ruling out a timing race).

Fixed by reading the owner's school_id off `profiles` instead, which any
teacher/admin can already read in full (profiles_read_users_and_admins,
migration 037) — so only the colleague's own school_members row needs to
be visible, which it always is.

Edited migration 041 in place rather than stacking a fix-up migration:
it has only ever run in this PR's own ephemeral CI sandboxes, never in a
shared/deployed environment.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… list

fetchRubrics() and fetchCommentBank() select with no owner filter,
relying entirely on RLS to scope results. That was already latent for
rubrics_shared_select (individual sharing, Phase 1) but never surfaced —
RLS additively grants visibility, so once a row is visible via ANY
policy it comes back from this same unscoped query and lands in
state.rubrics/state.commentBank with full owner-only edit/delete
controls, duplicated alongside the dedicated read-only "Shared with
me"/"Shared with your department" sections that already query it
separately.

Migration 041's new rubrics_school_select/comment_bank_school_select
policies made this newly reachable in a second way and is what the CI
trace caught: the colleague's "My Rubrics" grid showed a fully-editable
copy of the owner's shared rubric (Share/Duplicate/Edit/Delete buttons
and all) right next to the correct read-only entry in "Shared with your
department" — same row, two renderings, hence the e2e test's strict-mode
locator violation on "Department Shared Rubric".

Fixed by scoping both queries to owner_id = current user, matching what
fetchSharedRubrics()/fetchSchoolSharedRubrics()/
fetchSchoolSharedCommentBank() already do as separate, explicit calls.
Also tightened the e2e assertion to target the heading directly instead
of a bare text match, now that there's only one row to find either way.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@NesiciCoding NesiciCoding merged commit 1ffe5a5 into main Jun 22, 2026
8 checks passed
@NesiciCoding NesiciCoding deleted the claude/silly-shtern-3ca83d branch June 22, 2026 11:58
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