(null);
@@ -41,8 +47,12 @@ export default function CanvasMaterialImportModal({ isOpen, folderId, onClose, o
if (!auth.data?.connected) { setState('connect'); return; }
const result = await canvasApi.getImportCourses(folderId);
if (current !== requestId.current) return;
- setCourses(result.data?.courses || []); setState('ready');
+ const available = result.data?.courses || [];
+ setCourses(available); setState('ready');
setCourseId(''); setResources([]); setSelected(new Set()); setReport(null);
+ const linked = linkedCanvasCourseId ? available.find(course => String(course.id) === String(linkedCanvasCourseId)) : undefined;
+ setLinkedUnavailable(!!linkedCanvasCourseId && !linked);
+ if (linked) void loadMaterials(linked.id);
} catch (err) {
if (current === requestId.current) { setError(err instanceof Error ? err.message : 'Could not load Canvas. Try again.'); setState('connect'); }
}
@@ -87,6 +97,16 @@ export default function CanvasMaterialImportModal({ isOpen, folderId, onClose, o
} catch (err) { if (current === requestId.current) setError(err instanceof Error ? err.message : 'Could not load course materials.'); }
finally { if (current === requestId.current) setLoadingMaterials(false); }
}
+ async function linkSelectedCourse() {
+ if (!courseId || linking) return;
+ setLinking(true); setError('');
+ try {
+ await foldersApi.setLinkedCanvasCourse(folderId, courseId);
+ setLinkedUnavailable(false);
+ onLinkedCanvasCourseChange?.(courseId);
+ } catch (err) { setError(err instanceof Error ? err.message : 'Could not save the linked Canvas course.'); }
+ finally { setLinking(false); }
+ }
async function importSelected() {
if (!selected.size || importing) return;
setImporting(true); setError('');
@@ -123,9 +143,14 @@ export default function CanvasMaterialImportModal({ isOpen, folderId, onClose, o
+ {linkedUnavailable && The Canvas course linked to this course isn't available to your account. Choose another course.
}
+ {courseId && String(courseId) !== String(linkedCanvasCourseId ?? '') && onLinkedCanvasCourseChange &&
+
+ Imports and exports for this course will start with this Canvas course.
+
}
{!courses.length && No teaching courses found. This import currently requires a Teacher enrollment in Canvas.
}
{warnings.map((warning, index) => {warning}
)}
{(warnings.length > 0 || error) &&
diff --git a/src/components/CourseView.tsx b/src/components/CourseView.tsx
index 08469621..e71ddf8f 100644
--- a/src/components/CourseView.tsx
+++ b/src/components/CourseView.tsx
@@ -25,6 +25,7 @@ interface CourseData {
id: string;
name: string;
quizzes: QuizData[];
+ canvasCourseId?: string | null;
}
const CourseView = () => {
@@ -200,7 +201,8 @@ const CourseView = () => {
status: typeof quiz === 'string' ? undefined : quiz.status,
progress: typeof quiz === 'string' ? undefined : quiz.progress,
updatedAt: typeof quiz === 'string' ? undefined : quiz.updatedAt
- })) || []
+ })) || [],
+ canvasCourseId: folderResponse.folder.canvasCourseId
};
setCourse(course);
@@ -654,6 +656,8 @@ const CourseView = () => {
setCourse(prev => prev ? { ...prev, canvasCourseId } : prev)}
materials={materials.map(m => ({
id: m._id,
name: m.name,
diff --git a/src/components/CreateCourseModal.test.tsx b/src/components/CreateCourseModal.test.tsx
index 2c652bb7..b6fc2e68 100644
--- a/src/components/CreateCourseModal.test.tsx
+++ b/src/components/CreateCourseModal.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import CreateCourseModal from './CreateCourseModal';
import { canvasApi } from '../services/api';
@@ -15,6 +15,8 @@ vi.mock('../services/api', () => ({
canvasApi: {
getConfig: vi.fn().mockResolvedValue({ data: { enabled: false } }),
getAuthStatus: vi.fn(),
+ getCourses: vi.fn(),
+ createModule: vi.fn(),
},
}));
@@ -54,5 +56,21 @@ describe('CreateCourseModal workflow', () => {
fireEvent.click(screen.getByRole('button', { name: /Next/ }));
expect(screen.getByRole('button', { name: 'Connect to Canvas' })).toBeInTheDocument();
});
+ it('links a Canvas course without creating a Canvas module', async () => {
+ vi.mocked(canvasApi.getConfig).mockResolvedValueOnce({ data: { enabled: true } } as never);
+ vi.mocked(canvasApi.getAuthStatus).mockResolvedValueOnce({ data: { connected: true } } as never);
+ vi.mocked(canvasApi.getCourses).mockResolvedValueOnce({ data: { courses: [{ id: '42', name: 'Physics 101', courseCode: 'PHYS' }] } } as never);
+ const onSubmit = vi.fn().mockResolvedValue(undefined);
+ render();
+ await screen.findByRole('tab', { name: /^Canvas/ });
+ fireEvent.change(screen.getByLabelText('Course Name'), { target: { value: 'Physics' } });
+ fireEvent.click(screen.getByRole('button', { name: /Next/ }));
+ fireEvent.click(screen.getByRole('button', { name: /Next/ }));
+ fireEvent.click(await screen.findByText('Physics 101'));
+ expect(screen.queryByText(/will be created/)).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: 'Create Course' }));
+ await waitFor(() => expect(onSubmit).toHaveBeenCalledWith('Physics', [], '42'));
+ expect(canvasApi.createModule).not.toHaveBeenCalled();
+ });
});
diff --git a/src/components/CreateCourseModal.tsx b/src/components/CreateCourseModal.tsx
index 20963f04..cb63c634 100644
--- a/src/components/CreateCourseModal.tsx
+++ b/src/components/CreateCourseModal.tsx
@@ -27,7 +27,7 @@ interface CanvasCourse {
interface CreateCourseModalProps {
isOpen: boolean;
onClose: () => void;
- onSubmit: (courseName: string, materials: Material[], canvasCourseId?: string, canvasModuleId?: string) => Promise;
+ onSubmit: (courseName: string, materials: Material[], canvasCourseId?: string) => Promise;
}
const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps) => {
@@ -45,7 +45,6 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
const [canvasError, setCanvasError] = useState('');
const [canvasCourses, setCanvasCourses] = useState([]);
const [selectedCourse, setSelectedCourse] = useState(null);
- const [canvasModuleId, setCanvasModuleId] = useState(null);
const [linkToCanvas, setLinkToCanvas] = useState(false);
const [isDisconnecting, setIsDisconnecting] = useState(false);
@@ -163,7 +162,6 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
const handleUnlinkCanvas = () => {
setSelectedCourse(null);
- setCanvasModuleId(null);
setLinkToCanvas(false);
};
@@ -174,8 +172,7 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
setCanvasConnected(false);
setCanvasCourses([]);
setSelectedCourse(null);
- setCanvasModuleId(null);
- setLinkToCanvas(false);
+ setLinkToCanvas(false);
} catch {
// ignore
} finally {
@@ -188,23 +185,10 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
setIsCreating(true);
setUploadProgress(0);
try {
- // Create Canvas module now (only once, at submission time)
- let moduleId = canvasModuleId;
- if (linkToCanvas && selectedCourse && !moduleId) {
- try {
- const res = await canvasApi.createModule(selectedCourse.id, courseName.trim());
- moduleId = res.data?.module?.id || null;
- setCanvasModuleId(moduleId);
- } catch {
- moduleId = null;
- }
- }
-
await onSubmit(
courseName.trim(),
materials,
- linkToCanvas && selectedCourse ? selectedCourse.id : undefined,
- linkToCanvas && moduleId ? moduleId : undefined
+ linkToCanvas && selectedCourse ? selectedCourse.id : undefined
);
resetModal();
onClose();
@@ -230,7 +214,6 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
const handleSkipCanvas = async () => {
setLinkToCanvas(false);
setSelectedCourse(null);
- setCanvasModuleId(null);
await handleCreateCourse();
};
@@ -239,7 +222,6 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
setMaterials([]);
setCurrentStep(1);
setSelectedCourse(null);
- setCanvasModuleId(null);
setLinkToCanvas(false);
setCanvasCourses([]);
};
@@ -391,7 +373,7 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
Link to Canvas Course
-
Optionally link this course to a Canvas course. A module named "{courseName}" will be created automatically.
+
Optionally link this course to a Canvas course. Canvas imports and exports will start with this course selected.
{!canvasConnected ? (
@@ -421,7 +403,6 @@ const CreateCourseModal = ({ isOpen, onClose, onSubmit }: CreateCourseModalProps
{selectedCourse.courseCode}{selectedCourse.term ? ` — ${selectedCourse.term}` : ''}
-
Module "{courseName}" will be created on submit
+ {linkedUnavailable && !loading && (
+ The Canvas course linked to this course isn't available to your account. Choose another course.
+ )}
{loading ? (
Loading courses...
) : courses.length === 0 ? (
@@ -278,7 +320,7 @@ const CanvasExportModal = ({
onClick={() => handleCourseSelect(course)}
>
{course.name}
- {course.courseCode}{course.term ? ` - ${course.term}` : ''}
+ {course.courseCode}{course.term ? ` - ${course.term}` : ''}{String(course.id) === String(linkedCanvasCourseId) ? ' · Linked' : ''}
))}
@@ -293,6 +335,13 @@ const CanvasExportModal = ({
Back to courses
Course: {selectedCourse?.name}
+ {selectedCourse && folderId && onLinkedCanvasCourseChange
+ && String(selectedCourse.id) !== String(linkedCanvasCourseId ?? '') && (
+
+ )}
{loading ? (
Loading modules...
) : (
diff --git a/src/components/review/ReviewEdit.tsx b/src/components/review/ReviewEdit.tsx
index bb94e47a..b7bca6db 100644
--- a/src/components/review/ReviewEdit.tsx
+++ b/src/components/review/ReviewEdit.tsx
@@ -1056,6 +1056,13 @@ const ReviewEdit = ({ quizId, learningObjectives, workflowMode = 'review' }: Rev
onClose={() => setCanvasExportModalOpen(false)}
quizId={quizId}
quizName={currentQuiz?.name || 'Learning Object'}
+ linkedCanvasCourseId={typeof currentQuiz?.folder === 'object' ? currentQuiz.folder.canvasCourseId : undefined}
+ folderId={typeof currentQuiz?.folder === 'object' ? currentQuiz.folder._id : currentQuiz?.folder}
+ onLinkedCanvasCourseChange={canvasCourseId => {
+ if (currentQuiz && typeof currentQuiz.folder === 'object') {
+ dispatch(updateQuizLocally({ ...currentQuiz, folder: { ...currentQuiz.folder, canvasCourseId } }));
+ }
+ }}
showNotification={showNotification}
/>
diff --git a/src/services/api.ts b/src/services/api.ts
index 00b21a18..7a8946a1 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -50,6 +50,7 @@ export interface Folder {
instructor: string;
materials: Array;
quizzes: Array;
+ canvasCourseId?: string | null;
stats: {
totalQuizzes: number;
totalQuestions: number;
@@ -187,7 +188,7 @@ export interface CoverageMap {
export interface Quiz {
_id: string;
name: string;
- folder: string | Pick;
+ folder: string | Pick;
materials: string[] | Material[];
learningObjectives: string[];
questions: string[];
@@ -677,8 +678,8 @@ export const foldersApi = {
},
// POST /api/create/folders - Create new folder with quizzes
- createFolder: async (name: string, quizCount: number = 1, canvasCourseId?: string, canvasModuleId?: string): Promise<{ folder: Folder }> => {
- const response = await apiClient.post<{ success: boolean; data: { folder: Folder }; message: string }>('/folders', { name, quizCount, canvasCourseId, canvasModuleId });
+ createFolder: async (name: string, quizCount: number = 1, canvasCourseId?: string): Promise<{ folder: Folder }> => {
+ const response = await apiClient.post<{ success: boolean; data: { folder: Folder }; message: string }>('/folders', { name, quizCount, canvasCourseId });
return response.data;
},
@@ -688,6 +689,12 @@ export const foldersApi = {
return response.data;
},
+ // PUT /api/create/folders/:id - Link (or unlink with null) a Canvas course
+ setLinkedCanvasCourse: async (id: string, canvasCourseId: string | null): Promise<{ folder: Folder }> => {
+ const response = await apiClient.put<{ success: boolean; data: { folder: Folder }; message: string }>(`/folders/${id}`, { canvasCourseId });
+ return response.data;
+ },
+
// PUT /api/create/folders/:id - Update folder name
updateFolder: async (id: string, name: string): Promise<{ folder: Folder }> => {
const response = await apiClient.put<{ success: boolean; data: { folder: Folder }; message: string }>(`/folders/${id}`, { name });
diff --git a/src/styles/components/CreateCourseModal.css b/src/styles/components/CreateCourseModal.css
index bb914ffd..14834f1b 100644
--- a/src/styles/components/CreateCourseModal.css
+++ b/src/styles/components/CreateCourseModal.css
@@ -232,15 +232,6 @@
color: var(--color-muted-foreground);
}
-.canvas-linked-module {
- font-size: var(--font-size-xs);
- color: var(--color-primary);
- margin-top: 2px;
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
/* Reuse CanvasExportModal styles for course list */
.canvas-connect-step {
text-align: center;