PageControls.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import React, { memo, useCallback, useMemo } from 'react';
  2. import type {
  3. IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta,
  4. } from '@growi/core';
  5. import {
  6. isIPageInfoForEntity, isIPageInfoForOperation,
  7. } from '@growi/core';
  8. import { useTranslation } from 'next-i18next';
  9. import { DropdownItem } from 'reactstrap';
  10. import {
  11. toggleLike, toggleSubscribe,
  12. } from '~/client/services/page-operation';
  13. import { toastError } from '~/client/util/toastr';
  14. import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
  15. import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
  16. import { EditorMode, useEditorMode } from '~/stores/ui';
  17. import loggerFactory from '~/utils/logger';
  18. import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
  19. import { useSWRxUsersList } from '../../stores/user';
  20. import {
  21. AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType,
  22. PageItemControl,
  23. } from '../Common/Dropdown/PageItemControl';
  24. import { BookmarkButtons } from './BookmarkButtons';
  25. import LikeButtons from './LikeButtons';
  26. import SeenUserInfo from './SeenUserInfo';
  27. import SubscribeButton from './SubscribeButton';
  28. import styles from './PageControls.module.scss';
  29. const logger = loggerFactory('growi:components/PageControls');
  30. type TagsProps = {
  31. onClickEditTagsButton: () => void,
  32. }
  33. const Tags = (props: TagsProps): JSX.Element => {
  34. const { onClickEditTagsButton } = props;
  35. return (
  36. <div className="grw-taglabels-container d-flex align-items-center">
  37. <button
  38. type="button"
  39. className="btn btn-link btn-edit-tags text-muted border border-secondary p-1 d-flex align-items-center"
  40. onClick={onClickEditTagsButton}
  41. >
  42. <i className="icon-tag me-2" />
  43. Tags
  44. </button>
  45. </div>
  46. );
  47. };
  48. type WideViewMenuItemProps = AdditionalMenuItemsRendererProps & {
  49. onClickMenuItem: (newValue: boolean) => void,
  50. expandContentWidth?: boolean,
  51. }
  52. const WideViewMenuItem = (props: WideViewMenuItemProps): JSX.Element => {
  53. const { t } = useTranslation();
  54. const {
  55. onClickMenuItem, expandContentWidth,
  56. } = props;
  57. return (
  58. <DropdownItem
  59. onClick={() => onClickMenuItem(!(expandContentWidth))}
  60. className="grw-page-control-dropdown-item"
  61. >
  62. <div className="form-check form-switch ms-1">
  63. <input
  64. id="switchContentWidth"
  65. className="form-check-input"
  66. type="checkbox"
  67. checked={expandContentWidth}
  68. onChange={() => {}}
  69. />
  70. <label className="form-label form-check-label" htmlFor="switchContentWidth">
  71. { t('wide_view') }
  72. </label>
  73. </div>
  74. </DropdownItem>
  75. );
  76. };
  77. type CommonProps = {
  78. disableSeenUserInfoPopover?: boolean,
  79. showPageControlDropdown?: boolean,
  80. forceHideMenuItems?: ForceHideMenuItems,
  81. additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
  82. onClickDuplicateMenuItem?: (pageToDuplicate: IPageForPageDuplicateModal) => void,
  83. onClickRenameMenuItem?: (pageToRename: IPageToRenameWithMeta) => void,
  84. onClickDeleteMenuItem?: (pageToDelete: IPageToDeleteWithMeta) => void,
  85. onClickSwitchContentWidth?: (pageId: string, value: boolean) => void,
  86. }
  87. type PageControlsSubstanceProps = CommonProps & {
  88. pageId: string,
  89. shareLinkId?: string | null,
  90. revisionId: string | null,
  91. path?: string | null,
  92. pageInfo: IPageInfoForOperation,
  93. expandContentWidth?: boolean,
  94. onClickEditTagsButton: () => void,
  95. }
  96. const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element => {
  97. const {
  98. pageInfo,
  99. pageId, revisionId, path, shareLinkId, expandContentWidth,
  100. disableSeenUserInfoPopover, showPageControlDropdown, forceHideMenuItems, additionalMenuItemRenderer,
  101. onClickEditTagsButton, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
  102. } = props;
  103. const { data: isGuestUser } = useIsGuestUser();
  104. const { data: isReadOnlyUser } = useIsReadOnlyUser();
  105. const { data: editorMode } = useEditorMode();
  106. const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
  107. const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
  108. const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
  109. // Put in a mixture of seenUserIds and likerIds data to make the cache work
  110. const { data: usersList } = useSWRxUsersList([...likerIds, ...seenUserIds]);
  111. const likers = usersList != null ? usersList.filter(({ _id }) => likerIds.includes(_id)).slice(0, 15) : [];
  112. const seenUsers = usersList != null ? usersList.filter(({ _id }) => seenUserIds.includes(_id)).slice(0, 15) : [];
  113. const subscribeClickhandler = useCallback(async() => {
  114. if (isGuestUser ?? true) {
  115. return;
  116. }
  117. if (!isIPageInfoForOperation(pageInfo)) {
  118. return;
  119. }
  120. await toggleSubscribe(pageId, pageInfo.subscriptionStatus);
  121. mutatePageInfo();
  122. }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
  123. const likeClickhandler = useCallback(async() => {
  124. if (isGuestUser ?? true) {
  125. return;
  126. }
  127. if (!isIPageInfoForOperation(pageInfo)) {
  128. return;
  129. }
  130. await toggleLike(pageId, pageInfo.isLiked);
  131. mutatePageInfo();
  132. }, [isGuestUser, mutatePageInfo, pageId, pageInfo]);
  133. const duplicateMenuItemClickHandler = useCallback(async(_pageId: string): Promise<void> => {
  134. if (onClickDuplicateMenuItem == null || path == null) {
  135. return;
  136. }
  137. const page: IPageForPageDuplicateModal = { pageId, path };
  138. onClickDuplicateMenuItem(page);
  139. }, [onClickDuplicateMenuItem, pageId, path]);
  140. const renameMenuItemClickHandler = useCallback(async(_pageId: string): Promise<void> => {
  141. if (onClickRenameMenuItem == null || path == null) {
  142. return;
  143. }
  144. const page: IPageToRenameWithMeta = {
  145. data: {
  146. _id: pageId,
  147. revision: revisionId,
  148. path,
  149. },
  150. meta: pageInfo,
  151. };
  152. onClickRenameMenuItem(page);
  153. }, [onClickRenameMenuItem, pageId, pageInfo, path, revisionId]);
  154. const deleteMenuItemClickHandler = useCallback(async(_pageId: string): Promise<void> => {
  155. if (onClickDeleteMenuItem == null || path == null) {
  156. return;
  157. }
  158. const pageToDelete: IPageToDeleteWithMeta = {
  159. data: {
  160. _id: pageId,
  161. revision: revisionId,
  162. path,
  163. },
  164. meta: pageInfo,
  165. };
  166. onClickDeleteMenuItem(pageToDelete);
  167. }, [onClickDeleteMenuItem, pageId, pageInfo, path, revisionId]);
  168. const switchContentWidthClickHandler = useCallback(async(newValue: boolean) => {
  169. if (onClickSwitchContentWidth == null || (isGuestUser ?? true) || (isReadOnlyUser ?? true)) {
  170. logger.warn('Could not switch content width', {
  171. onClickSwitchContentWidth: onClickSwitchContentWidth == null ? 'null' : 'not null',
  172. isGuestUser,
  173. isReadOnlyUser,
  174. });
  175. return;
  176. }
  177. if (!isIPageInfoForEntity(pageInfo)) {
  178. return;
  179. }
  180. try {
  181. onClickSwitchContentWidth(pageId, newValue);
  182. }
  183. catch (err) {
  184. toastError(err);
  185. }
  186. }, [isGuestUser, isReadOnlyUser, onClickSwitchContentWidth, pageId, pageInfo]);
  187. const additionalMenuItemOnTopRenderer = useMemo(() => {
  188. if (!isIPageInfoForEntity(pageInfo)) {
  189. return undefined;
  190. }
  191. const wideviewMenuItemRenderer = (props: WideViewMenuItemProps) => {
  192. return <WideViewMenuItem {...props} onClickMenuItem={switchContentWidthClickHandler} expandContentWidth={expandContentWidth} />;
  193. };
  194. return wideviewMenuItemRenderer;
  195. }, [pageInfo, switchContentWidthClickHandler, expandContentWidth]);
  196. if (!isIPageInfoForOperation(pageInfo)) {
  197. return <></>;
  198. }
  199. const {
  200. sumOfLikers, sumOfSeenUsers, isLiked,
  201. } = pageInfo;
  202. const forceHideMenuItemsWithAdditions = [
  203. ...(forceHideMenuItems ?? []),
  204. MenuItemType.BOOKMARK,
  205. MenuItemType.REVERT,
  206. ];
  207. const isViewMode = editorMode === EditorMode.View;
  208. return (
  209. <div className={`grw-page-controls ${styles['grw-page-controls']} d-flex`} style={{ gap: '2px' }}>
  210. {revisionId != null && !isViewMode && (
  211. <Tags
  212. onClickEditTagsButton={onClickEditTagsButton}
  213. />
  214. )}
  215. {revisionId != null && (
  216. <SubscribeButton
  217. status={pageInfo.subscriptionStatus}
  218. onClick={subscribeClickhandler}
  219. />
  220. )}
  221. {revisionId != null && (
  222. <LikeButtons
  223. onLikeClicked={likeClickhandler}
  224. sumOfLikers={sumOfLikers}
  225. isLiked={isLiked}
  226. likers={likers}
  227. />
  228. )}
  229. {revisionId != null && (
  230. <BookmarkButtons
  231. pageId={pageId}
  232. isBookmarked={pageInfo.isBookmarked}
  233. bookmarkCount={pageInfo.bookmarkCount}
  234. />
  235. )}
  236. {revisionId != null && (
  237. <SeenUserInfo
  238. seenUsers={seenUsers}
  239. sumOfSeenUsers={sumOfSeenUsers}
  240. disabled={disableSeenUserInfoPopover}
  241. />
  242. ) }
  243. { showPageControlDropdown && (
  244. <PageItemControl
  245. alignEnd
  246. pageId={pageId}
  247. pageInfo={pageInfo}
  248. isEnableActions={!isGuestUser}
  249. isReadOnlyUser={!!isReadOnlyUser}
  250. forceHideMenuItems={forceHideMenuItemsWithAdditions}
  251. additionalMenuItemOnTopRenderer={!isReadOnlyUser ? additionalMenuItemOnTopRenderer : undefined}
  252. additionalMenuItemRenderer={additionalMenuItemRenderer}
  253. onClickRenameMenuItem={renameMenuItemClickHandler}
  254. onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
  255. onClickDeleteMenuItem={deleteMenuItemClickHandler}
  256. />
  257. )}
  258. </div>
  259. );
  260. };
  261. type PageControlsProps = CommonProps & {
  262. pageId: string,
  263. shareLinkId?: string | null,
  264. revisionId?: string,
  265. path?: string | null,
  266. expandContentWidth?: boolean,
  267. };
  268. export const PageControls = memo((props: PageControlsProps): JSX.Element => {
  269. const {
  270. pageId, revisionId, path, shareLinkId, expandContentWidth,
  271. onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickSwitchContentWidth,
  272. } = props;
  273. const { data: pageInfo, error } = useSWRxPageInfo(pageId ?? null, shareLinkId);
  274. const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
  275. const { open: openTagEditModal } = useTagEditModal();
  276. const onClickEditTagsButton = useCallback(() => {
  277. if (tagsInfoData == null || revisionId == null) {
  278. return;
  279. }
  280. openTagEditModal(tagsInfoData.tags, pageId, revisionId);
  281. }, [pageId, revisionId, tagsInfoData, openTagEditModal]);
  282. if (error != null) {
  283. return <></>;
  284. }
  285. if (!isIPageInfoForOperation(pageInfo)) {
  286. return <></>;
  287. }
  288. return (
  289. <PageControlsSubstance
  290. {...props}
  291. pageInfo={pageInfo}
  292. pageId={pageId}
  293. revisionId={revisionId ?? null}
  294. path={path}
  295. onClickEditTagsButton={onClickEditTagsButton}
  296. onClickDuplicateMenuItem={onClickDuplicateMenuItem}
  297. onClickRenameMenuItem={onClickRenameMenuItem}
  298. onClickDeleteMenuItem={onClickDeleteMenuItem}
  299. onClickSwitchContentWidth={onClickSwitchContentWidth}
  300. expandContentWidth={expandContentWidth}
  301. />
  302. );
  303. });