PageSideContents.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import React, {
  2. Suspense,
  3. useCallback,
  4. useRef,
  5. type JSX,
  6. } from 'react';
  7. import type { IPagePopulatedToShowRevision } from '@growi/core';
  8. import { isIPageInfoForOperation } from '@growi/core/dist/interfaces';
  9. import { pagePathUtils } from '@growi/core/dist/utils';
  10. import { useAtomValue } from 'jotai';
  11. import { useTranslation } from 'next-i18next';
  12. import dynamic from 'next/dynamic';
  13. import { scroller } from 'react-scroll';
  14. import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
  15. import { showPageSideAuthorsAtom } from '~/states/server-configurations';
  16. import { useDescendantsPageListModalActions } from '~/states/ui/modal/descendants-page-list';
  17. import { useTagEditModalActions } from '~/states/ui/modal/tag-edit';
  18. import { useIsAbleToShowTagLabel } from '~/states/ui/page-abilities';
  19. import { useSWRxPageInfo, useSWRxTagsInfo } from '~/stores/page';
  20. import { ContentLinkButtons } from '../ContentLinkButtons';
  21. import { PageTagsSkeleton } from '../PageTags';
  22. import TableOfContents from '../TableOfContents';
  23. import { PageAccessoriesControl } from './PageAccessoriesControl';
  24. const { isTopPage, isUsersHomepage, isTrashPage } = pagePathUtils;
  25. const PageTags = dynamic(() => import('../PageTags').then(mod => mod.PageTags), {
  26. ssr: false,
  27. loading: PageTagsSkeleton,
  28. });
  29. const AuthorInfo = dynamic(() => import('~/client/components/AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
  30. type TagsProps = {
  31. pageId: string,
  32. revisionId: string,
  33. }
  34. const Tags = (props: TagsProps): JSX.Element => {
  35. const { pageId, revisionId } = props;
  36. const { data: tagsInfoData } = useSWRxTagsInfo(pageId, { suspense: true });
  37. const showTagLabel = useIsAbleToShowTagLabel();
  38. const isGuestUser = useIsGuestUser();
  39. const isReadOnlyUser = useIsReadOnlyUser();
  40. const { open: openTagEditModal } = useTagEditModalActions();
  41. const onClickEditTagsButton = useCallback(() => {
  42. if (tagsInfoData == null) {
  43. return;
  44. }
  45. openTagEditModal(tagsInfoData.tags, pageId, revisionId);
  46. }, [pageId, revisionId, tagsInfoData, openTagEditModal]);
  47. if (!showTagLabel || tagsInfoData == null) {
  48. return <></>;
  49. }
  50. const isTagLabelsDisabled = !!isGuestUser || !!isReadOnlyUser;
  51. return (
  52. <div className="grw-tag-labels-container">
  53. <PageTags
  54. tags={tagsInfoData.tags}
  55. isTagLabelsDisabled={isTagLabelsDisabled}
  56. onClickEditTagsButton={onClickEditTagsButton}
  57. />
  58. </div>
  59. );
  60. };
  61. type PageSideContentsProps = {
  62. page: IPagePopulatedToShowRevision,
  63. isSharedUser?: boolean,
  64. }
  65. export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
  66. const { t } = useTranslation();
  67. const { open: openDescendantPageListModal } = useDescendantsPageListModalActions();
  68. const { page, isSharedUser } = props;
  69. const tagsRef = useRef<HTMLDivElement>(null);
  70. const { data: pageInfo } = useSWRxPageInfo(page._id);
  71. const showPageSideAuthors = useAtomValue(showPageSideAuthorsAtom);
  72. const {
  73. creator, lastUpdateUser, createdAt, updatedAt,
  74. } = page;
  75. const pagePath = page.path;
  76. const isTopPagePath = isTopPage(pagePath);
  77. const isUsersHomepagePath = isUsersHomepage(pagePath);
  78. const isTrash = isTrashPage(pagePath);
  79. return (
  80. <>
  81. {/* AuthorInfo */}
  82. {showPageSideAuthors && (
  83. <div className="d-none d-md-block page-meta border-bottom pb-2 ms-lg-3 mb-3">
  84. <AuthorInfo user={creator} date={createdAt} mode="create" locate="pageSide" />
  85. <AuthorInfo user={lastUpdateUser} date={updatedAt} mode="update" locate="pageSide" />
  86. </div>
  87. )}
  88. {/* Tags */}
  89. {page.revision != null && (
  90. <div ref={tagsRef}>
  91. <Suspense fallback={<PageTagsSkeleton />}>
  92. <Tags pageId={page._id} revisionId={page.revision._id} />
  93. </Suspense>
  94. </div>
  95. )}
  96. <div className=" d-flex flex-column gap-2">
  97. {/* Page list */}
  98. {!isSharedUser && (
  99. <div className="d-flex" data-testid="pageListButton">
  100. <PageAccessoriesControl
  101. icon={<span className="material-symbols-outlined">subject</span>}
  102. label={t('page_list')}
  103. // Do not display CountBadge if '/trash/*': https://github.com/growilabs/growi/pull/7600
  104. count={!isTrash && isIPageInfoForOperation(pageInfo) ? pageInfo.descendantCount : undefined}
  105. offset={1}
  106. onClick={() => openDescendantPageListModal(pagePath)}
  107. />
  108. </div>
  109. )}
  110. {/* Comments */}
  111. {!isTopPagePath && (
  112. <div className="d-flex" data-testid="page-comment-button">
  113. <PageAccessoriesControl
  114. icon={<span className="material-symbols-outlined">chat</span>}
  115. label={t('comments')}
  116. count={isIPageInfoForOperation(pageInfo) ? pageInfo.commentCount : undefined}
  117. onClick={() => scroller.scrollTo('comments-container', { smooth: false, offset: -120 })}
  118. />
  119. </div>
  120. )}
  121. </div>
  122. <div className="d-none d-xl-block">
  123. <TableOfContents tagsElementHeight={tagsRef.current?.clientHeight} />
  124. {isUsersHomepagePath && <ContentLinkButtons author={page?.creator} />}
  125. </div>
  126. </>
  127. );
  128. };