PageListItemL.tsx 6.4 KB

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