import React, { forwardRef, ForwardRefRenderFunction, useEffect, useImperativeHandle, useRef, useState, } from 'react'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import { ISelectableAll } from '~/client/interfaces/selectable-all'; import { toastSuccess } from '~/client/util/toastr'; import { IFormattedSearchResult, IPageWithSearchMeta } from '~/interfaces/search'; import { OnDeletedFunction } from '~/interfaces/ui'; import { useIsGuestUser, useIsReadOnlyUser, useIsSearchServiceConfigured, useIsSearchServiceReachable, } from '~/stores/context'; import { usePageDeleteModal } from '~/stores/modal'; import { mutatePageTree } from '~/stores/page-listing'; import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl'; import { SearchResultList } from './SearchResultList'; import styles from './SearchPageBase.module.scss'; // https://regex101.com/r/brrkBu/1 const highlightKeywordsSplitter = new RegExp('"[^"]+"|[^\u{20}\u{3000}]+', 'ug'); export interface IReturnSelectedPageIds { getSelectedPageIds?: () => Set, } type Props = { pages?: IPageWithSearchMeta[], searchingKeyword?: string, forceHideMenuItems?: ForceHideMenuItems, onSelectedPagesByCheckboxesChanged?: (selectedCount: number, totalCount: number) => void, searchControl: React.ReactNode, searchResultListHead: React.ReactElement, searchPager: React.ReactNode, } const SearchResultContent = dynamic(() => import('./SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false, loading: () => <>, }); const SearchPageBaseSubstance: ForwardRefRenderFunction = (props:Props, ref) => { const { pages, searchingKeyword, forceHideMenuItems, onSelectedPagesByCheckboxesChanged, searchControl, searchResultListHead, searchPager, } = props; const searchResultListRef = useRef(null); const { data: isGuestUser } = useIsGuestUser(); const { data: isReadOnlyUser } = useIsReadOnlyUser(); const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured(); const { data: isSearchServiceReachable } = useIsSearchServiceReachable(); const [selectedPageIdsByCheckboxes] = useState>(new Set()); // const [allPageIds] = useState>(new Set()); const [selectedPageWithMeta, setSelectedPageWithMeta] = useState(); // publish selectAll() useImperativeHandle(ref, () => ({ selectAll: () => { const instance = searchResultListRef.current; if (instance != null) { instance.selectAll(); } if (pages != null) { pages.forEach(page => selectedPageIdsByCheckboxes.add(page.data._id)); } }, deselectAll: () => { const instance = searchResultListRef.current; if (instance != null) { instance.deselectAll(); } selectedPageIdsByCheckboxes.clear(); }, getSelectedPageIds: () => { return selectedPageIdsByCheckboxes; }, })); const checkboxChangedHandler = (isChecked: boolean, pageId: string) => { if (pages == null || pages.length === 0) { return; } if (isChecked) { selectedPageIdsByCheckboxes.add(pageId); } else { selectedPageIdsByCheckboxes.delete(pageId); } if (onSelectedPagesByCheckboxesChanged != null) { onSelectedPagesByCheckboxesChanged(selectedPageIdsByCheckboxes.size, pages.length); } }; // select first item on load useEffect(() => { if ((pages == null || pages.length === 0)) { setSelectedPageWithMeta(undefined); } else if ((pages != null && pages.length > 0)) { setSelectedPageWithMeta(pages[0]); } }, [pages, setSelectedPageWithMeta]); // reset selectedPageIdsByCheckboxes useEffect(() => { if (pages == null) { return; } if (pages.length > 0) { selectedPageIdsByCheckboxes.clear(); } if (onSelectedPagesByCheckboxesChanged != null) { onSelectedPagesByCheckboxesChanged(selectedPageIdsByCheckboxes.size, pages.length); } }, [onSelectedPagesByCheckboxesChanged, pages, selectedPageIdsByCheckboxes]); if (!isSearchServiceConfigured) { return (

Search service is not configured in this system.

); } if (!isSearchServiceReachable) { return (

Search service occures errors. Please contact to administrators of this system.

); } const highlightKeywords = searchingKeyword != null // Remove double quotation marks before and after a keyword if present // https://regex101.com/r/4QKBwg/1 ? searchingKeyword.match(highlightKeywordsSplitter)?.map(keyword => keyword.replace(/^"(.*)"$/, '$1')) ?? undefined : undefined; return (
{searchControl}
{/* Loading */} { pages == null && (
) } {/* Loaded */} { pages != null && ( <>
{searchResultListHead}
{ pages.length > 0 && (
(setSelectedPageWithMeta(page)) } onCheckboxChanged={checkboxChangedHandler} />
) }
{searchPager}
) }
{pages != null && pages.length !== 0 && selectedPageWithMeta != null && ( )}
); }; type VoidFunction = () => void; export const usePageDeleteModalForBulkDeletion = ( data: IFormattedSearchResult | undefined, ref: React.MutableRefObject<(ISelectableAll & IReturnSelectedPageIds) | null>, onDeleted?: OnDeletedFunction, ): VoidFunction => { const { t } = useTranslation(); const { open: openDeleteModal } = usePageDeleteModal(); return () => { if (data == null) { return; } const instance = ref.current; if (instance == null || instance.getSelectedPageIds == null) { return; } const selectedPageIds = instance.getSelectedPageIds(); if (selectedPageIds.size === 0) { return; } const selectedPages = data.data .filter(pageWithMeta => selectedPageIds.has(pageWithMeta.data._id)); openDeleteModal(selectedPages, { onDeleted: (...args) => { const path = args[0]; const isCompletely = args[2]; if (path == null || isCompletely == null) { toastSuccess(t('deleted_page')); } else { toastSuccess(t('deleted_pages_completely', { path })); } mutatePageTree(); if (onDeleted != null) { onDeleted(...args); } }, }); }; }; export const SearchPageBase = forwardRef(SearchPageBaseSubstance);