diff --git a/routes/create/__tests__/integration/folders.test.js b/routes/create/__tests__/integration/folders.test.js index 09cae0e4..e9842e75 100644 --- a/routes/create/__tests__/integration/folders.test.js +++ b/routes/create/__tests__/integration/folders.test.js @@ -314,6 +314,45 @@ describe('Folder Management API Integration Tests', () => { expect(response.body.success).toBe(false); expect(response.body.error.message).toContain('already exists'); }); + + test('should link, change and unlink the Canvas course without renaming', async () => { + await request(app).put(`/api/folders/${folderId}`).send({ canvasCourseId: '42' }).expect(200); + let folderInDb = await Folder.findById(folderId); + expect(folderInDb.canvasCourseId).toBe('42'); + expect(folderInDb.name).toBe('Original Name'); + + await request(app).put(`/api/folders/${folderId}`).send({ canvasCourseId: '77' }).expect(200); + folderInDb = await Folder.findById(folderId); + expect(folderInDb.canvasCourseId).toBe('77'); + + await request(app).put(`/api/folders/${folderId}`).send({ canvasCourseId: null }).expect(200); + folderInDb = await Folder.findById(folderId); + expect(folderInDb.canvasCourseId).toBeNull(); + }); + + test('should keep the Canvas link when only the name changes', async () => { + await Folder.updateOne({ _id: folderId }, { canvasCourseId: '42' }); + await request(app).put(`/api/folders/${folderId}`).send({ name: 'Renamed' }).expect(200); + const folderInDb = await Folder.findById(folderId); + expect(folderInDb.canvasCourseId).toBe('42'); + }); + + test('should accept a numeric Canvas course ID as Canvas returns it', async () => { + await request(app).put(`/api/folders/${folderId}`).send({ canvasCourseId: 42 }).expect(200); + const folderInDb = await Folder.findById(folderId); + expect(folderInDb.canvasCourseId).toBe('42'); + }); + + test('should reject an invalid Canvas course ID', async () => { + for (const canvasCourseId of ['', { id: 42 }, 4.2]) { + const response = await request(app) + .put(`/api/folders/${folderId}`) + .send({ canvasCourseId }) + .expect(400); + + expect(response.body.success).toBe(false); + } + }); }); describe('DELETE /api/folders/:id', () => { diff --git a/routes/create/controllers/folderController.js b/routes/create/controllers/folderController.js index a8ffa0fe..9657bde6 100644 --- a/routes/create/controllers/folderController.js +++ b/routes/create/controllers/folderController.js @@ -33,7 +33,7 @@ router.get('/', authenticateToken, attachUser, asyncHandler(async (req, res) => * Create new folder with auto-generated quizzes */ router.post('/', authenticateToken, validateCreateFolder, asyncHandler(async (req, res) => { - const { name, quizCount = 1, canvasCourseId = null, canvasModuleId = null } = req.body; + const { name, quizCount = 1, canvasCourseId = null } = req.body; const userId = req.user.id; console.log('➕ POST /folders - Creating folder:', name, 'with', quizCount, 'quizzes for user:', userId); @@ -66,8 +66,7 @@ router.post('/', authenticateToken, validateCreateFolder, asyncHandler(async (re const folder = new Folder({ name: name.trim(), instructor: userId, - canvasCourseId, - canvasModuleId + canvasCourseId }); await folder.save(); @@ -144,7 +143,7 @@ router.get('/:id', authenticateToken, validateMongoId, asyncHandler(async (req, /** * PUT /api/folders/:id - * Update folder name + * Update folder name and/or linked Canvas course (null unlinks) */ router.put('/:id', authenticateToken, validateUpdateFolder, asyncHandler(async (req, res) => { const folderId = req.params.id; @@ -177,6 +176,10 @@ router.put('/:id', authenticateToken, validateUpdateFolder, asyncHandler(async ( folder.name = name.trim(); } + if (Object.prototype.hasOwnProperty.call(req.body, 'canvasCourseId')) { + folder.canvasCourseId = req.body.canvasCourseId || null; + } + await folder.updateStats(); return successResponse(res, { folder }, 'Folder updated successfully'); diff --git a/routes/create/controllers/quizController.js b/routes/create/controllers/quizController.js index 027afe7c..160bd28a 100644 --- a/routes/create/controllers/quizController.js +++ b/routes/create/controllers/quizController.js @@ -94,7 +94,7 @@ router.get('/:id', authenticateToken, validateMongoId, asyncHandler(async (req, const userId = req.user.id; const quiz = await Quiz.findOne({ _id: quizId, createdBy: userId }) - .populate('folder', 'name') + .populate('folder', 'name canvasCourseId') .populate({ path: 'materials', select: 'name type processingStatus fileSize createdAt' @@ -146,7 +146,7 @@ router.put('/:id/review', authenticateToken, validateMongoId, asyncHandler(async ...freeQuestionMutation(new Date()), 'questionGenerationLease.token': { $exists: false } }, { $set: { 'progress.reviewCompleted': true }, $inc: { __v: 1 } }, { new: true }) - .populate('folder', 'name') + .populate('folder', 'name canvasCourseId') .populate('materials', 'name type processingStatus fileSize createdAt') .populate({ path: 'learningObjectives', select: 'text order generationMetadata createdAt', options: { sort: { order: 1 } } }) .populate({ path: 'questions', select: 'type difficulty questionText reviewStatus order createdAt', options: { sort: { order: 1 } } }) @@ -212,7 +212,7 @@ router.put('/:id', authenticateToken, validateUpdateQuiz, asyncHandler(async (re }); await quiz.save(); - await quiz.populate('folder', 'name'); + await quiz.populate('folder', 'name canvasCourseId'); return successResponse(res, { quiz }, 'Quiz updated successfully'); })); @@ -319,7 +319,7 @@ router.put('/:id/materials', authenticateToken, validateAssignMaterials, asyncHa ); const updatedQuiz = await Quiz.findById(quizId) - .populate('folder', 'name') + .populate('folder', 'name canvasCourseId') .populate('materials', 'name type processingStatus'); return successResponse(res, { quiz: updatedQuiz }, 'Materials assigned successfully'); diff --git a/routes/create/middleware/validator.js b/routes/create/middleware/validator.js index a5f16b90..ff826cc9 100644 --- a/routes/create/middleware/validator.js +++ b/routes/create/middleware/validator.js @@ -89,6 +89,16 @@ export const validateUpdateFolder = [ .trim() .isLength({ min: 1, max: 200 }) .withMessage('Folder name must be between 1 and 200 characters'), + + // Canvas returns numeric course IDs; store them as strings like the Folder schema. + body('canvasCourseId') + .optional({ nullable: true }) + .custom(value => typeof value === 'string' || Number.isInteger(value)) + .withMessage('Canvas course ID must be a string, an integer or null') + .bail() + .customSanitizer(value => String(value).trim()) + .isLength({ min: 1, max: 64 }) + .withMessage('Canvas course ID must be between 1 and 64 characters'), handleValidationErrors ]; diff --git a/routes/create/models/Folder.js b/routes/create/models/Folder.js index 071a148f..ed757dca 100644 --- a/routes/create/models/Folder.js +++ b/routes/create/models/Folder.js @@ -31,6 +31,7 @@ const folderSchema = new mongoose.Schema({ // Canvas integration (optional — linked Canvas course and module) canvasCourseId: { type: String, default: null }, + // Legacy: set by old course creation; no longer written or read. canvasModuleId: { type: String, default: null }, // Folder Statistics (for dashboard) diff --git a/src/components/CanvasMaterialImportModal.test.tsx b/src/components/CanvasMaterialImportModal.test.tsx index c9d4de98..14485074 100644 --- a/src/components/CanvasMaterialImportModal.test.tsx +++ b/src/components/CanvasMaterialImportModal.test.tsx @@ -2,7 +2,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import CanvasMaterialImportModal from './CanvasMaterialImportModal'; const mocks = vi.hoisted(() => ({ getConfig: vi.fn(), getAuthStatus: vi.fn(), getImportCourses: vi.fn(), getImportMaterials: vi.fn(), importMaterials: vi.fn(), getConnectUrl: vi.fn() })); -vi.mock('../services/api', () => ({ canvasApi: mocks })); +const folderMocks = vi.hoisted(() => ({ setLinkedCanvasCourse: vi.fn() })); +vi.mock('../services/api', () => ({ canvasApi: mocks, foldersApi: folderMocks })); const resources = [ { id: '4', resourceType: 'file', name: 'Lecture.pdf', materialType: 'pdf', supported: true }, { id: '1', resourceType: 'page', name: 'Lesson page', materialType: 'text', supported: true }, @@ -65,6 +66,34 @@ describe('Canvas material import', () => { expect(screen.getByText('No materials could be listed. Review the access messages above.')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Reconnect Canvas' })).toBeInTheDocument(); }); + it('preselects the linked Canvas course and loads its materials', async () => { + render(); + await screen.findByText('Lecture.pdf'); + expect(screen.getByRole('combobox', { name: 'Canvas course' })).toHaveValue('2'); + expect(mocks.getImportMaterials).toHaveBeenCalledWith('folder', '2'); + }); + it('saves a newly chosen course as the linked course', async () => { + folderMocks.setLinkedCanvasCourse.mockResolvedValue({ folder: {} }); + const onLinked = vi.fn(); + render(); + await screen.findByRole('combobox', { name: 'Canvas course' }); + expect(screen.queryByRole('button', { name: 'Set as linked course' })).not.toBeInTheDocument(); + fireEvent.change(screen.getByRole('combobox'), { target: { value: '2' } }); + fireEvent.click(await screen.findByRole('button', { name: 'Set as linked course' })); + await waitFor(() => expect(onLinked).toHaveBeenCalledWith('2')); + expect(folderMocks.setLinkedCanvasCourse).toHaveBeenCalledWith('folder', '2'); + }); + it('does not offer to link the course that is already linked', async () => { + render(); + await screen.findByText('Lecture.pdf'); + expect(screen.queryByRole('button', { name: 'Set as linked course' })).not.toBeInTheDocument(); + }); + it('falls back to the course list when the linked course is not available', async () => { + render(); + await screen.findByText(/linked to this course isn't available/); + expect(screen.getByRole('combobox', { name: 'Canvas course' })).toHaveValue(''); + expect(mocks.getImportMaterials).not.toHaveBeenCalled(); + }); it('explains unavailable configuration and handles blocked sign-in popups', async () => { mocks.getAuthStatus.mockResolvedValue({ data: { connected: false } }); vi.spyOn(window, 'open').mockReturnValue(null); diff --git a/src/components/CanvasMaterialImportModal.tsx b/src/components/CanvasMaterialImportModal.tsx index a80a5aaf..f6bb87ac 100644 --- a/src/components/CanvasMaterialImportModal.tsx +++ b/src/components/CanvasMaterialImportModal.tsx @@ -1,17 +1,21 @@ import { useEffect, useRef, useState } from 'react'; -import { canvasApi, CanvasImportResource, CanvasImportReport } from '../services/api'; +import { canvasApi, foldersApi, CanvasImportResource, CanvasImportReport } from '../services/api'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import '../styles/components/CanvasMaterialImportModal.css'; interface Props { isOpen: boolean; folderId: string; + /** Canvas course linked to this CREATE course; preselected when the user can still access it. */ + linkedCanvasCourseId?: string | null; + /** Called after the user saves a different Canvas course as this course's link. */ + onLinkedCanvasCourseChange?: (canvasCourseId: string) => void; onClose: () => void; onImported?: () => void | Promise; } const key = (resource: Pick) => `${resource.resourceType}:${resource.id}`; -export default function CanvasMaterialImportModal({ isOpen, folderId, onClose, onImported }: Props) { +export default function CanvasMaterialImportModal({ isOpen, folderId, linkedCanvasCourseId, onLinkedCanvasCourseChange, onClose, onImported }: Props) { const [state, setState] = useState<'loading' | 'unavailable' | 'connect' | 'ready'>('loading'); const [courses, setCourses] = useState>([]); const [courseId, setCourseId] = useState(''); @@ -23,6 +27,8 @@ export default function CanvasMaterialImportModal({ isOpen, folderId, onClose, o const [importing, setImporting] = useState(false); const [error, setError] = useState(''); const [warnings, setWarnings] = useState([]); + const [linkedUnavailable, setLinkedUnavailable] = useState(false); + const [linking, setLinking] = useState(false); const [limit, setLimit] = useState(10); const [maxBytes, setMaxBytes] = useState(50 * 1024 * 1024); const [report, setReport] = useState(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;