import { Badge, Box, Button, ButtonGroup, Center, Checkbox, Flex, FormControl, IconButton, Input, InputGroup, InputRightElement, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, SimpleGrid, Spinner, Stack, Text, Tooltip, useToast, } from '@chakra-ui/react'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; import { BiLinkExternal, BiPlus } from 'react-icons/bi'; import { MdClear } from 'react-icons/md'; import { PiPlus } from 'react-icons/pi'; import Select from '../../../../assets/js/back-end/components/common/Select'; import { VirtualizedContainer } from '../../../../assets/js/back-end/components/common/VirtualizedContainer'; import { SkeletonQuestionsList } from '../../../../assets/js/back-end/skeleton'; import API from '../../../../assets/js/back-end/utils/api'; import { isEmpty } from '../../../../assets/js/back-end/utils/utils'; import H5P_BADGE_COLOR from '../../constants/badgeColors'; import h5pUrls from '../../constants/urls'; import TruncatedText from '../TruncatedText'; export interface H5PContent { id: number; title: string; content_type: string; library_name: string; created_at: string; view_url?: string; } interface H5PContentListResponse { data: H5PContent[]; meta: { total: number; pages: number; current_page: number; per_page: number; }; } interface FilterForm { search: string; typeFilter: string; } interface Props { isOpen: boolean; onClose: () => void; onAdd: (items: H5PContent[]) => void; excludeIds?: number[]; } const h5pContentsAPI = new API(h5pUrls.h5pContents); const h5pContentTypesAPI = new API(h5pUrls.h5pContentTypes); const H5PContentPicker: React.FC = ({ isOpen, onClose, onAdd, excludeIds = [], }) => { const [bulkIds, setBulkIds] = useState([]); const containerRef = useRef(null); const toast = useToast(); const methods = useForm({ defaultValues: { search: '', typeFilter: 'all' }, }); const { control, register, setValue } = methods; const search = useWatch({ control, name: 'search' }); const typeFilter = useWatch({ control, name: 'typeFilter' }); // Fetch installed H5P content types from the H5P libraries table — the single source of truth, no hardcoded list. const typesQuery = useQuery<{ name: string; title: string }[]>({ queryKey: ['h5pContentTypes'], queryFn: () => h5pContentTypesAPI.list({}), staleTime: 5 * 60 * 1000, }); const contentTypeOptions = useMemo(() => { const types = typesQuery.data ?? []; return types.map((t) => ({ label: t.title, value: t.name })); }, [typesQuery.data]); useEffect(() => { if (!isOpen) { setBulkIds([]); setValue('search', ''); setValue('typeFilter', 'all'); } }, [isOpen, setValue]); // Stable key for the exclude list so the query refetches when the quiz's questions change. const excludeKey = useMemo( () => [...excludeIds].sort((a, b) => a - b).join(','), [excludeIds], ); const contentsQuery = useInfiniteQuery({ queryKey: ['h5pContents', search?.trim(), typeFilter, excludeKey], queryFn: ({ pageParam = 1 }) => { const params: Record = { per_page: 20, page: pageParam, }; if (search) params.search = search; if (typeFilter !== 'all') params.type = typeFilter; if (excludeIds.length) params.exclude = excludeIds; return h5pContentsAPI.list(params); }, enabled: isOpen, initialPageParam: 1, staleTime: 60 * 1000, getNextPageParam: (lastResponse: H5PContentListResponse) => { if (!lastResponse?.meta) return undefined; return lastResponse.meta.current_page >= lastResponse.meta.pages ? undefined : lastResponse.meta.current_page + 1; }, }); useEffect(() => { if (contentsQuery.isError) { toast({ title: __('Failed to fetch H5P content.', 'learning-management-system'), status: 'error', isClosable: true, }); } }, [contentsQuery.isError, toast]); const allItems: H5PContent[] = useMemo(() => { const pages = contentsQuery.data?.pages; if (!pages) return []; return pages.flatMap((page: H5PContentListResponse) => page.data ?? []); }, [contentsQuery.data]); const hasNextPage = contentsQuery.hasNextPage; const isFetchingNextPage = contentsQuery.isFetchingNextPage; const handleAddImmediate = useCallback( (item: H5PContent) => { onAdd([item]); onClose(); }, [onAdd, onClose], ); const handleAddSelected = () => { const selectedItems = allItems.filter((i) => bulkIds.includes(String(i.id)), ); if (selectedItems.length > 0) { onAdd(selectedItems); onClose(); } }; const getItemHeight = useCallback(() => 52, []); const renderItem = useCallback( (index: number) => { const item = allItems[index]; if (!item) return null; const isChecked = bulkIds.includes(String(item.id)); return (
{ const id = String(item.id); setBulkIds((prev) => { const set = new Set(prev); if (e.target.checked) set.add(id); else set.delete(id); return Array.from(set); }); }} />
{item.content_type || item.library_name}
{item.view_url && ( } minW="auto" /> )} } minW="auto" onClick={() => handleAddImmediate(item)} />
); }, [allItems, bulkIds, handleAddImmediate], ); return ( {__('H5P Question Bank', 'learning-management-system')} {/* Filter bar — identical structure to QuestionBank */} 0 && bulkIds.length > 0 && bulkIds.length < allItems.length } isChecked={ allItems.length > 0 && bulkIds.length === allItems.length } onChange={(e) => { setBulkIds( e.target.checked ? allItems.map((i) => String(i.id)) : [], ); }} /> {search && ( setValue('search', '')} /> )} ( )} /> (