2
0

PageSideContents.tsx 5.1 KB

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