|
@@ -1,4 +1,9 @@
|
|
|
-import React, { memo, useCallback } from 'react';
|
|
|
|
|
|
|
+import React, {
|
|
|
|
|
+ forwardRef,
|
|
|
|
|
+ ForwardRefRenderFunction, memo, useCallback, useImperativeHandle, useRef,
|
|
|
|
|
+} from 'react';
|
|
|
|
|
+
|
|
|
|
|
+import { CustomInput } from 'reactstrap';
|
|
|
|
|
|
|
|
import Clamp from 'react-multiline-clamp';
|
|
import Clamp from 'react-multiline-clamp';
|
|
|
import { format } from 'date-fns';
|
|
import { format } from 'date-fns';
|
|
@@ -6,47 +11,83 @@ import urljoin from 'url-join';
|
|
|
|
|
|
|
|
import { UserPicture, PageListMeta } from '@growi/ui';
|
|
import { UserPicture, PageListMeta } from '@growi/ui';
|
|
|
import { DevidedPagePath } from '@growi/core';
|
|
import { DevidedPagePath } from '@growi/core';
|
|
|
-import { useIsDeviceSmallerThanLg, usePageRenameModalStatus, usePageDuplicateModalStatus } from '~/stores/ui';
|
|
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+import { ISelectable } from '~/client/interfaces/selectable-all';
|
|
|
|
|
+import { bookmark, unbookmark } from '~/client/services/page-operation';
|
|
|
|
|
+import { useIsDeviceSmallerThanLg } from '~/stores/ui';
|
|
|
import {
|
|
import {
|
|
|
- IPageInfoAll, IPageWithMeta, isIPageInfoForEntity, isIPageInfoForListing,
|
|
|
|
|
|
|
+ usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
|
|
|
|
|
+} from '~/stores/modal';
|
|
|
|
|
+import {
|
|
|
|
|
+ IPageInfoAll, IPageInfoForEntity, IPageInfoForListing, IPageWithMeta, isIPageInfoForListing,
|
|
|
} from '~/interfaces/page';
|
|
} from '~/interfaces/page';
|
|
|
import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
|
|
import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
|
|
|
-
|
|
|
|
|
-import { PageItemControl } from '../Common/Dropdown/PageItemControl';
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
|
|
|
|
|
+} from '~/interfaces/ui';
|
|
|
import LinkedPagePath from '~/models/linked-page-path';
|
|
import LinkedPagePath from '~/models/linked-page-path';
|
|
|
|
|
+
|
|
|
|
|
+import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
|
|
|
import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
|
|
import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
|
|
|
|
|
|
|
|
type Props = {
|
|
type Props = {
|
|
|
- page: IPageWithMeta | IPageWithMeta<IPageInfoAll & IPageSearchMeta>,
|
|
|
|
|
|
|
+ page: IPageWithMeta<IPageInfoForEntity> | IPageWithMeta<IPageSearchMeta> | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,
|
|
|
isSelected?: boolean, // is item selected(focused)
|
|
isSelected?: boolean, // is item selected(focused)
|
|
|
- isChecked?: boolean, // is checkbox of item checked
|
|
|
|
|
isEnableActions?: boolean,
|
|
isEnableActions?: boolean,
|
|
|
|
|
+ forceHideMenuItems?: ForceHideMenuItems,
|
|
|
showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
|
|
showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
|
|
|
- onClickCheckbox?: (pageId: string) => void,
|
|
|
|
|
|
|
+ onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
|
|
|
onClickItem?: (pageId: string) => void,
|
|
onClickItem?: (pageId: string) => void,
|
|
|
- onClickDeleteButton?: (pageId: string) => void,
|
|
|
|
|
|
|
+ onPageDuplicated?: OnDuplicatedFunction,
|
|
|
|
|
+ onPageRenamed?: OnRenamedFunction,
|
|
|
|
|
+ onPageDeleted?: OnDeletedFunction,
|
|
|
|
|
+ onPagePutBacked?: OnPutBackedFunction,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export const PageListItemL = memo((props: Props): JSX.Element => {
|
|
|
|
|
|
|
+const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
|
|
|
const {
|
|
const {
|
|
|
// todo: refactoring variable name to clear what changed
|
|
// todo: refactoring variable name to clear what changed
|
|
|
- page: { pageData, pageMeta }, isSelected, onClickItem, onClickCheckbox, isChecked, isEnableActions,
|
|
|
|
|
|
|
+ page: { data: pageData, meta: pageMeta }, isSelected, isEnableActions,
|
|
|
|
|
+ forceHideMenuItems,
|
|
|
showPageUpdatedTime,
|
|
showPageUpdatedTime,
|
|
|
|
|
+ onClickItem, onCheckboxChanged, onPageDuplicated, onPageRenamed, onPageDeleted, onPagePutBacked,
|
|
|
} = props;
|
|
} = props;
|
|
|
|
|
|
|
|
|
|
+ const inputRef = useRef<HTMLInputElement>(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 { data: isDeviceSmallerThanLg } = useIsDeviceSmallerThanLg();
|
|
|
- const { open: openDuplicateModal } = usePageDuplicateModalStatus();
|
|
|
|
|
- const { open: openRenameModal } = usePageRenameModalStatus();
|
|
|
|
|
|
|
+ const { open: openDuplicateModal } = usePageDuplicateModal();
|
|
|
|
|
+ const { open: openRenameModal } = usePageRenameModal();
|
|
|
|
|
+ const { open: openDeleteModal } = usePageDeleteModal();
|
|
|
|
|
+ const { open: openPutBackPageModal } = usePutBackPageModal();
|
|
|
|
|
|
|
|
const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
|
|
const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
|
|
|
const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
|
|
const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
|
|
|
|
|
|
|
|
- const dPagePath: DevidedPagePath = new DevidedPagePath(pageData.path, true);
|
|
|
|
|
|
|
+ const dPagePath: DevidedPagePath = new DevidedPagePath(elasticSearchResult?.highlightedPath || pageData.path, true);
|
|
|
const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
|
|
const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
|
|
|
const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
|
|
const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
|
|
|
|
|
|
|
|
const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
|
|
const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
|
|
|
|
|
|
|
|
|
|
+
|
|
|
// click event handler
|
|
// click event handler
|
|
|
const clickHandler = useCallback(() => {
|
|
const clickHandler = useCallback(() => {
|
|
|
// do nothing if mobile
|
|
// do nothing if mobile
|
|
@@ -59,20 +100,47 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
|
|
|
}
|
|
}
|
|
|
}, [isDeviceSmallerThanLg, onClickItem, pageData._id]);
|
|
}, [isDeviceSmallerThanLg, onClickItem, pageData._id]);
|
|
|
|
|
|
|
|
|
|
+ const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
|
|
|
|
|
+ const bookmarkOperation = _newValue ? bookmark : unbookmark;
|
|
|
|
|
+ await bookmarkOperation(_pageId);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const duplicateMenuItemClickHandler = useCallback(() => {
|
|
const duplicateMenuItemClickHandler = useCallback(() => {
|
|
|
- const { _id: pageId, path } = pageData;
|
|
|
|
|
- openDuplicateModal(pageId, path);
|
|
|
|
|
- }, [openDuplicateModal, pageData]);
|
|
|
|
|
|
|
+ const page = {
|
|
|
|
|
+ pageId: pageData._id,
|
|
|
|
|
+ path: pageData.path,
|
|
|
|
|
+ };
|
|
|
|
|
+ openDuplicateModal(page, { onDuplicated: onPageDuplicated });
|
|
|
|
|
+ }, [onPageDuplicated, openDuplicateModal, pageData._id, pageData.path]);
|
|
|
|
|
|
|
|
const renameMenuItemClickHandler = useCallback(() => {
|
|
const renameMenuItemClickHandler = useCallback(() => {
|
|
|
- const { _id: pageId, revision: revisionId, path } = pageData;
|
|
|
|
|
- openRenameModal(pageId, revisionId as string, path);
|
|
|
|
|
- }, [openRenameModal, pageData]);
|
|
|
|
|
|
|
+ const page = {
|
|
|
|
|
+ pageId: pageData._id,
|
|
|
|
|
+ revisionId: pageData.revision as string,
|
|
|
|
|
+ path: pageData.path,
|
|
|
|
|
+ };
|
|
|
|
|
+ openRenameModal(page, { onRenamed: onPageRenamed });
|
|
|
|
|
+ }, [onPageRenamed, openRenameModal, pageData._id, pageData.path, pageData.revision]);
|
|
|
|
|
+
|
|
|
|
|
|
|
|
- const styleListGroupItem = (!isDeviceSmallerThanLg && onClickCheckbox != null) ? 'list-group-item-action' : '';
|
|
|
|
|
|
|
+ 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(() => {
|
|
|
|
|
+ const { _id: pageId, path } = pageData;
|
|
|
|
|
+ openPutBackPageModal({ pageId, path }, { onPutBacked: onPagePutBacked });
|
|
|
|
|
+ }, [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'
|
|
// background color of list item changes when class "active" exists under 'list-group-item'
|
|
|
const styleActive = !isDeviceSmallerThanLg && isSelected ? 'active' : '';
|
|
const styleActive = !isDeviceSmallerThanLg && isSelected ? 'active' : '';
|
|
|
|
|
|
|
|
|
|
+ const shouldDangerouslySetInnerHTMLForPaths = elasticSearchResult != null && elasticSearchResult.highlightedPath.length > 0;
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<li
|
|
<li
|
|
|
key={pageData._id}
|
|
key={pageData._id}
|
|
@@ -84,14 +152,14 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
|
|
|
>
|
|
>
|
|
|
<div className="d-flex">
|
|
<div className="d-flex">
|
|
|
{/* checkbox */}
|
|
{/* checkbox */}
|
|
|
- {onClickCheckbox != null && (
|
|
|
|
|
- <div className="form-check d-flex align-items-center justify-content-center px-md-2 pl-3 pr-2 search-item-checkbox">
|
|
|
|
|
- <input
|
|
|
|
|
- className="form-check-input position-relative m-0"
|
|
|
|
|
|
|
+ {onCheckboxChanged != null && (
|
|
|
|
|
+ <div className="d-flex align-items-center justify-content-center pl-md-2 pl-3">
|
|
|
|
|
+ <CustomInput
|
|
|
type="checkbox"
|
|
type="checkbox"
|
|
|
- id="flexCheckDefault"
|
|
|
|
|
- onChange={() => { onClickCheckbox(pageData._id) }}
|
|
|
|
|
- checked={isChecked}
|
|
|
|
|
|
|
+ id={`cbSelect-${pageData._id}`}
|
|
|
|
|
+ data-testid="cb-select"
|
|
|
|
|
+ innerRef={inputRef}
|
|
|
|
|
+ onChange={(e) => { onCheckboxChanged(e.target.checked, pageData._id) }}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
@@ -99,7 +167,10 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
|
|
|
<div className="flex-grow-1 p-md-3 pl-2 py-3 pr-3">
|
|
<div className="flex-grow-1 p-md-3 pl-2 py-3 pr-3">
|
|
|
<div className="d-flex justify-content-between">
|
|
<div className="d-flex justify-content-between">
|
|
|
{/* page path */}
|
|
{/* page path */}
|
|
|
- <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
|
|
|
|
|
|
|
+ <PagePathHierarchicalLink
|
|
|
|
|
+ linkedPagePath={linkedPagePathFormer}
|
|
|
|
|
+ shouldDangerouslySetInnerHTML={shouldDangerouslySetInnerHTMLForPaths}
|
|
|
|
|
+ />
|
|
|
{ showPageUpdatedTime && (
|
|
{ showPageUpdatedTime && (
|
|
|
<span className="page-list-updated-at text-muted">Last update: {lastUpdateDate}</span>
|
|
<span className="page-list-updated-at text-muted">Last update: {lastUpdateDate}</span>
|
|
|
) }
|
|
) }
|
|
@@ -114,27 +185,39 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
|
|
|
<span className="h5 mb-0">
|
|
<span className="h5 mb-0">
|
|
|
{/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
|
|
{/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
|
|
|
<span className="grw-page-path-hierarchical-link text-break">
|
|
<span className="grw-page-path-hierarchical-link text-break">
|
|
|
- <a className="page-segment" href={encodeURI(urljoin('/', pageData._id))}>{linkedPagePathLatter.pathName}</a>
|
|
|
|
|
|
|
+ {shouldDangerouslySetInnerHTMLForPaths
|
|
|
|
|
+ ? (
|
|
|
|
|
+ <a
|
|
|
|
|
+ className="page-segment"
|
|
|
|
|
+ href={encodeURI(urljoin('/', pageData._id))}
|
|
|
|
|
+ // eslint-disable-next-line react/no-danger
|
|
|
|
|
+ dangerouslySetInnerHTML={{ __html: linkedPagePathLatter.pathName }}
|
|
|
|
|
+ >
|
|
|
|
|
+ </a>
|
|
|
|
|
+ )
|
|
|
|
|
+ : <a className="page-segment" href={encodeURI(urljoin('/', pageData._id))}>{linkedPagePathLatter.pathName}</a>
|
|
|
|
|
+ }
|
|
|
</span>
|
|
</span>
|
|
|
</span>
|
|
</span>
|
|
|
</Clamp>
|
|
</Clamp>
|
|
|
|
|
|
|
|
{/* page meta */}
|
|
{/* page meta */}
|
|
|
- { isIPageInfoForEntity(pageMeta) && (
|
|
|
|
|
- <div className="d-none d-md-flex py-0 px-1">
|
|
|
|
|
- <PageListMeta page={pageData} bookmarkCount={pageMeta.bookmarkCount} shouldSpaceOutIcon />
|
|
|
|
|
- </div>
|
|
|
|
|
- ) }
|
|
|
|
|
|
|
+ <div className="d-none d-md-flex py-0 px-1">
|
|
|
|
|
+ <PageListMeta page={pageData} bookmarkCount={pageMeta?.bookmarkCount} shouldSpaceOutIcon />
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
{/* doropdown icon includes page control buttons */}
|
|
{/* doropdown icon includes page control buttons */}
|
|
|
<div className="item-control ml-auto">
|
|
<div className="item-control ml-auto">
|
|
|
<PageItemControl
|
|
<PageItemControl
|
|
|
pageId={pageData._id}
|
|
pageId={pageData._id}
|
|
|
- pageInfo={pageMeta}
|
|
|
|
|
- onClickDeleteMenuItem={props.onClickDeleteButton}
|
|
|
|
|
- onClickRenameMenuItem={renameMenuItemClickHandler}
|
|
|
|
|
|
|
+ pageInfo={isIPageInfoForListing(pageMeta) ? pageMeta : undefined}
|
|
|
isEnableActions={isEnableActions}
|
|
isEnableActions={isEnableActions}
|
|
|
|
|
+ forceHideMenuItems={forceHideMenuItems}
|
|
|
|
|
+ onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
|
|
|
|
|
+ onClickRenameMenuItem={renameMenuItemClickHandler}
|
|
|
onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
|
|
onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
|
|
|
|
|
+ onClickDeleteMenuItem={deleteMenuItemClickHandler}
|
|
|
|
|
+ onClickRevertMenuItem={revertMenuItemClickHandler}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -155,4 +238,6 @@ export const PageListItemL = memo((props: Props): JSX.Element => {
|
|
|
</div>
|
|
</div>
|
|
|
</li>
|
|
</li>
|
|
|
);
|
|
);
|
|
|
-});
|
|
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export const PageListItemL = memo(forwardRef(PageListItemLSubstance));
|