From 30c957364ddbf4473d4a9f9eead5cf8062f07a59 Mon Sep 17 00:00:00 2001 From: NesiciCoding Date: Thu, 13 Aug 2026 15:25:02 +0200 Subject: [PATCH 1/2] perf(render): migrate ModerationQueuePage to the selector store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Data slices (students, studentRubrics, rubrics, peerReviews, settings) now come from one useStoreSelector and grading actions from the stable useStoreActions context; only usePlatform remains for fetchSchoolMembers. The admin a11y suite previously let the page's render crash be swallowed by the router error boundary, so it now routes selectors/actions through its shared mocked app value and genuinely renders the page. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- src/pages/ModerationQueuePage.tsx | 17 +- .../__tests__/ModerationQueuePage.test.tsx | 5 + src/pages/__tests__/pages.admin.a11y.test.tsx | 288 +++++++++--------- 3 files changed, 161 insertions(+), 149 deletions(-) diff --git a/src/pages/ModerationQueuePage.tsx b/src/pages/ModerationQueuePage.tsx index bbc2a556..b3b170ab 100644 --- a/src/pages/ModerationQueuePage.tsx +++ b/src/pages/ModerationQueuePage.tsx @@ -4,7 +4,8 @@ import { UserCheck, AlertTriangle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import Topbar from '../components/Layout/Topbar'; import ReconcileModal from '../components/Modals/ReconcileModal'; -import { useAssessment, useAuthoring, useGrading, usePlatform, useSettings, useStudents } from '../context/AppContext'; +import { usePlatform } from '../context/AppContext'; +import { useStoreActions, useStoreSelector } from '../context/useStore'; import { useDbStatus } from '../hooks/useDbStatus'; import { buildReconciledEntries, @@ -17,12 +18,14 @@ import type { DbUser } from '../services/database'; export default function ModerationQueuePage() { const { t } = useTranslation(); const navigate = useNavigate(); - const { students } = useStudents(); - const { studentRubrics, saveStudentRubric } = useGrading(); - - const { rubrics } = useAuthoring(); - const { peerReviews, deletePeerReview } = useAssessment(); - const { settings } = useSettings(); + const { students, studentRubrics, rubrics, peerReviews, settings } = useStoreSelector((s) => ({ + students: s.students, + studentRubrics: s.studentRubrics, + rubrics: s.rubrics, + peerReviews: s.peerReviews, + settings: s.settings, + })); + const { saveStudentRubric, deletePeerReview } = useStoreActions(); const { fetchSchoolMembers } = usePlatform(); const dbStatus = useDbStatus(); diff --git a/src/pages/__tests__/ModerationQueuePage.test.tsx b/src/pages/__tests__/ModerationQueuePage.test.tsx index cbdfa510..4c285792 100644 --- a/src/pages/__tests__/ModerationQueuePage.test.tsx +++ b/src/pages/__tests__/ModerationQueuePage.test.tsx @@ -92,6 +92,11 @@ vi.mock('../../context/AppContext', () => ({ usePlatform: () => makeAppContextMock(), })); +vi.mock('../../context/useStore', () => ({ + useStoreSelector: (selector: (state: any) => any) => selector(makeAppContextMock()), + useStoreActions: () => makeAppContextMock(), +})); + vi.mock('../../hooks/useDbStatus', () => ({ useDbStatus: () => ({ isConnected: false }), })); diff --git a/src/pages/__tests__/pages.admin.a11y.test.tsx b/src/pages/__tests__/pages.admin.a11y.test.tsx index 015f9090..85b12d6d 100644 --- a/src/pages/__tests__/pages.admin.a11y.test.tsx +++ b/src/pages/__tests__/pages.admin.a11y.test.tsx @@ -49,148 +49,152 @@ const mockRubric: Rubric = { // renders — a fresh object/array each call makes any page whose effect depends on a // context array re-run that effect forever (setState → re-render → new array ref → // effect → …), which OOMs the worker. In the real app these references are stable. -vi.mock('../../context/AppContext', () => { - const base = { - rubrics: [mockRubric], - students: [mockStudent], - classes: [mockClass], - studentRubrics: [] as StudentRubric[], - selfAssessments: [], - speakingSessions: [], - gradeScales: [{ id: 'gs1', name: 'Default', ranges: [] }], - settings: mockSettings, - updateSettings: vi.fn(), - enterLocalMode: vi.fn(), - connectForOAuth: vi.fn(() => Promise.resolve(true)), - showMigrationPrompt: false, - dismissMigrationPrompt: vi.fn(), - getActiveGradeScale: vi.fn(), - // Phase 3/4 collections - tests: [], - studentTests: [], - questionBank: [], - exportTemplates: [], - flashcardDecks: [], - flashcardAssignments: [], - flashcardReviews: [], - essayAssignments: [], - essaySubmissions: [], - peerReviews: [], - analysisResults: [], - attachments: [], - essayTemplates: [], - messages: [], - newsFlashes: [], - newsFlashReads: [], - commentBank: [], - userTemplates: [], - gradingTasks: [], - standardMasteryTargets: [], - notificationDismissals: [], - dismissNotification: vi.fn(), - markMessageReadByTeacher: vi.fn(), - // Phase 3/4 actions - updateClass: vi.fn(), - addEssayAssignments: vi.fn(), - // RubricBuilder actions - addRubric: vi.fn(), - updateRubric: vi.fn(), - syncRubricSnapshot: vi.fn(), - fetchRubricVersions: vi.fn(() => Promise.resolve([])), - saveRubricVersion: vi.fn(), - restoreRubricVersion: vi.fn(), - addVocabularyItem: vi.fn(), - updateVocabularyItem: vi.fn(), - deleteVocabularyItem: vi.fn(), - deleteVocabularyItems: vi.fn(), - // GradeStudent actions - saveStudentRubric: vi.fn(), - saveAnalysisResult: vi.fn(), - addCommentBankItem: vi.fn(), - addAttachment: vi.fn(), - saveEssayAssignment: vi.fn(), - saveEssayTemplate: vi.fn(), - fetchEssaySubmissionsForStudent: vi.fn(() => Promise.resolve([])), - deleteEssaySubmission: vi.fn(), - getEssaySignedUrl: vi.fn(() => Promise.resolve(null)), - // Tests / Question Bank actions - addTest: vi.fn(), - updateTest: vi.fn(), - deleteTest: vi.fn(), - saveStudentTest: vi.fn(), - addSectionBankItem: vi.fn(), - addQuestionBankItems: vi.fn(), - updateQuestionBankItem: vi.fn(), - deleteQuestionBankItem: vi.fn(), - deleteQuestionBankItems: vi.fn(), - bulkUpdateQuestionBankItems: vi.fn(), - // Flashcard actions - addFlashcardDeck: vi.fn(), - updateFlashcardDeck: vi.fn(), - deleteFlashcardDeck: vi.fn(), - addFlashcardAssignments: vi.fn(), - // Essay actions - deleteEssayGroup: vi.fn(), - updateEssayGroup: vi.fn(), - addEssaySubmission: vi.fn(), - // News-flash actions - addNewsFlash: vi.fn(), - updateNewsFlash: vi.fn(), - deleteNewsFlash: vi.fn(), - // Comment-bank actions - updateCommentBankItem: vi.fn(), - deleteCommentBankItem: vi.fn(), - // Speaking / live-monitor actions - saveSpeakingSession: vi.fn(), - fetchTestAssignmentTeacherKeys: vi.fn(() => Promise.resolve([])), - setPlacementOverride: vi.fn(), - fetchEssayAssignmentByKey: vi.fn(() => Promise.resolve(null)), - // Dashboard / activity / rubric-list / students / attachments actions - deleteUserTemplate: vi.fn(), - sendMessage: vi.fn(), - notifyStudentMessage: vi.fn(), - addGradingTasks: vi.fn(), - deleteGradingTask: vi.fn(), - deleteRubric: vi.fn(), - createGroupStudentRubrics: vi.fn(), - addStudent: vi.fn(), - updateStudent: vi.fn(), - deleteStudent: vi.fn(), - addClass: vi.fn(), - deleteClass: vi.fn(), - mergeClasses: vi.fn(), - setStudentPassword: vi.fn(), - deleteAttachment: vi.fn(), - // Admin / settings / moderation / onboarding actions - fetchAllUsers: vi.fn(() => Promise.resolve([])), - updateUserRole: vi.fn(() => Promise.resolve()), - getCurrentDatabaseUserId: vi.fn(() => null), - addGradeScale: vi.fn(), - updateGradeScale: vi.fn(), - deleteGradeScale: vi.fn(), - addExportTemplate: vi.fn(), - deleteExportTemplate: vi.fn(), - importBackup: vi.fn(), - deleteStandardMasteryTarget: vi.fn(), - deletePeerReview: vi.fn(), - fetchSchoolMembers: vi.fn(() => Promise.resolve([])), - createSchool: vi.fn(() => Promise.resolve()), - joinSchool: vi.fn(() => Promise.resolve()), - signOutFromDatabase: vi.fn(() => Promise.resolve()), - }; - return { - useRoster: () => base, - useStudents: () => base, - useClasses: () => base, - useGrading: () => base, - useAuthoring: () => base, - useAssessment: () => base, - useEssays: () => base, - useFlashcards: () => base, - useSettings: () => base, - usePlatform: () => base, - }; -}); +const base = { + rubrics: [mockRubric], + students: [mockStudent], + classes: [mockClass], + studentRubrics: [] as StudentRubric[], + selfAssessments: [], + speakingSessions: [], + gradeScales: [{ id: 'gs1', name: 'Default', ranges: [] }], + settings: mockSettings, + updateSettings: vi.fn(), + enterLocalMode: vi.fn(), + connectForOAuth: vi.fn(() => Promise.resolve(true)), + showMigrationPrompt: false, + dismissMigrationPrompt: vi.fn(), + getActiveGradeScale: vi.fn(), + // Phase 3/4 collections + tests: [], + studentTests: [], + questionBank: [], + exportTemplates: [], + flashcardDecks: [], + flashcardAssignments: [], + flashcardReviews: [], + essayAssignments: [], + essaySubmissions: [], + peerReviews: [], + analysisResults: [], + attachments: [], + essayTemplates: [], + messages: [], + newsFlashes: [], + newsFlashReads: [], + commentBank: [], + userTemplates: [], + gradingTasks: [], + standardMasteryTargets: [], + notificationDismissals: [], + dismissNotification: vi.fn(), + markMessageReadByTeacher: vi.fn(), + // Phase 3/4 actions + updateClass: vi.fn(), + addEssayAssignments: vi.fn(), + // RubricBuilder actions + addRubric: vi.fn(), + updateRubric: vi.fn(), + syncRubricSnapshot: vi.fn(), + fetchRubricVersions: vi.fn(() => Promise.resolve([])), + saveRubricVersion: vi.fn(), + restoreRubricVersion: vi.fn(), + addVocabularyItem: vi.fn(), + updateVocabularyItem: vi.fn(), + deleteVocabularyItem: vi.fn(), + deleteVocabularyItems: vi.fn(), + // GradeStudent actions + saveStudentRubric: vi.fn(), + saveAnalysisResult: vi.fn(), + addCommentBankItem: vi.fn(), + addAttachment: vi.fn(), + saveEssayAssignment: vi.fn(), + saveEssayTemplate: vi.fn(), + fetchEssaySubmissionsForStudent: vi.fn(() => Promise.resolve([])), + deleteEssaySubmission: vi.fn(), + getEssaySignedUrl: vi.fn(() => Promise.resolve(null)), + // Tests / Question Bank actions + addTest: vi.fn(), + updateTest: vi.fn(), + deleteTest: vi.fn(), + saveStudentTest: vi.fn(), + addSectionBankItem: vi.fn(), + addQuestionBankItems: vi.fn(), + updateQuestionBankItem: vi.fn(), + deleteQuestionBankItem: vi.fn(), + deleteQuestionBankItems: vi.fn(), + bulkUpdateQuestionBankItems: vi.fn(), + // Flashcard actions + addFlashcardDeck: vi.fn(), + updateFlashcardDeck: vi.fn(), + deleteFlashcardDeck: vi.fn(), + addFlashcardAssignments: vi.fn(), + // Essay actions + deleteEssayGroup: vi.fn(), + updateEssayGroup: vi.fn(), + addEssaySubmission: vi.fn(), + // News-flash actions + addNewsFlash: vi.fn(), + updateNewsFlash: vi.fn(), + deleteNewsFlash: vi.fn(), + // Comment-bank actions + updateCommentBankItem: vi.fn(), + deleteCommentBankItem: vi.fn(), + // Speaking / live-monitor actions + saveSpeakingSession: vi.fn(), + fetchTestAssignmentTeacherKeys: vi.fn(() => Promise.resolve([])), + setPlacementOverride: vi.fn(), + fetchEssayAssignmentByKey: vi.fn(() => Promise.resolve(null)), + // Dashboard / activity / rubric-list / students / attachments actions + deleteUserTemplate: vi.fn(), + sendMessage: vi.fn(), + notifyStudentMessage: vi.fn(), + addGradingTasks: vi.fn(), + deleteGradingTask: vi.fn(), + deleteRubric: vi.fn(), + createGroupStudentRubrics: vi.fn(), + addStudent: vi.fn(), + updateStudent: vi.fn(), + deleteStudent: vi.fn(), + addClass: vi.fn(), + deleteClass: vi.fn(), + mergeClasses: vi.fn(), + setStudentPassword: vi.fn(), + deleteAttachment: vi.fn(), + // Admin / settings / moderation / onboarding actions + fetchAllUsers: vi.fn(() => Promise.resolve([])), + updateUserRole: vi.fn(() => Promise.resolve()), + getCurrentDatabaseUserId: vi.fn(() => null), + addGradeScale: vi.fn(), + updateGradeScale: vi.fn(), + deleteGradeScale: vi.fn(), + addExportTemplate: vi.fn(), + deleteExportTemplate: vi.fn(), + importBackup: vi.fn(), + deleteStandardMasteryTarget: vi.fn(), + deletePeerReview: vi.fn(), + fetchSchoolMembers: vi.fn(() => Promise.resolve([])), + createSchool: vi.fn(() => Promise.resolve()), + joinSchool: vi.fn(() => Promise.resolve()), + signOutFromDatabase: vi.fn(() => Promise.resolve()), +}; + +vi.mock('../../context/AppContext', () => ({ + useRoster: () => base, + useStudents: () => base, + useClasses: () => base, + useGrading: () => base, + useAuthoring: () => base, + useAssessment: () => base, + useEssays: () => base, + useFlashcards: () => base, + useSettings: () => base, + usePlatform: () => base, +})); + +vi.mock('../../context/useStore', () => ({ + useStoreSelector: (selector: (state: any) => any) => selector(base), + useStoreActions: () => base, +})); vi.mock('../../services/database', () => ({ loadSupabaseConfig: vi.fn(() => null), From 18dc76ede968617855b7c3e8b37376920cfd5090 Mon Sep 17 00:00:00 2001 From: NesiciCoding Date: Tue, 18 Aug 2026 08:59:47 +0200 Subject: [PATCH 2/2] fix(render): exclude archived students and deleted rubrics on ModerationQueuePage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The roster domain hooks filtered soft-deleted rows; the selector-store migration read raw slices. Restore the active-student / active-rubric filtering so archived students and deleted student rubrics don't surface in the moderation queue. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- src/pages/ModerationQueuePage.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/ModerationQueuePage.tsx b/src/pages/ModerationQueuePage.tsx index b3b170ab..7cab8c8b 100644 --- a/src/pages/ModerationQueuePage.tsx +++ b/src/pages/ModerationQueuePage.tsx @@ -18,13 +18,22 @@ import type { DbUser } from '../services/database'; export default function ModerationQueuePage() { const { t } = useTranslation(); const navigate = useNavigate(); - const { students, studentRubrics, rubrics, peerReviews, settings } = useStoreSelector((s) => ({ + const { + students: allStudents, + studentRubrics: allStudentRubrics, + rubrics, + peerReviews, + settings, + } = useStoreSelector((s) => ({ students: s.students, studentRubrics: s.studentRubrics, rubrics: s.rubrics, peerReviews: s.peerReviews, settings: s.settings, })); + // The roster domain hooks filtered soft-deleted rows; keep that behavior here. + const students = useMemo(() => allStudents.filter((s) => !s.archivedAt), [allStudents]); + const studentRubrics = useMemo(() => allStudentRubrics.filter((sr) => !sr.deletedAt), [allStudentRubrics]); const { saveStudentRubric, deletePeerReview } = useStoreActions(); const { fetchSchoolMembers } = usePlatform();