import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { getSikshyaApi, getWpApi, SIKSHYA_ENDPOINTS } from '../api'; import { EmbeddableShell } from '../components/shared/EmbeddableShell'; import { GatedFeatureWorkspace } from '../components/GatedFeatureWorkspace'; import { ApiErrorPanel } from '../components/shared/ApiErrorPanel'; import { BulkActionsBar } from '../components/shared/list/BulkActionsBar'; import { ListPanel } from '../components/shared/list/ListPanel'; import { ListSearchToolbar } from '../components/shared/list/ListSearchToolbar'; import { ButtonPrimary, ButtonSecondary } from '../components/shared/buttons'; import { DataTable, type Column } from '../components/shared/DataTable'; import { DataTableSkeleton } from '../components/shared/Skeleton'; import { Modal } from '../components/shared/Modal'; import { ListEmptyState } from '../components/shared/list/ListEmptyState'; import { ListPaginationBar } from '../components/shared/list/ListPaginationBar'; import { useSikshyaDialog } from '../components/shared/SikshyaDialogContext'; import { MultiCoursePicker } from '../components/shared/MultiCoursePicker'; import { SingleCoursePicker } from '../components/shared/SingleCoursePicker'; import { CourseFilterSelect } from '../components/shared/CourseFilterSelect'; import { FieldHint } from '../components/shared/FieldHint'; import { PrerequisiteLockDetailPopover } from '../components/shared/PrerequisiteLockDetailPopover'; import { RowActionsMenu, type RowActionItem } from '../components/shared/list/RowActionsMenu'; import { useAsyncData } from '../hooks/useAsyncData'; import { useDebouncedValue } from '../hooks/useDebouncedValue'; import { useAddonEnabled } from '../hooks/useAddons'; import { isFeatureEnabled, resolveGatedWorkspaceMode } from '../lib/licensing'; import type { SikshyaReactConfig } from '../types'; import { TopRightToast, useTopRightToast } from '../components/shared/TopRightToast'; import { __ } from '../lib/i18n'; type LessonRow = { id: number; title: string; status: string }; type CoursePrereqResp = { ok?: boolean; course_id?: number; prerequisite_course_ids?: number[]; prerequisite_courses?: LessonRow[]; }; type LessonPrereqResp = { ok?: boolean; lesson_id?: number; prerequisite_lesson_ids?: number[]; prerequisite_lessons?: LessonRow[]; }; type LessonsResp = { ok?: boolean; course_id?: number; lessons?: LessonRow[] }; type PrereqCourseRow = { course_id: number; course_title: string; required_courses_count: number; lesson_locks_count: number; has_any_rules?: boolean; }; type PrereqCourseListResp = { ok?: boolean; courses?: PrereqCourseRow[]; total?: number; page?: number; per_page?: number; total_pages?: number; }; export function PrerequisitesPage(props: { config: SikshyaReactConfig; title: string; embedded?: boolean }) { const { config, title, embedded } = props; const dialog = useSikshyaDialog(); const featureOk = isFeatureEnabled(config, 'prerequisites'); const addon = useAddonEnabled('prerequisites'); const mode = resolveGatedWorkspaceMode(featureOk, addon.enabled, addon.loading); const enabled = mode === 'full'; const [search, setSearch] = useState(''); const debouncedSearch = useDebouncedValue(search, 320); const [page, setPage] = useState(1); const perPage = 20; const [enrollmentModalOpen, setEnrollmentModalOpen] = useState(false); const [lessonModalOpen, setLessonModalOpen] = useState(false); const [addModalOpen, setAddModalOpen] = useState(false); const [addCourseId, setAddCourseId] = useState(0); const [addLockType, setAddLockType] = useState<'enrollment' | 'lessons'>('enrollment'); const [addStarting, setAddStarting] = useState(false); const [activeCourseRow, setActiveCourseRow] = useState(null); const [modalCoursePrereqs, setModalCoursePrereqs] = useState([]); const [modalCoursePrereqLabels, setModalCoursePrereqLabels] = useState>({}); const [modalSavingEnrollment, setModalSavingEnrollment] = useState(false); const [modalLessonId, setModalLessonId] = useState(0); const [modalLessonPrereqs, setModalLessonPrereqs] = useState([]); const [modalSavingLesson, setModalSavingLesson] = useState(false); const [clearingEnrollmentId, setClearingEnrollmentId] = useState(null); const [clearingLessonsCourseId, setClearingLessonsCourseId] = useState(null); const [clearingAllLocksCourseId, setClearingAllLocksCourseId] = useState(null); /** When true, list every course (including none with locks). Default false = only courses that still have locks, so a full delete removes the row. */ const [showAllCourses, setShowAllCourses] = useState(false); const initialCourse = config.query?.course_id ? Number(config.query.course_id) : 0; const [filterCourseId, setFilterCourseId] = useState(Number.isFinite(initialCourse) ? initialCourse : 0); const [listOrderBy, setListOrderBy] = useState<'modified' | 'title' | 'id'>('modified'); const [listOrder, setListOrder] = useState<'asc' | 'desc'>('desc'); const [selectedCourseIds, setSelectedCourseIds] = useState>(() => new Set()); const [bulkActionValue, setBulkActionValue] = useState(''); const [bulkBusy, setBulkBusy] = useState(false); const [bulkError, setBulkError] = useState(null); const headerSelectRef = useRef(null); useEffect(() => setPage(1), [debouncedSearch, filterCourseId, listOrderBy, listOrder, showAllCourses]); useEffect(() => { setSelectedCourseIds(new Set()); setBulkActionValue(''); }, [page, filterCourseId, debouncedSearch, listOrderBy, listOrder, showAllCourses]); const listLoader = useCallback(async () => { if (!enabled) { return { ok: true, courses: [] as PrereqCourseRow[], total: 0, page: 1, per_page: perPage, total_pages: 0 } as PrereqCourseListResp; } return getSikshyaApi().get( SIKSHYA_ENDPOINTS.pro.prerequisiteCourses({ search: filterCourseId > 0 ? undefined : debouncedSearch.trim() || undefined, page: filterCourseId > 0 ? 1 : page, per_page: perPage, orderby: listOrderBy, order: listOrder, ...(filterCourseId > 0 ? { course_id: filterCourseId } : {}), ...(filterCourseId <= 0 && !showAllCourses ? { only_with_locks: true } : {}), }) ); }, [enabled, debouncedSearch, page, filterCourseId, perPage, listOrderBy, listOrder, showAllCourses]); const listQ = useAsyncData(listLoader, [ enabled, debouncedSearch, page, filterCourseId, listOrderBy, listOrder, perPage, showAllCourses, ]); const rows = listQ.data?.courses ?? []; const toast = useTopRightToast(2600); const prereqBulkOptions = useMemo( () => [ { value: 'clear_all', label: 'Delete all access locks' }, { value: 'clear_enrollment', label: 'Delete enrollment lock only' }, { value: 'clear_lesson_locks', label: 'Delete lesson locks only' }, ], [] ); const visibleCourseIds = useMemo(() => rows.map((r) => r.course_id), [rows]); const checkedOnPage = useMemo( () => visibleCourseIds.filter((id) => selectedCourseIds.has(id)).length, [visibleCourseIds, selectedCourseIds] ); const allVisibleSelected = visibleCourseIds.length > 0 && checkedOnPage === visibleCourseIds.length; useLayoutEffect(() => { const el = headerSelectRef.current; if (!el) return; el.indeterminate = checkedOnPage > 0 && checkedOnPage < visibleCourseIds.length; }, [checkedOnPage, visibleCourseIds.length]); const toggleSelectAllCourses = useCallback(() => { setSelectedCourseIds((prev) => { const next = new Set(prev); const allSel = visibleCourseIds.length > 0 && visibleCourseIds.every((id) => next.has(id)); if (allSel) { visibleCourseIds.forEach((id) => next.delete(id)); } else { visibleCourseIds.forEach((id) => next.add(id)); } return next; }); }, [visibleCourseIds]); const toggleCourseSelected = useCallback((courseId: number) => { setSelectedCourseIds((prev) => { const next = new Set(prev); if (next.has(courseId)) { next.delete(courseId); } else { next.add(courseId); } return next; }); }, []); const clearAllAccessLocksForCourseId = useCallback(async (courseId: number) => { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.coursePrerequisites(courseId), { prerequisite_course_ids: [], }); const data = await getSikshyaApi().get<{ locked_lessons?: { lesson_id: number }[] }>( SIKSHYA_ENDPOINTS.pro.courseLessonPrerequisiteSummary(courseId) ); const lids = (data.locked_lessons ?? []).map((x) => x.lesson_id).filter((lid) => lid > 0); await Promise.all( lids.map((lid) => getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.lessonPrerequisites(lid), { prerequisite_lesson_ids: [] }) ) ); }, []); const clearAllAccessLocksForRow = useCallback( async (r: PrereqCourseRow) => { const hasAny = (r.required_courses_count || 0) > 0 || (r.lesson_locks_count || 0) > 0; if (!hasAny) return; const ok = await dialog.confirm({ title: __('Delete all access locks?', 'sikshya'), message: __('Removes enrollment prerequisites and every lesson lock in this course. The course stays in your catalog; it disappears from this list until you add locks again.', 'sikshya'), confirmLabel: __('Delete all locks', 'sikshya'), variant: 'danger', }); if (!ok) return; setClearingAllLocksCourseId(r.course_id); toast.clear(); try { await clearAllAccessLocksForCourseId(r.course_id); toast.success(__('Removed', 'sikshya'), 'All access locks removed for this course.'); void listQ.refetch(); } catch (e) { toast.error(__('Request failed', 'sikshya'), e instanceof Error ? e.message : 'Request failed'); } finally { setClearingAllLocksCourseId(null); } }, [clearAllAccessLocksForCourseId, dialog, listQ] ); const clearEnrollmentLocks = useCallback( async (r: PrereqCourseRow) => { if ((r.required_courses_count || 0) <= 0) return; const ok = await dialog.confirm({ title: __('Clear enrollment lock?', 'sikshya'), message: __('Learners will be able to enroll in this course without completing other courses first. This cannot be undone from here except by setting new requirements.', 'sikshya'), confirmLabel: __('Clear lock', 'sikshya'), variant: 'danger', }); if (!ok) return; setClearingEnrollmentId(r.course_id); toast.clear(); try { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.coursePrerequisites(r.course_id), { prerequisite_course_ids: [], }); toast.success(__('Removed', 'sikshya'), 'Enrollment lock cleared.'); void listQ.refetch(); } catch (e) { toast.error(__('Request failed', 'sikshya'), e instanceof Error ? e.message : 'Request failed'); } finally { setClearingEnrollmentId(null); } }, [dialog, listQ] ); const clearAllLessonLocks = useCallback( async (r: PrereqCourseRow) => { if ((r.lesson_locks_count || 0) <= 0) return; const ok = await dialog.confirm({ title: __('Remove all lesson locks?', 'sikshya'), message: __('Every lesson in this course that currently requires other lessons first will be reset. Learners follow curriculum order only until you set locks again.', 'sikshya'), confirmLabel: __('Remove all', 'sikshya'), variant: 'danger', }); if (!ok) return; setClearingLessonsCourseId(r.course_id); toast.clear(); try { const data = await getSikshyaApi().get<{ locked_lessons?: { lesson_id: number }[] }>( SIKSHYA_ENDPOINTS.pro.courseLessonPrerequisiteSummary(r.course_id) ); const lids = (data.locked_lessons ?? []).map((x) => x.lesson_id).filter((id) => id > 0); await Promise.all( lids.map((lid) => getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.lessonPrerequisites(lid), { prerequisite_lesson_ids: [] }) ) ); toast.success(__('Removed', 'sikshya'), 'Lesson locks removed.'); void listQ.refetch(); } catch (e) { toast.error(__('Request failed', 'sikshya'), e instanceof Error ? e.message : 'Request failed'); } finally { setClearingLessonsCourseId(null); } }, [dialog, listQ] ); const onPrereqBulkApply = useCallback(async () => { if (!enabled || selectedCourseIds.size === 0 || bulkActionValue === '') { return; } const ids = [...selectedCourseIds]; const n = ids.length; setBulkError(null); toast.clear(); const run = async () => { if (bulkActionValue === 'clear_all') { await Promise.all(ids.map((courseId) => clearAllAccessLocksForCourseId(courseId))); toast.success(__('Removed', 'sikshya'), `Removed all access locks for ${n} course(s).`); return; } if (bulkActionValue === 'clear_enrollment') { await Promise.all( ids.map((courseId) => getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.coursePrerequisites(courseId), { prerequisite_course_ids: [], }) ) ); toast.success(__('Updated', 'sikshya'), `Updated ${n} course(s).`); return; } if (bulkActionValue === 'clear_lesson_locks') { for (const courseId of ids) { const data = await getSikshyaApi().get<{ locked_lessons?: { lesson_id: number }[] }>( SIKSHYA_ENDPOINTS.pro.courseLessonPrerequisiteSummary(courseId) ); const lids = (data.locked_lessons ?? []).map((x) => x.lesson_id).filter((lid) => lid > 0); await Promise.all( lids.map((lid) => getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.lessonPrerequisites(lid), { prerequisite_lesson_ids: [] }) ) ); } toast.success(__('Removed', 'sikshya'), `Lesson locks cleared for ${n} course(s).`); } }; if (bulkActionValue === 'clear_all') { const ok = await dialog.confirm({ title: `Delete all access locks for ${n} course(s)?`, message: __('Clears enrollment prerequisites and every lesson lock for each selected course. Rows disappear from this list until you add locks again (unless you turn on “Show courses without locks”).', 'sikshya'), confirmLabel: __('Delete all locks', 'sikshya'), variant: 'danger', }); if (!ok) return; } else if (bulkActionValue === 'clear_enrollment') { const ok = await dialog.confirm({ title: `Delete enrollment lock for ${n} course(s)?`, message: __('Learners will be able to enroll in those courses without completing other courses first, where a lock existed.', 'sikshya'), confirmLabel: __('Delete locks', 'sikshya'), variant: 'danger', }); if (!ok) return; } else if (bulkActionValue === 'clear_lesson_locks') { const ok = await dialog.confirm({ title: `Delete all lesson locks for ${n} course(s)?`, message: __('Every lesson in those courses that currently requires other lessons first will be reset. This can take a moment.', 'sikshya'), confirmLabel: __('Delete all lesson locks', 'sikshya'), variant: 'danger', }); if (!ok) return; } else { return; } setBulkBusy(true); try { await run(); setSelectedCourseIds(new Set()); setBulkActionValue(''); void listQ.refetch(); } catch (e) { setBulkError(e); } finally { setBulkBusy(false); } }, [bulkActionValue, clearAllAccessLocksForCourseId, dialog, enabled, listQ, selectedCourseIds]); const columns: Column[] = useMemo( () => [ { id: '_bulk_select', header: ( ), alwaysVisible: true, headerClassName: 'w-12', cellClassName: 'w-12', render: (r) => ( toggleCourseSelected(r.course_id)} /> ), }, { id: 'course', header: 'Course', render: (r) => (
{r.course_title}
#{r.course_id}
), }, { id: 'enrollment', header: 'Enrollment lock', cellClassName: 'whitespace-nowrap text-slate-700 dark:text-slate-200', render: (r) => ( ), }, { id: 'lessons', header: 'Lesson locks', cellClassName: 'whitespace-nowrap text-slate-700 dark:text-slate-200', render: (r) => ( ), }, { id: 'actions', header: '', headerClassName: 'w-[1%]', cellClassName: 'text-right align-middle', render: (r) => { const enrollmentBusy = clearingEnrollmentId === r.course_id; const lessonsBusy = clearingLessonsCourseId === r.course_id; const allBusy = clearingAllLocksCourseId === r.course_id; const hasEnrollmentLock = (r.required_courses_count || 0) > 0; const hasLessonLocks = (r.lesson_locks_count || 0) > 0; const hasAnyLock = hasEnrollmentLock || hasLessonLocks; const items: RowActionItem[] = [ { key: 'edit-enrollment', label: 'Edit enrollment lock', onClick: () => { setActiveCourseRow(r); setEnrollmentModalOpen(true); }, }, { key: 'edit-lessons', label: 'Edit lesson locks', onClick: () => { setActiveCourseRow(r); setLessonModalOpen(true); }, }, { key: 'delete-all-access', label: allBusy ? __('Deleting all locks…', 'sikshya') : __('Delete all access locks', 'sikshya'), danger: true, disabled: !hasAnyLock || allBusy || enrollmentBusy || lessonsBusy, onClick: () => void clearAllAccessLocksForRow(r), }, { key: 'delete-enrollment', label: enrollmentBusy ? __('Deleting enrollment lock…', 'sikshya') : __('Delete enrollment lock only', 'sikshya'), danger: true, disabled: !hasEnrollmentLock || enrollmentBusy || allBusy, onClick: () => void clearEnrollmentLocks(r), }, { key: 'delete-lesson-locks', label: lessonsBusy ? __('Deleting lesson locks…', 'sikshya') : __('Delete lesson locks only', 'sikshya'), danger: true, disabled: !hasLessonLocks || lessonsBusy || allBusy, onClick: () => void clearAllLessonLocks(r), }, ]; return (
e.stopPropagation()}>
); }, }, ], [ enabled, clearingAllLocksCourseId, clearingEnrollmentId, clearingLessonsCourseId, clearAllAccessLocksForRow, clearAllLessonLocks, clearEnrollmentLocks, allVisibleSelected, selectedCourseIds, toggleSelectAllCourses, toggleCourseSelected, ] ); const modalLessonsLoader = useCallback(async () => { const cid = activeCourseRow?.course_id ?? 0; if (!enabled || !lessonModalOpen || cid <= 0) { return { ok: true, course_id: cid, lessons: [] } as LessonsResp; } return getSikshyaApi().get(SIKSHYA_ENDPOINTS.pro.courseLessons(cid)); }, [enabled, lessonModalOpen, activeCourseRow?.course_id]); const modalLessonsQ = useAsyncData(modalLessonsLoader, [enabled, lessonModalOpen, activeCourseRow?.course_id]); const modalLessons = modalLessonsQ.data?.lessons ?? []; const modalLessonLoader = useCallback(async () => { if (!enabled || !lessonModalOpen || modalLessonId <= 0) { return { ok: true, lesson_id: modalLessonId, prerequisite_lesson_ids: [], prerequisite_lessons: [] } as LessonPrereqResp; } return getSikshyaApi().get(SIKSHYA_ENDPOINTS.pro.lessonPrerequisites(modalLessonId)); }, [enabled, lessonModalOpen, modalLessonId]); const modalLessonQ = useAsyncData(modalLessonLoader, [enabled, lessonModalOpen, modalLessonId]); useEffect(() => { if (!enrollmentModalOpen) return; const cid = activeCourseRow?.course_id ?? 0; if (cid <= 0) return; setModalSavingEnrollment(false); setModalCoursePrereqs([]); setModalCoursePrereqLabels({}); void (async () => { try { const data = await getSikshyaApi().get(SIKSHYA_ENDPOINTS.pro.coursePrerequisites(cid)); setModalCoursePrereqs(data.prerequisite_course_ids ?? []); const labels: Record = {}; for (const r of data.prerequisite_courses ?? []) labels[r.id] = r.title; setModalCoursePrereqLabels(labels); } catch { // Surface via toast on save; keep empty state. } })(); }, [enrollmentModalOpen, activeCourseRow?.course_id]); useEffect(() => { const data = modalLessonQ.data; if (!data) return; setModalLessonPrereqs(data.prerequisite_lesson_ids ?? []); }, [modalLessonQ.data]); const saveEnrollmentModal = async () => { const cid = activeCourseRow?.course_id ?? 0; if (cid <= 0) return; setModalSavingEnrollment(true); toast.clear(); try { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.coursePrerequisites(cid), { prerequisite_course_ids: modalCoursePrereqs, }); toast.success(__('Saved', 'sikshya'), 'Enrollment lock saved.'); setEnrollmentModalOpen(false); setActiveCourseRow(null); void listQ.refetch(); } catch (e) { toast.error(__('Save failed', 'sikshya'), e instanceof Error ? e.message : 'Save failed'); } finally { setModalSavingEnrollment(false); } }; const saveLessonModal = async () => { if (modalLessonId <= 0) { toast.error(__('Missing lesson', 'sikshya'), 'Pick a lesson first.'); return; } setModalSavingLesson(true); toast.clear(); try { await getSikshyaApi().post(SIKSHYA_ENDPOINTS.pro.lessonPrerequisites(modalLessonId), { prerequisite_lesson_ids: modalLessonPrereqs, }); toast.success(__('Saved', 'sikshya'), 'Lesson lock saved.'); setLessonModalOpen(false); setActiveCourseRow(null); setModalLessonId(0); void listQ.refetch(); } catch (e) { toast.error(__('Save failed', 'sikshya'), e instanceof Error ? e.message : 'Save failed'); } finally { setModalSavingLesson(false); } }; const startAddPrerequisite = useCallback(async () => { if (addCourseId <= 0 || addStarting) { return; } setAddStarting(true); toast.clear(); try { let courseTitle = rows.find((row) => row.course_id === addCourseId)?.course_title || ''; if (!courseTitle) { const course = await getWpApi().get<{ id?: number; title?: { rendered?: string } }>( `/sik_course/${encodeURIComponent(String(addCourseId))}?context=edit&_fields=id,title` ); courseTitle = course?.title?.rendered ? String(course.title.rendered).replace(/<[^>]*>/g, '').trim() : ''; } const nextRow: PrereqCourseRow = { course_id: addCourseId, course_title: courseTitle || `Course #${addCourseId}`, required_courses_count: 0, lesson_locks_count: 0, has_any_rules: false, }; setActiveCourseRow(nextRow); setAddModalOpen(false); if (addLockType === 'lessons') { setLessonModalOpen(true); } else { setEnrollmentModalOpen(true); } } catch (e) { toast.error(__('Could not load', 'sikshya'), e instanceof Error ? e.message : 'Could not load the selected course.'); } finally { setAddStarting(false); } }, [addCourseId, addLockType, addStarting, rows]); // Auto-dismiss handled by shared toast. return ( addon.enable()} addonError={addon.error} >
(addStarting ? null : setAddModalOpen(false))} size="lg" footer={
setAddModalOpen(false)} disabled={addStarting}> Cancel void startAddPrerequisite()} disabled={addCourseId <= 0 || addStarting}> {addStarting ? __('Opening…', 'sikshya') : __('Continue', 'sikshya')}
} >

{__('Lock type', 'sikshya')}

(modalSavingEnrollment ? null : (setEnrollmentModalOpen(false), setActiveCourseRow(null)))} size="lg" footer={
void saveEnrollmentModal()} disabled={modalSavingEnrollment}> {modalSavingEnrollment ? __('Saving…', 'sikshya') : __('Save', 'sikshya')}
} >
(modalSavingLesson ? null : (setLessonModalOpen(false), setActiveCourseRow(null), setModalLessonId(0)))} size="xl" footer={
void saveLessonModal()} disabled={modalSavingLesson}> {modalSavingLesson ? __('Saving…', 'sikshya') : __('Save', 'sikshya')}
} >
{__('Select the lesson you want to lock.', 'sikshya')}
{modalLessonId > 0 ? ( {modalLessonPrereqs.length} selected ) : null}
{modalLessonId <= 0 ? (
{__('Pick a lesson to edit prerequisites.', 'sikshya')}
) : modalLessonQ.loading ? (
{__('Loading…', 'sikshya')}
) : ( <> {modalLessons .filter((l) => { if (l.id === modalLessonId) return false; const idx = modalLessons.findIndex((x) => x.id === l.id); const selfIdx = modalLessons.findIndex((x) => x.id === modalLessonId); // Backend rejects forward dependencies; keep UI aligned by showing only earlier lessons. if (idx >= 0 && selfIdx >= 0) return idx < selfIdx; return true; }) .map((l) => { const checked = modalLessonPrereqs.includes(l.id); return ( ); })} {modalLessons.length <= 1 ? (
{__('This course has no other lessons yet.', 'sikshya')}
) : null} )}
{__('Only lessons earlier in the curriculum can be prerequisites.', 'sikshya')}

{__('Courses with access locks', 'sikshya')}

Each row is a course that still has an enrollment lock and/or lesson locks. Deleting all locks removes the row from this list (the course is not deleted from the site). Use “Show courses without locks” to browse the full catalog.

setAddModalOpen(true)} disabled={!enabled}> Add prerequisites
0 ? __('Clear course filter (right) to search by title…', 'sikshya') : __('Search courses by title…', 'sikshya') } searchDisabled={filterCourseId > 0} sortField={listOrderBy} sortFieldOptions={[ { value: 'title', label: 'Course title' }, { value: 'modified', label: 'Last modified' }, { value: 'id', label: 'Course ID' }, ]} onSortFieldChange={(v) => setListOrderBy(v as 'modified' | 'title' | 'id')} sortOrder={listOrder} onSortOrderToggle={() => setListOrder((o) => (o === 'asc' ? 'desc' : 'asc'))} trailing={
} />
void onPrereqBulkApply()} applyBusy={bulkBusy} trashMode={false} />
{listQ.data?.total != null ? `${listQ.data.total} course${listQ.data.total === 1 ? '' : 's'}` : '\u00a0'}
{bulkError ? (
setBulkError(null)} />
) : null} {listQ.error ? (
listQ.refetch()} />
) : listQ.loading ? ( ) : ( <> r.course_id} wrapInCard={false} emptyContent={ 0 ? 'No course matches this filter, or it was removed.' : 'Try another search.' } /> } /> )}
{/* What is this page? — disambiguation block */}

{__('What this page does', 'sikshya')}

These are {__('access rules', 'sikshya')} the LMS enforces. They are different from the marketing "Prerequisites" list inside the Course Builder, which is just text shown to visitors. Use this page when you want learners to {__('actually', 'sikshya')} be blocked until they finish prior content.

); }