import React, { forwardRef, useState, ForwardRefRenderFunction, memo, useCallback, useImperativeHandle, useRef, useEffect, } from 'react'; import type { IPageInfoAll, IPageWithMeta, IPageInfoForListing, } from '@growi/core'; import { isIPageInfoForListing, isIPageInfoForEntity } from '@growi/core'; import { DevidedPagePath } from '@growi/core/dist/models'; import { pathUtils } from '@growi/core/dist/utils'; import { UserPicture } from '@growi/ui/dist/components'; import { PageListMeta } from '@growi/ui/dist/components/PagePath'; import { format } from 'date-fns'; import { useTranslation } from 'next-i18next'; import Link from 'next/link'; import Clamp from 'react-multiline-clamp'; import { Input } from 'reactstrap'; import { ISelectable } from '~/client/interfaces/selectable-all'; import { unlink, bookmark, unbookmark } from '~/client/services/page-operation'; import { toastError } from '~/client/util/toastr'; import { IPageSearchMeta, IPageWithSearchMeta, isIPageSearchMeta } from '~/interfaces/search'; import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction, } from '~/interfaces/ui'; import LinkedPagePath from '~/models/linked-page-path'; import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark'; import { usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal, } from '~/stores/modal'; import { useIsDeviceSmallerThanLg } from '~/stores/ui'; import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../stores/page'; import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl'; import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink'; type Props = { page: IPageWithSearchMeta | IPageWithMeta, isSelected?: boolean, // is item selected(focused) isEnableActions?: boolean, isReadOnlyUser: boolean, forceHideMenuItems?: ForceHideMenuItems, showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item onCheckboxChanged?: (isChecked: boolean, pageId: string) => void, onClickItem?: (pageId: string) => void, onPageDuplicated?: OnDuplicatedFunction, onPageRenamed?: OnRenamedFunction, onPageDeleted?: OnDeletedFunction, onPagePutBacked?: OnPutBackedFunction, } const PageListItemLSubstance: ForwardRefRenderFunction = (props: Props, ref): JSX.Element => { const { page: { data: pageData, meta: pageMeta }, isSelected, isEnableActions, isReadOnlyUser, forceHideMenuItems, showPageUpdatedTime, onClickItem, onCheckboxChanged, onPageDuplicated, onPageRenamed, onPageDeleted, onPagePutBacked, } = props; const { returnPathForURL } = pathUtils; const [likerCount, setLikerCount] = useState(pageData.liker.length); const [bookmarkCount, setBookmarkCount] = useState(pageMeta && pageMeta.bookmarkCount ? pageMeta.bookmarkCount : 0); const { t } = useTranslation(); const inputRef = useRef(null); // publish ISelectable methods useImperativeHandle(ref, () => ({ select: () => { const input = inputRef.current; if (input != null) { input.checked = true; } }, deselect: () => { const input = inputRef.current; if (input != null) { input.checked = false; } }, })); const { data: isDeviceSmallerThanLg } = useIsDeviceSmallerThanLg(); const { open: openDuplicateModal } = usePageDuplicateModal(); const { open: openRenameModal } = usePageRenameModal(); const { open: openDeleteModal } = usePageDeleteModal(); const { open: openPutBackPageModal } = usePutBackPageModal(); const shouldFetch = isSelected && (pageData != null || pageMeta != null); const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? pageData?._id : null); const { trigger: mutatePageInfo } = useSWRMUTxPageInfo(pageData?._id ?? null); const { trigger: mutateCurrentUserBookmarks } = useSWRMUTxCurrentUserBookmarks(); const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null; const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null; const dPagePath: DevidedPagePath = new DevidedPagePath(pageData.path, false); const linkedPagePathFormer = new LinkedPagePath(dPagePath.former); const dPagePathHighlighted: DevidedPagePath = new DevidedPagePath(elasticSearchResult?.highlightedPath || pageData.path, true); const linkedPagePathHighlightedFormer = new LinkedPagePath(dPagePathHighlighted.former); const linkedPagePathHighlightedLatter = new LinkedPagePath(dPagePathHighlighted.latter); const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss'); useEffect(() => { if (isIPageInfoForEntity(pageInfo)) { // likerCount setLikerCount(pageInfo.likerIds?.length ?? 0); // bookmarkCount setBookmarkCount(pageInfo.bookmarkCount ?? 0); } }, [pageInfo]); // click event handler const clickHandler = useCallback(() => { // do nothing if mobile if (isDeviceSmallerThanLg) { return; } if (onClickItem != null) { onClickItem(pageData._id); } }, [isDeviceSmallerThanLg, onClickItem, pageData._id]); const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise => { const bookmarkOperation = _newValue ? bookmark : unbookmark; await bookmarkOperation(_pageId); mutateCurrentUserBookmarks(); mutatePageInfo(); }; const duplicateMenuItemClickHandler = useCallback(() => { const page = { pageId: pageData._id, path: pageData.path, }; openDuplicateModal(page, { onDuplicated: onPageDuplicated }); }, [onPageDuplicated, openDuplicateModal, pageData._id, pageData.path]); const renameMenuItemClickHandler = useCallback((_id: string, pageInfo: IPageInfoAll | undefined) => { const page = { data: pageData, meta: pageInfo }; openRenameModal(page, { onRenamed: onPageRenamed }); }, [pageData, onPageRenamed, openRenameModal]); const deleteMenuItemClickHandler = useCallback((_id: string, pageInfo: IPageInfoAll | undefined) => { const pageToDelete = { data: pageData, meta: pageInfo }; // open modal openDeleteModal([pageToDelete], { onDeleted: onPageDeleted }); }, [pageData, openDeleteModal, onPageDeleted]); const revertMenuItemClickHandler = useCallback(async() => { const { _id: pageId, path } = pageData; const putBackedHandler = async(path) => { try { // pageData path should be `/trash/fuga` (`/trash` should be included to the prefix) await unlink(pageData.path); } catch (err) { toastError(err); } if (onPagePutBacked != null) { // This path should be `/fuga` ( `/trash` is not included to the prefix) onPagePutBacked(path); } }; openPutBackPageModal({ pageId, path }, { onPutBacked: putBackedHandler }); }, [onPagePutBacked, openPutBackPageModal, pageData]); const styleListGroupItem = (!isDeviceSmallerThanLg && onClickItem != null) ? 'list-group-item-action' : ''; // background color of list item changes when class "active" exists under 'list-group-item' const styleActive = !isDeviceSmallerThanLg && isSelected ? 'active' : ''; const shouldDangerouslySetInnerHTMLForPaths = elasticSearchResult != null && elasticSearchResult.highlightedPath != null; const canRenderESSnippet = elasticSearchResult != null && elasticSearchResult.snippet != null; const canRenderRevisionSnippet = revisionShortBody != null; const hasBrowsingRights = canRenderESSnippet || canRenderRevisionSnippet; return (
  • {/* checkbox */} {onCheckboxChanged != null && (
    { onCheckboxChanged(e.target.checked, pageData._id) }} />
    )}
    {/* page path */} {showPageUpdatedTime && ( Last update: {lastUpdateDate} )}
    {/* Picture */} {/* page title */} {/* Use permanent links to care for pages with the same name (Cannot use page path url) */} {shouldDangerouslySetInnerHTMLForPaths ? ( ) : {linkedPagePathHighlightedLatter.pathName} } {/* page meta */}
    {/* doropdown icon includes page control buttons */} {hasBrowsingRights && (
    ) }
    {elasticSearchResult != null && elasticSearchResult.snippet != null && ( // eslint-disable-next-line react/no-danger
    )} {revisionShortBody != null && (
    {revisionShortBody}
    )} { !hasBrowsingRights && ( <> {t('not_allowed_to_see_this_page')} ) }
    {/* TODO: adjust snippet position */}
  • ); }; export const PageListItemL = memo(forwardRef(PageListItemLSubstance));