PageSideContents.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import React, { Suspense, useCallback, useRef } from 'react';
  2. import type { IPagePopulatedToShowRevision } from '@growi/core';
  3. import { getIdForRef, type IPageInfoForOperation } from '@growi/core';
  4. import { pagePathUtils } from '@growi/core/dist/utils';
  5. import { useTranslation } from 'next-i18next';
  6. import dynamic from 'next/dynamic';
  7. import { scroller } from 'react-scroll';
  8. import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
  9. import { useDescendantsPageListModal, useTagEditModal } from '~/stores/modal';
  10. import { useSWRxPageInfo, useSWRxTagsInfo } from '~/stores/page';
  11. import { useIsAbleToShowTagLabel } from '~/stores/ui';
  12. import { ContentLinkButtons } from '../ContentLinkButtons';
  13. import { PageTagsSkeleton } from '../PageTags';
  14. import TableOfContents from '../TableOfContents';
  15. import { PageAccessoriesControl } from './PageAccessoriesControl';
  16. import styles from './PageSideContents.module.scss';
  17. const { isTopPage, isUsersHomepage, isTrashPage } = pagePathUtils;
  18. const PageTags = dynamic(() => import('../PageTags').then(mod => mod.PageTags), {
  19. ssr: false,
  20. loading: PageTagsSkeleton,
  21. });
  22. type TagsProps = {
  23. pageId: string,
  24. revisionId: string,
  25. }
  26. const Tags = (props: TagsProps): JSX.Element => {
  27. const { pageId, revisionId } = props;
  28. const { data: tagsInfoData } = useSWRxTagsInfo(pageId, { suspense: true });
  29. const { data: showTagLabel } = useIsAbleToShowTagLabel();
  30. const { data: isGuestUser } = useIsGuestUser();
  31. const { data: isReadOnlyUser } = useIsReadOnlyUser();
  32. const { open: openTagEditModal } = useTagEditModal();
  33. const onClickEditTagsButton = useCallback(() => {
  34. if (tagsInfoData == null) {
  35. return;
  36. }
  37. openTagEditModal(tagsInfoData.tags, pageId, revisionId);
  38. }, [pageId, revisionId, tagsInfoData, openTagEditModal]);
  39. if (!showTagLabel || tagsInfoData == null) {
  40. return <></>;
  41. }
  42. const isTagLabelsDisabled = !!isGuestUser || !!isReadOnlyUser;
  43. return (
  44. <div className="grw-tag-labels-container">
  45. <PageTags
  46. tags={tagsInfoData.tags}
  47. isTagLabelsDisabled={isTagLabelsDisabled}
  48. onClickEditTagsButton={onClickEditTagsButton}
  49. />
  50. </div>
  51. );
  52. };
  53. export type PageSideContentsProps = {
  54. page: IPagePopulatedToShowRevision,
  55. isSharedUser?: boolean,
  56. }
  57. export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
  58. const { t } = useTranslation();
  59. const { open: openDescendantPageListModal } = useDescendantsPageListModal();
  60. const { page, isSharedUser } = props;
  61. const tagsRef = useRef<HTMLDivElement>(null);
  62. const { data: pageInfo } = useSWRxPageInfo(page._id);
  63. const pagePath = page.path;
  64. const isTopPagePath = isTopPage(pagePath);
  65. const isUsersHomepagePath = isUsersHomepage(pagePath);
  66. const isTrash = isTrashPage(pagePath);
  67. return (
  68. <>
  69. {/* Tags */}
  70. { page.revision != null && (
  71. <div ref={tagsRef}>
  72. <Suspense fallback={<PageTagsSkeleton />}>
  73. <Tags pageId={page._id} revisionId={page.revision._id} />
  74. </Suspense>
  75. </div>
  76. ) }
  77. <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
  78. {/* Page list */}
  79. {!isSharedUser && (
  80. <div className="d-flex" data-testid="pageListButton">
  81. <PageAccessoriesControl
  82. icon={<span className="material-symbols-outlined">subject</span>}
  83. label={t('page_list')}
  84. // Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600
  85. count={!isTrash && pageInfo != null ? (pageInfo as IPageInfoForOperation).descendantCount : undefined}
  86. offset={1}
  87. onClick={() => openDescendantPageListModal(pagePath)}
  88. />
  89. </div>
  90. )}
  91. {/* Comments */}
  92. {!isTopPagePath && (
  93. <div className="d-flex" data-testid="page-comment-button">
  94. <PageAccessoriesControl
  95. icon={<span className="material-symbols-outlined">chat</span>}
  96. label={t('comments')}
  97. count={pageInfo != null ? (pageInfo as IPageInfoForOperation).commentCount : undefined}
  98. onClick={() => scroller.scrollTo('comments-container', { smooth: false, offset: -120 })}
  99. />
  100. </div>
  101. )}
  102. </div>
  103. <div className="d-none d-xl-block">
  104. <TableOfContents tagsElementHeight={tagsRef.current?.clientHeight} />
  105. {isUsersHomepagePath && <ContentLinkButtons author={page?.creator} />}
  106. </div>
  107. </>
  108. );
  109. };