PageListItemL.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import React, {
  2. forwardRef,
  3. ForwardRefRenderFunction, memo, useCallback, useImperativeHandle, useRef,
  4. } from 'react';
  5. import { CustomInput } from 'reactstrap';
  6. import Clamp from 'react-multiline-clamp';
  7. import { format } from 'date-fns';
  8. import urljoin from 'url-join';
  9. import { UserPicture, PageListMeta } from '@growi/ui';
  10. import { DevidedPagePath } from '@growi/core';
  11. import { useIsDeviceSmallerThanLg } from '~/stores/ui';
  12. import { usePageRenameModal, usePageDuplicateModal } from '~/stores/modal';
  13. import {
  14. IPageInfoAll, IPageWithMeta, isIPageInfoForEntity, isIPageInfoForListing,
  15. } from '~/interfaces/page';
  16. import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
  17. import { PageItemControl } from '../Common/Dropdown/PageItemControl';
  18. import LinkedPagePath from '~/models/linked-page-path';
  19. import PagePathHierarchicalLink from '../PagePathHierarchicalLink';
  20. import { ISelectable } from '~/client/interfaces/selectable-all';
  21. type Props = {
  22. page: IPageWithMeta | IPageWithMeta<IPageInfoAll & IPageSearchMeta>,
  23. isSelected?: boolean, // is item selected(focused)
  24. isEnableActions?: boolean,
  25. showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
  26. onCheckboxChanged?: (isChecked: boolean, pageId: string) => void,
  27. onClickItem?: (pageId: string) => void,
  28. onClickDeleteButton?: (pageId: string) => void,
  29. }
  30. const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (props: Props, ref): JSX.Element => {
  31. const {
  32. // todo: refactoring variable name to clear what changed
  33. page: { pageData, pageMeta }, isSelected, isEnableActions,
  34. showPageUpdatedTime,
  35. onClickItem, onCheckboxChanged,
  36. } = props;
  37. const inputRef = useRef<HTMLInputElement>(null);
  38. // publish ISelectable methods
  39. useImperativeHandle(ref, () => ({
  40. select: () => {
  41. const input = inputRef.current;
  42. if (input != null) {
  43. input.checked = true;
  44. }
  45. },
  46. deselect: () => {
  47. const input = inputRef.current;
  48. if (input != null) {
  49. input.checked = false;
  50. }
  51. },
  52. }));
  53. const { data: isDeviceSmallerThanLg } = useIsDeviceSmallerThanLg();
  54. const { open: openDuplicateModal } = usePageDuplicateModal();
  55. const { open: openRenameModal } = usePageRenameModal();
  56. const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
  57. const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
  58. const dPagePath: DevidedPagePath = new DevidedPagePath(pageData.path, true);
  59. const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
  60. const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
  61. const lastUpdateDate = format(new Date(pageData.updatedAt), 'yyyy/MM/dd HH:mm:ss');
  62. // click event handler
  63. const clickHandler = useCallback(() => {
  64. // do nothing if mobile
  65. if (isDeviceSmallerThanLg) {
  66. return;
  67. }
  68. if (onClickItem != null) {
  69. onClickItem(pageData._id);
  70. }
  71. }, [isDeviceSmallerThanLg, onClickItem, pageData._id]);
  72. const duplicateMenuItemClickHandler = useCallback(() => {
  73. const { _id: pageId, path } = pageData;
  74. openDuplicateModal(pageId, path);
  75. }, [openDuplicateModal, pageData]);
  76. const renameMenuItemClickHandler = useCallback(() => {
  77. const { _id: pageId, revision: revisionId, path } = pageData;
  78. openRenameModal(pageId, revisionId as string, path);
  79. }, [openRenameModal, pageData]);
  80. const styleListGroupItem = (!isDeviceSmallerThanLg && onClickItem != null) ? 'list-group-item-action' : '';
  81. // background color of list item changes when class "active" exists under 'list-group-item'
  82. const styleActive = !isDeviceSmallerThanLg && isSelected ? 'active' : '';
  83. return (
  84. <li
  85. key={pageData._id}
  86. className={`list-group-item p-0 ${styleListGroupItem} ${styleActive}`}
  87. >
  88. <div
  89. className="text-break"
  90. onClick={clickHandler}
  91. >
  92. <div className="d-flex">
  93. {/* checkbox */}
  94. {onCheckboxChanged != null && (
  95. <div className="d-flex align-items-center justify-content-center pl-md-2 pl-3">
  96. <CustomInput
  97. type="checkbox"
  98. id={`cbSelect-${pageData._id}`}
  99. data-testid="cb-select"
  100. innerRef={inputRef}
  101. onChange={(e) => { onCheckboxChanged(e.target.checked, pageData._id) }}
  102. />
  103. </div>
  104. )}
  105. <div className="flex-grow-1 p-md-3 pl-2 py-3 pr-3">
  106. <div className="d-flex justify-content-between">
  107. {/* page path */}
  108. <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
  109. { showPageUpdatedTime && (
  110. <span className="page-list-updated-at text-muted">Last update: {lastUpdateDate}</span>
  111. ) }
  112. </div>
  113. <div className="d-flex align-items-center mb-1">
  114. {/* Picture */}
  115. <span className="mr-2 d-none d-md-block">
  116. <UserPicture user={pageData.lastUpdateUser} size="md" />
  117. </span>
  118. {/* page title */}
  119. <Clamp lines={1}>
  120. <span className="h5 mb-0">
  121. {/* Use permanent links to care for pages with the same name (Cannot use page path url) */}
  122. <span className="grw-page-path-hierarchical-link text-break">
  123. <a className="page-segment" href={encodeURI(urljoin('/', pageData._id))}>{linkedPagePathLatter.pathName}</a>
  124. </span>
  125. </span>
  126. </Clamp>
  127. {/* page meta */}
  128. { isIPageInfoForEntity(pageMeta) && (
  129. <div className="d-none d-md-flex py-0 px-1">
  130. <PageListMeta page={pageData} bookmarkCount={pageMeta.bookmarkCount} shouldSpaceOutIcon />
  131. </div>
  132. ) }
  133. {/* doropdown icon includes page control buttons */}
  134. <div className="item-control ml-auto">
  135. <PageItemControl
  136. pageId={pageData._id}
  137. pageInfo={pageMeta}
  138. onClickDeleteMenuItem={props.onClickDeleteButton}
  139. onClickRenameMenuItem={renameMenuItemClickHandler}
  140. isEnableActions={isEnableActions}
  141. onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
  142. />
  143. </div>
  144. </div>
  145. <div className="page-list-snippet py-1">
  146. <Clamp lines={2}>
  147. { elasticSearchResult != null && elasticSearchResult?.snippet.length > 0 && (
  148. // eslint-disable-next-line react/no-danger
  149. <div dangerouslySetInnerHTML={{ __html: elasticSearchResult.snippet }}></div>
  150. ) }
  151. { revisionShortBody != null && (
  152. <div>{revisionShortBody}</div>
  153. ) }
  154. </Clamp>
  155. </div>
  156. </div>
  157. </div>
  158. {/* TODO: adjust snippet position */}
  159. </div>
  160. </li>
  161. );
  162. };
  163. export const PageListItemL = memo(forwardRef(PageListItemLSubstance));