PageItemControl.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import React, { useState, useCallback } from 'react';
  2. import {
  3. Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
  4. } from 'reactstrap';
  5. import { useTranslation } from 'react-i18next';
  6. import loggerFactory from '~/utils/logger';
  7. import {
  8. IPageInfoAll, isIPageInfoForOperation,
  9. } from '~/interfaces/page';
  10. import { useSWRxPageInfo } from '~/stores/page';
  11. const logger = loggerFactory('growi:cli:PageItemControl');
  12. export const MenuItemType = {
  13. BOOKMARK: 'bookmark',
  14. DUPLICATE: 'duplicate',
  15. RENAME: 'rename',
  16. DELETE: 'delete',
  17. REVERT: 'revert',
  18. } as const;
  19. export type MenuItemType = typeof MenuItemType[keyof typeof MenuItemType];
  20. export type ForceHideMenuItems = MenuItemType[];
  21. export type AdditionalMenuItemsRendererProps = { pageInfo: IPageInfoAll };
  22. type CommonProps = {
  23. pageInfo?: IPageInfoAll,
  24. isEnableActions?: boolean,
  25. forceHideMenuItems?: ForceHideMenuItems,
  26. onClickBookmarkMenuItem?: (pageId: string, newValue?: boolean) => Promise<void>,
  27. onClickDuplicateMenuItem?: (pageId: string) => Promise<void> | void,
  28. onClickRenameMenuItem?: (pageId: string) => Promise<void> | void,
  29. onClickDeleteMenuItem?: (pageId: string, pageInfo: IPageInfoAll | undefined) => Promise<void> | void,
  30. onClickRevertMenuItem?: (pageId: string) => Promise<void> | void,
  31. additionalMenuItemRenderer?: React.FunctionComponent<AdditionalMenuItemsRendererProps>,
  32. }
  33. type DropdownMenuProps = CommonProps & {
  34. pageId: string,
  35. isLoading?: boolean,
  36. }
  37. const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.Element => {
  38. const { t } = useTranslation('');
  39. const {
  40. pageId, isLoading,
  41. pageInfo, isEnableActions, forceHideMenuItems,
  42. onClickBookmarkMenuItem, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem, onClickRevertMenuItem,
  43. additionalMenuItemRenderer: AdditionalMenuItems,
  44. } = props;
  45. // eslint-disable-next-line react-hooks/rules-of-hooks
  46. const bookmarkItemClickedHandler = useCallback(async() => {
  47. if (!isIPageInfoForOperation(pageInfo) || onClickBookmarkMenuItem == null) {
  48. return;
  49. }
  50. await onClickBookmarkMenuItem(pageId, !pageInfo.isBookmarked);
  51. }, [onClickBookmarkMenuItem, pageId, pageInfo]);
  52. // eslint-disable-next-line react-hooks/rules-of-hooks
  53. const duplicateItemClickedHandler = useCallback(async() => {
  54. if (onClickDuplicateMenuItem == null) {
  55. return;
  56. }
  57. await onClickDuplicateMenuItem(pageId);
  58. }, [onClickDuplicateMenuItem, pageId]);
  59. // eslint-disable-next-line react-hooks/rules-of-hooks
  60. const renameItemClickedHandler = useCallback(async() => {
  61. if (onClickRenameMenuItem == null) {
  62. return;
  63. }
  64. await onClickRenameMenuItem(pageId);
  65. }, [onClickRenameMenuItem, pageId]);
  66. const revertItemClickedHandler = useCallback(async() => {
  67. if (onClickRevertMenuItem == null) {
  68. return;
  69. }
  70. await onClickRevertMenuItem(pageId);
  71. }, [onClickRevertMenuItem]);
  72. // eslint-disable-next-line react-hooks/rules-of-hooks
  73. const deleteItemClickedHandler = useCallback(async() => {
  74. if (pageInfo == null || onClickDeleteMenuItem == null) {
  75. return;
  76. }
  77. if (!pageInfo.isDeletable) {
  78. logger.warn('This page could not be deleted.');
  79. return;
  80. }
  81. await onClickDeleteMenuItem(pageId, pageInfo);
  82. }, [onClickDeleteMenuItem, pageId, pageInfo]);
  83. let contents = <></>;
  84. if (isLoading) {
  85. contents = (
  86. <div className="text-muted text-center my-2">
  87. <i className="fa fa-spinner fa-pulse"></i>
  88. </div>
  89. );
  90. }
  91. else if (pageId != null && pageInfo != null) {
  92. const showDeviderBeforeAdditionalMenuItems = (forceHideMenuItems?.length ?? 0) < 3;
  93. const showDeviderBeforeDelete = AdditionalMenuItems != null || showDeviderBeforeAdditionalMenuItems;
  94. contents = (
  95. <>
  96. { !isEnableActions && (
  97. <DropdownItem>
  98. <p>
  99. {t('search_result.currently_not_implemented')}
  100. </p>
  101. </DropdownItem>
  102. ) }
  103. {/* Bookmark */}
  104. { !forceHideMenuItems?.includes(MenuItemType.BOOKMARK) && isEnableActions && !pageInfo.isEmpty && isIPageInfoForOperation(pageInfo) && (
  105. <DropdownItem onClick={bookmarkItemClickedHandler}>
  106. <i className="fa fa-fw fa-bookmark-o"></i>
  107. { pageInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark') }
  108. </DropdownItem>
  109. ) }
  110. {/* Duplicate */}
  111. { !forceHideMenuItems?.includes(MenuItemType.DUPLICATE) && isEnableActions && (
  112. <DropdownItem onClick={duplicateItemClickedHandler}>
  113. <i className="icon-fw icon-docs"></i>
  114. {t('Duplicate')}
  115. </DropdownItem>
  116. ) }
  117. {/* Move/Rename */}
  118. { !forceHideMenuItems?.includes(MenuItemType.RENAME) && isEnableActions && pageInfo.isMovable && (
  119. <DropdownItem onClick={renameItemClickedHandler}>
  120. <i className="icon-fw icon-action-redo"></i>
  121. {t('Move/Rename')}
  122. </DropdownItem>
  123. ) }
  124. {/* Revert */}
  125. { !forceHideMenuItems?.includes(MenuItemType.REVERT) && isEnableActions && pageInfo.isRevertible && (
  126. <DropdownItem onClick={revertItemClickedHandler}>
  127. <i className="icon-fw icon-action-undo"></i>
  128. {t('modal_putback.label.Put Back Page')}
  129. </DropdownItem>
  130. ) }
  131. { AdditionalMenuItems && (
  132. <>
  133. { showDeviderBeforeAdditionalMenuItems && <DropdownItem divider /> }
  134. <AdditionalMenuItems pageInfo={pageInfo} />
  135. </>
  136. ) }
  137. {/* divider */}
  138. {/* Delete */}
  139. { !forceHideMenuItems?.includes(MenuItemType.DELETE) && isEnableActions && pageInfo.isMovable && (
  140. <>
  141. { showDeviderBeforeDelete && <DropdownItem divider /> }
  142. <DropdownItem
  143. className={`pt-2 ${pageInfo.isDeletable ? 'text-danger' : ''}`}
  144. disabled={!pageInfo.isDeletable}
  145. onClick={deleteItemClickedHandler}
  146. >
  147. <i className="icon-fw icon-trash"></i>
  148. {t('Delete')}
  149. </DropdownItem>
  150. </>
  151. )}
  152. </>
  153. );
  154. }
  155. return (
  156. <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: undefined } }}>
  157. {contents}
  158. </DropdownMenu>
  159. );
  160. });
  161. type PageItemControlSubstanceProps = CommonProps & {
  162. pageId: string,
  163. fetchOnInit?: boolean,
  164. children?: React.ReactNode,
  165. }
  166. export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
  167. const {
  168. pageId, pageInfo: presetPageInfo, fetchOnInit,
  169. children,
  170. onClickBookmarkMenuItem, onClickDuplicateMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem,
  171. } = props;
  172. const [isOpen, setIsOpen] = useState(false);
  173. const shouldFetch = fetchOnInit === true || (!isIPageInfoForOperation(presetPageInfo) && isOpen);
  174. const shouldMutate = fetchOnInit === true || !isIPageInfoForOperation(presetPageInfo);
  175. const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
  176. // mutate after handle event
  177. const bookmarkMenuItemClickHandler = useCallback(async(_pageId: string, _newValue: boolean) => {
  178. if (onClickBookmarkMenuItem != null) {
  179. await onClickBookmarkMenuItem(_pageId, _newValue);
  180. }
  181. if (shouldMutate) {
  182. mutatePageInfo();
  183. }
  184. }, [mutatePageInfo, onClickBookmarkMenuItem, shouldMutate]);
  185. const isLoading = shouldFetch && fetchedPageInfo == null;
  186. const duplicateMenuItemClickHandler = useCallback(async() => {
  187. if (onClickDuplicateMenuItem == null) {
  188. return;
  189. }
  190. await onClickDuplicateMenuItem(pageId);
  191. }, [onClickDuplicateMenuItem, pageId]);
  192. const renameMenuItemClickHandler = useCallback(async() => {
  193. if (onClickRenameMenuItem == null) {
  194. return;
  195. }
  196. await onClickRenameMenuItem(pageId);
  197. }, [onClickRenameMenuItem, pageId]);
  198. const deleteMenuItemClickHandler = useCallback(async() => {
  199. if (onClickDeleteMenuItem == null) {
  200. return;
  201. }
  202. await onClickDeleteMenuItem(pageId, fetchedPageInfo ?? presetPageInfo);
  203. }, [onClickDeleteMenuItem, pageId, fetchedPageInfo, presetPageInfo]);
  204. return (
  205. <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
  206. { children ?? (
  207. <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control">
  208. <i className="icon-options text-muted"></i>
  209. </DropdownToggle>
  210. ) }
  211. <PageItemControlDropdownMenu
  212. {...props}
  213. isLoading={isLoading}
  214. pageInfo={fetchedPageInfo ?? presetPageInfo}
  215. onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
  216. onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
  217. onClickRenameMenuItem={renameMenuItemClickHandler}
  218. onClickDeleteMenuItem={deleteMenuItemClickHandler}
  219. />
  220. </Dropdown>
  221. );
  222. };
  223. type PageItemControlProps = CommonProps & {
  224. pageId?: string,
  225. children?: React.ReactNode,
  226. }
  227. export const PageItemControl = (props: PageItemControlProps): JSX.Element => {
  228. const { pageId } = props;
  229. if (pageId == null) {
  230. return <></>;
  231. }
  232. return <PageItemControlSubstance pageId={pageId} {...props} />;
  233. };
  234. type AsyncPageItemControlProps = Omit<CommonProps, 'pageInfo'> & {
  235. pageId?: string,
  236. children?: React.ReactNode,
  237. }
  238. export const AsyncPageItemControl = (props: AsyncPageItemControlProps): JSX.Element => {
  239. const { pageId } = props;
  240. if (pageId == null) {
  241. return <></>;
  242. }
  243. return <PageItemControlSubstance pageId={pageId} fetchOnInit {...props} />;
  244. };