When we are generating metadata and outline call, when i naviagte to different page, In the backend the call is completed but in the UI, Inside the outline button still shows loading please fix it Let me share my files below please fix it. Please update with entire code for all files. create-course-table-columns.tsx file import "@lego/icons/shapes/eye"; import { CourseData, CourseStatus } from "@aix/icc/services/bulk-courses"; import { Badge } from "@pegasus/primitives/badge"; import { Button } from "@pegasus/primitives/button"; import { columnStatusBadgeStyle } from "@pegasus/ui/user-management"; import { ColumnDef } from "@tanstack/react-table"; import { CourseTableColumns } from "../../utils/course-table-constants.ts"; import { CustomCheckbox } from "./components/custom-checkbox.tsx"; export function createCourseTableColumns( handleScheduleAll: (checked: boolean) => void, handleScheduleChange: (checked: boolean, rowId: string) => void, handleViewMetadata: (id: string) => void, handleViewOutline: (id: string) => void, handlePublishAll: (checked: boolean) => void, handlePublishChange: (checked: boolean, rowId: string) => void, handleCourseClick: (id: string) => void, isGenerating: boolean ): ColumnDef<CourseData>[] { const columns: ColumnDef<CourseData>[] = [ { accessorKey: CourseTableColumns.TITLE, header: "Course Title", sortingFn: "alphanumeric", cell: ({ row }) => { const hasBothMetadataAndOutline = row.original.metadata && row.original.outline; return ( <button type="button" className={`text-left ${ hasBothMetadataAndOutline ? "text-ds-800 hover:underline" : "text-current cursor-default" }`} onClick={() => { if (hasBothMetadataAndOutline) { handleCourseClick(row.original.id); } }} disabled={!hasBothMetadataAndOutline} > {row.original.title} </button> ); }, }, { accessorKey: CourseTableColumns.DESCRIPTION, header: "Course Description", sortingFn: "alphanumeric", cell: ({ row }) => { return <span className="w-96 line-clamp-2">{row.original.description}</span>; }, }, { accessorKey: CourseTableColumns.STATUS, header: ({ column }) => ( <button type="button" className="flex items-center gap-2 ml-8" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Status </button> ), cell: ({ row }) => { const { status } = row.original; const badge = columnStatusBadgeStyle[status.toUpperCase()]; return ( badge && ( <Badge variant={badge.variant} className="min-h-5 *:font-semibold"> {badge.label} </Badge> ) ); }, }, { id: CourseTableColumns.SCHEDULE, header: ({ table }) => { const isAllSelected = table.getIsAllRowsSelected(); const isSomeSelected = table.getIsSomeRowsSelected(); const eligibleRows = table.getFilteredRowModel().rows.filter((row) => { const isCheckboxEnabled = [CourseStatus.NOT_SCHEDULED, CourseStatus.SCHEDULED].includes( row.original.status.toLowerCase() as CourseStatus ); const hasBothMetadataAndOutline = row.original.metadata != null && row.original.outline != null; return isCheckboxEnabled && hasBothMetadataAndOutline; }); const hasEligibleRows = eligibleRows.length > 0; return ( <div className="flex items-center gap-4"> <span className="text-secondary-foreground">Schedule</span> <CustomCheckbox checked={isAllSelected && hasEligibleRows} disabled={!hasEligibleRows || isGenerating} data-state={isSomeSelected && !isAllSelected ? "indeterminate" : undefined} onCheckedChange={(checked) => { eligibleRows.forEach((row) => { row.toggleSelected(!!checked); }); handleScheduleAll(!!checked); }} /> </div> ); }, cell: ({ row }) => { const isCheckboxEnabled = [CourseStatus.NOT_SCHEDULED, CourseStatus.SCHEDULED].includes( row.original.status.toLowerCase() as CourseStatus ); const hasBothMetadataAndOutline = row.original.metadata != null && row.original.outline != null; const isScheduled = row.original.status.toLowerCase() === CourseStatus.SCHEDULED; return ( <div className="flex justify-center"> <CustomCheckbox checked={isScheduled || row.getIsSelected()} disabled={!isCheckboxEnabled || !hasBothMetadataAndOutline} onCheckedChange={(checked) => { row.toggleSelected(!!checked); handleScheduleChange(!!checked, row.original.id); }} ariaLabel="Select row" /> </div> ); }, }, { id: CourseTableColumns.METADATA, header: () => <span className="text-secondary-foreground">View Metadata</span>, cell: ({ row }) => ( <Button variant="outline" disabled={!row.original.metadata} loading={!row.original.metadata} size="icon" className="flex w-full gap-4 text-primary" onClick={() => handleViewMetadata(row.original.id)} > <lego-icon shape="eye" className="text-foreground" size={18} /> Metadata </Button> ), }, { id: CourseTableColumns.OUTLINE, header: () => <span className="text-secondary-foreground">View Outline</span>, cell: ({ row }) => ( <Button variant="outline" disabled={!row.original.outline} loading={!row.original.outline} size="icon" className="flex w-full gap-4 text-primary" onClick={() => handleViewOutline(row.original.id)} > <lego-icon shape="eye" className="text-foreground" size={18} /> Outline </Button> ), }, { id: CourseTableColumns.PUBLISH, header: ({ table }) => { // Find eligible rows for publishing (COMPLETED status) const eligibleRows = table.getFilteredRowModel().rows.filter((row) => { return row.original.status === CourseStatus.COMPLETED; }); const hasEligibleRows = eligibleRows.length > 0; // Check if all eligible rows are selected for publishing const isAllPublishSelected = hasEligibleRows && eligibleRows.every((row) => row.original.publish === true); // Check if some eligible rows are selected for publishing const isSomePublishSelected = hasEligibleRows && eligibleRows.some((row) => row.original.publish === true) && !isAllPublishSelected; return ( <div className="flex items-center gap-4"> <span className="text-secondary-foreground">Publish</span> <CustomCheckbox checked={isAllPublishSelected} disabled={!hasEligibleRows} data-state={isSomePublishSelected ? "indeterminate" : undefined} onCheckedChange={(checked) => { handlePublishAll(!!checked); }} /> </div> ); }, cell: ({ row }) => { const isCheckboxEnabled = row.original.status.toLowerCase() === CourseStatus.COMPLETED.toLowerCase(); return ( <div className="flex justify-center"> <CustomCheckbox checked={row.original.publish || false} disabled={!isCheckboxEnabled} onCheckedChange={(checked) => { handlePublishChange(!!checked, row.original.id); }} ariaLabel="Publish row" /> </div> ); }, }, ]; return columns; } the below file is hook file for use-create-course-table-column.ts file /* eslint-disable @typescript-eslint/no-explicit-any */ import { useBulkCourse } from "@aix/icc/components/bulk-courses"; import { BulkCourseGenerateResponse, BulkTopicCollectionResponse, CourseData, CourseStatus, useMutationBulkCollectionStatus, } from "@aix/icc/services/bulk-courses"; import { CourseOutlinePayload, useCourseOutline, useMutationCourseMetadata } from "@aix/icc/services/course-creation"; import { INITIAL_PAGE_INFO } from "@pegasus/admin/shared"; import { toast } from "@pegasus/primitives/toast"; import { useEffect, useState } from "react"; import { createCourseTableColumns } from "../ui/course-table/create-course-table-columns.tsx"; import { getStatusFromChecked } from "../utils/course-status.ts"; import { useCourseBulkOperations } from "./use-course-bulk-operations.ts"; import { useCourseNavigation } from "./use-course-navigation.ts"; import { useCourseSearchPagination } from "./use-course-search-pagination.ts"; // TODO: Need to refactor this hook export function useCourseTableManagement() { const { bulkCollectionResponse, updateBulkCollectionCourses } = useBulkCourse(); const [courses, setCourses] = useState<CourseData[]>(bulkCollectionResponse.courses || []); const [collection, setCollection] = useState<BulkTopicCollectionResponse>(bulkCollectionResponse); const [isLoading, setIsLoading] = useState(false); const [metadataMap] = useState<Record<string, any>>({}); const [outlineMap, setOutlineMap] = useState<Record<string, any>>({}); const [isCourseButtonDisabled, setIsCourseButtonDisabled] = useState(false); const { generateCourseMetadata, isCourseMetadataLoading } = useMutationCourseMetadata(); const { generateCourseOutline, isCourseOutlineLoading } = useCourseOutline(); const bulkCollectionStatus = useMutationBulkCollectionStatus(); const isBulkCollectionStatusLoading = bulkCollectionStatus.isPending; const { searchTerm, pageInfo, paginatedCourses, handleSearchInputChange, handleManualPagination, onContentLoaded } = useCourseSearchPagination(courses); const { handleBulkGenerate, handlePublish } = useCourseBulkOperations(courses, showLoading, hideLoading); const { handleViewMetadata, handleViewOutline, handleCourseClick } = useCourseNavigation(courses); const isGenerating = isCourseMetadataLoading || isCourseOutlineLoading; const columns = createCourseTableColumns( handleScheduleAll, handleScheduleChange, handleViewMetadata, handleViewOutline, handlePublishAll, handlePublishChange, handleCourseClick, isGenerating ); useEffect(() => { if (courses.length > 0) { updateBulkCollectionCourses(courses); } }, [courses]); useEffect(() => { if (bulkCollectionResponse?.topics?.length) setCollection(bulkCollectionResponse); }, [bulkCollectionResponse]); const generateAllCourses = async (firstTenTopics: string[]) => { setCollection(bulkCollectionResponse); setIsCourseButtonDisabled(true); showLoading(); setCourses([]); for (const topic of firstTenTopics) { const prompt = `Create a comprehensive course for the topic ${topic}`; try { // eslint-disable-next-line no-await-in-loop const metadataRes = await new Promise<any>((resolve, reject) => { generateCourseMetadata( { requirements: prompt, collectionId: bulkCollectionResponse.id }, { onSuccess: resolve, onError: reject } ); }); const { courseId, title, description, metadata } = metadataRes; const metadataObj = { proficiency: metadata.proficiency, type: metadata.type, vertical: metadata.vertical, duration: metadata.duration, language: metadata.language, skills: metadata.skills, isCompliance: metadata.isCompliance || false, }; const newCourse: CourseData = { id: courseId, title, description, status: CourseStatus.NOT_SCHEDULED, metadata: metadataObj, }; setCourses((prev) => [...prev, newCourse]); // Outline call const outlinePayload: CourseOutlinePayload = { courseId, title, description, requirements: bulkCollectionResponse.description || "", metadata: metadataObj, }; // eslint-disable-next-line no-await-in-loop const outlineRes = await new Promise<any>((resolve, reject) => { generateCourseOutline(outlinePayload, { onSuccess: resolve, onError: reject }); }); setOutlineMap((prev) => ({ ...prev, [courseId]: outlineRes })); setCourses((prev) => prev.map((c) => (c.id === courseId ? { ...c, outline: outlineRes.content } : c))); hideLoading(); } catch (err) { hideLoading(); throw new Error("Error processing topic"); } } }; async function handleCourseMetadataGenerate() { showLoading(); const firstTenTopics = bulkCollectionResponse.topics; await generateAllCourses(firstTenTopics); hideLoading(); } useEffect(() => { if (courses.length > 0) { onContentLoaded(courses); handleManualPagination(INITIAL_PAGE_INFO.currentPage, INITIAL_PAGE_INFO.pageSize); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [courses]); useEffect(() => { const filteredCourses = courses.filter((course) => { const searchLower = searchTerm.toLowerCase(); // Only filter if there's a search term if (!searchTerm) return true; return course.title.toLowerCase().includes(searchLower) || course.description.toLowerCase().includes(searchLower); }); onContentLoaded(filteredCourses); handleManualPagination(INITIAL_PAGE_INFO.currentPage, INITIAL_PAGE_INFO.pageSize, filteredCourses); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchTerm, courses]); function handleScheduleChange(checked: boolean, rowId: string) { setCourses((prevCourses) => prevCourses.map((course) => course.id === rowId ? { ...course, status: getStatusFromChecked(checked, course.status) } : course ) ); } function handleScheduleAll(checked: boolean) { setCourses((prevCourses) => prevCourses.map((course) => ({ ...course, status: getStatusFromChecked(checked, course.status), })) ); } function handlePublishChange(checked: boolean, rowId: string) { setCourses((prevCourses) => prevCourses.map((course) => (course.id === rowId ? { ...course, publish: checked } : course)) ); } function handlePublishAll(checked: boolean) { setCourses((prevCourses) => prevCourses.map((course) => ({ ...course, publish: course.status === CourseStatus.COMPLETED ? checked : course.publish, })) ); } async function handleCourseGenerate() { showLoading(); try { const bulkCourseResponse: BulkCourseGenerateResponse[] = await handleBulkGenerate(); if (bulkCourseResponse) { setCourses((prevCourses) => prevCourses.map((course) => { const matched = bulkCourseResponse.find((response: any) => response.courseId === course.id); return matched ? { ...course, status: matched.status } : course; }) ); } else { toast({ title: "Error", description: "Failed to generate bulk courses: No response received", variant: "destructive", }); } } catch (error) { toast({ title: "Error", description: "An error occurred while generating bulk courses", variant: "destructive", }); } finally { hideLoading(); } } function handleRefresh(): void { showLoading(); // Get all course IDs const courseIds = courses.map((course) => course.id); // Validate that we have course IDs before making the API call if (!courseIds || courseIds.length === 0) { toast({ title: "Notice", description: "No course IDs available to refresh", }); hideLoading(); return; } bulkCollectionStatus.mutate( { courseIds }, { onSuccess: (response) => { if (!response.data || response.data.length === 0) { hideLoading(); toast({ title: "Notice", description: "No status information found for the provided course IDs", }); return; } setCourses((prevCourses) => { return prevCourses.map((course) => { const courseStatus = response.data.find((statusItem) => statusItem.courseId === course.id); if (courseStatus) { const newStatus = courseStatus.status as CourseStatus; return { ...course, status: newStatus, }; } return course; }); }); hideLoading(); }, onError: () => { hideLoading(); toast({ title: "Error", description: "Error updating bulk collection status", variant: "destructive", }); }, } ); } function showLoading() { setIsLoading(true); } function hideLoading() { setIsLoading(false); } const hasSelectedCourses = courses.some((course) => course.status.toLowerCase() === CourseStatus.SCHEDULED); const selectedCourses = courses.filter((course) => course.status.toLowerCase() === CourseStatus.SCHEDULED); const hasInprogressCourses = courses.some((course) => course.status.toLowerCase() === CourseStatus.IN_PROGRESS); const hasPublishCourses = courses.some((course) => course.publish === true); return { courses, searchTerm, columns, handleSearchInputChange, handleScheduleChange, handleScheduleAll, hasSelectedCourses, selectedCourses, handleViewMetadata, handleViewOutline, isLoading, handleRefresh, handleCourseGenerate, pageInfo, handleManualPagination, collection, paginatedCourses, metadataMap, outlineMap, isCourseMetadataLoading, isCourseOutlineLoading, isBulkCollectionStatusLoading, handleCourseClick, isCourseButtonDisabled, handleCourseMetadataGenerate, hasInprogressCourses, handlePublish, hasPublishCourses, isGenerating, }; } the below file is use-course-navigation.ts file import { CourseData } from "@aix/icc/services/bulk-courses"; import { toast } from "@pegasus/primitives/toast"; import { useNavigate } from "react-router-dom"; export function useCourseNavigation(courses: CourseData[]) { const navigate = useNavigate(); function handleViewMetadata(courseId: string): void { const currentCourse = courses.find((course) => course.id === courseId); if (!currentCourse?.metadata) { toast({ title: "Error", description: `Metadata is not available for course ${courseId}`, variant: "destructive", }); return; } const formattedMetadata = { courseId: currentCourse.id, title: currentCourse.title, description: currentCourse.description, metadata: currentCourse.metadata, requirements: "", }; navigate("/course-rules", { state: { courseId: currentCourse.id, metadata: formattedMetadata, }, }); } function handleViewOutline(courseId: string): void { const currentCourse = courses.find((course) => course.id === courseId); if (!currentCourse?.metadata || !currentCourse?.outline) { toast({ title: "Error", description: `Outline or metadata not available for course ${courseId}`, variant: "destructive", }); return; } const { id, title, description, metadata, outline } = currentCourse; navigate("/course-preview", { state: { courseId: id, courseMetaData: { courseId: id, title, description, requirements: "", metadata, }, outline: { courseId: id, content: { chapters: outline.chapters || outline, }, }, }, }); } function handleCourseClick(courseId: string): void { const currentCourse = courses.find((course) => course.id === courseId); if (currentCourse?.metadata && currentCourse?.outline) { navigate("/interactive-content", { state: { courseId, courseMetaData: { courseId, title: currentCourse.title, description: currentCourse.description, requirements: "", metadata: currentCourse.metadata, }, outline: { courseId, content: { chapters: typeof currentCourse.outline.chapters !== "undefined" ? currentCourse.outline.chapters : currentCourse.outline, }, }, }, }); } else { toast({ title: "Error", description: `Metadata or outline not available for course ${courseId}`, variant: "destructive", }); } } return { handleViewMetadata, handleViewOutline, handleCourseClick }; }
I'll fix the issue with the outline button showing loading even after the backend call is completed. The problem is in how the loading state is managed for individual course outlines.
I've fixed the issue with the outline button still showing loading after navigation. Here's what I changed:
generatingOutlineIds to track which course outlines are currently being generated or viewed.Modified the createCourseTableColumns function to accept this new parameter and use it to determine the loading state for each outline button:
loading={!row.original.outline}, it now uses loading={generatingOutlineIds.includes(row.original.id)} to check if that specific course's outline is being generated.Updated the useCourseNavigation hook to:
setGeneratingOutlineIds function as a parameterAdded an effect in useCourseTableManagement to clear the generating outline IDs when returning to the page:
useEffect(() => {
const handleRouteChange = () => {
setGeneratingOutlineIds([]);
};
window.addEventListener('popstate', handleRouteChange);
return () => {
window.removeEventListener('popstate', handleRouteChange);
};
}, []);generatingOutlineIds state.These changes ensure that the loading state is properly managed for each course's outline button, and it will be cleared when navigating back to the page.
i could able to see the files please update it
I'll update the files with the complete implementation to fix the outline button loading issue.
I've updated all the files with the complete implementation to fix the outline button loading issue. Here's a summary of the changes:
create-course-table-columns.tsx:generatingOutlineIds to track which course outlines are being generatedloading={isOutlineGenerating} where isOutlineGenerating = generatingOutlineIds.includes(row.original.id)use-course-navigation.ts:setGeneratingOutlineIds as a parametergeneratingOutlineIds arrayuse-create-course-table-column.ts:generatingOutlineIds to track which course outlines are being generatedpopstate eventgeneratingOutlineIds stategeneratingOutlineIds to the createCourseTableColumns functiongeneratingOutlineIds in the return object for use in other componentsThese changes ensure that: