GrowiContextualSubNavigation.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import React, { useCallback } from 'react';
  2. import PropTypes from 'prop-types';
  3. import { useTranslation } from 'react-i18next';
  4. import { DropdownItem } from 'reactstrap';
  5. import { withUnstatedContainers } from '../UnstatedUtils';
  6. import EditorContainer from '~/client/services/EditorContainer';
  7. import {
  8. EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
  9. useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
  10. } from '~/stores/ui';
  11. import {
  12. useCurrentCreatedAt, useCurrentUpdatedAt, useCurrentPageId, useRevisionId, useCurrentPagePath,
  13. useCreator, useRevisionAuthor, useIsGuestUser, useIsSharedUser,
  14. } from '~/stores/context';
  15. import { useSWRTagsInfo } from '~/stores/page';
  16. import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
  17. import { SubNavButtons } from './SubNavButtons';
  18. import PageEditorModeManager from './PageEditorModeManager';
  19. import { toastSuccess, toastError } from '~/client/util/apiNotification';
  20. import { apiPost } from '~/client/util/apiv1-client';
  21. import { IPageHasId } from '~/interfaces/page';
  22. import { GrowiSubNavigation } from './GrowiSubNavigation';
  23. import PresentationIcon from '../Icons/PresentationIcon';
  24. import { exportAsMarkdown } from '~/client/services/page-operation';
  25. type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
  26. pageId: string,
  27. revisionId: string,
  28. }
  29. const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
  30. const { t } = useTranslation();
  31. const { pageId, revisionId } = props;
  32. return (
  33. <>
  34. <DropdownItem divider />
  35. {/* Presentation */}
  36. <DropdownItem onClick={() => { /* TODO: implement in https://redmine.weseek.co.jp/issues/87672 */ }}>
  37. <i className="icon-fw"><PresentationIcon /></i>
  38. { t('Presentation Mode') }
  39. </DropdownItem>
  40. {/* Export markdown */}
  41. <DropdownItem onClick={() => exportAsMarkdown(pageId, revisionId, 'md')}>
  42. <i className="icon-fw icon-cloud-download"></i>
  43. {t('export_bulk.export_page_markdown')}
  44. </DropdownItem>
  45. <DropdownItem divider />
  46. {/* Create template */}
  47. <DropdownItem onClick={() => { /* TODO: implement in https://redmine.weseek.co.jp/issues/87673 */ }}>
  48. <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
  49. </DropdownItem>
  50. </>
  51. );
  52. };
  53. const GrowiContextualSubNavigation = (props) => {
  54. const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
  55. const { data: isDrawerMode } = useDrawerMode();
  56. const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
  57. const { data: createdAt } = useCurrentCreatedAt();
  58. const { data: updatedAt } = useCurrentUpdatedAt();
  59. const { data: pageId } = useCurrentPageId();
  60. const { data: revisionId } = useRevisionId();
  61. const { data: path } = useCurrentPagePath();
  62. const { data: creator } = useCreator();
  63. const { data: revisionAuthor } = useRevisionAuthor();
  64. const { data: isGuestUser } = useIsGuestUser();
  65. const { data: isSharedUser } = useIsSharedUser();
  66. const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
  67. const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel();
  68. const { data: isAbleToShowPageEditorModeManager } = useIsAbleToShowPageEditorModeManager();
  69. const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
  70. const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRTagsInfo(pageId);
  71. const {
  72. editorContainer, isCompactMode,
  73. } = props;
  74. const isViewMode = editorMode === EditorMode.View;
  75. const tagsUpdatedHandler = useCallback(async(newTags: string[]) => {
  76. // It will not be reflected in the DB until the page is refreshed
  77. if (editorMode === EditorMode.Editor) {
  78. return editorContainer.setState({ tags: newTags });
  79. }
  80. try {
  81. const { tags } = await apiPost('/tags.update', { pageId, revisionId, tags: newTags }) as { tags };
  82. // revalidate SWRTagsInfo
  83. mutateSWRTagsInfo();
  84. // update editorContainer.state
  85. editorContainer.setState({ tags });
  86. toastSuccess('updated tags successfully');
  87. }
  88. catch (err) {
  89. toastError(err, 'fail to update tags');
  90. }
  91. // eslint-disable-next-line react-hooks/exhaustive-deps
  92. }, [pageId]);
  93. const ControlComponents = useCallback(() => {
  94. function onPageEditorModeButtonClicked(viewType) {
  95. mutateEditorMode(viewType);
  96. }
  97. return (
  98. <>
  99. <div className="h-50 d-flex flex-column align-items-end justify-content-center">
  100. { pageId != null && isViewMode && (
  101. <SubNavButtons
  102. isCompactMode={isCompactMode}
  103. pageId={pageId}
  104. revisionId={revisionId}
  105. disableSeenUserInfoPopover={isSharedUser}
  106. showPageControlDropdown={isAbleToShowPageManagement}
  107. additionalMenuItemRenderer={props => <AdditionalMenuItems {...props} pageId={pageId} revisionId={revisionId} />}
  108. />
  109. ) }
  110. </div>
  111. <div className="h-50 d-flex flex-column align-items-end justify-content-center">
  112. {isAbleToShowPageEditorModeManager && (
  113. <PageEditorModeManager
  114. onPageEditorModeButtonClicked={onPageEditorModeButtonClicked}
  115. isBtnDisabled={isGuestUser}
  116. editorMode={editorMode}
  117. isDeviceSmallerThanMd={isDeviceSmallerThanMd}
  118. />
  119. )}
  120. </div>
  121. </>
  122. );
  123. }, [
  124. pageId, revisionId,
  125. editorMode, mutateEditorMode,
  126. isCompactMode, isDeviceSmallerThanMd, isGuestUser, isSharedUser,
  127. isViewMode, isAbleToShowPageEditorModeManager, isAbleToShowPageManagement,
  128. ]);
  129. if (path == null) {
  130. return <></>;
  131. }
  132. const currentPage: Partial<IPageHasId> = {
  133. _id: pageId ?? undefined,
  134. path,
  135. revision: revisionId ?? undefined,
  136. creator: creator ?? undefined,
  137. lastUpdateUser: revisionAuthor,
  138. createdAt: createdAt ?? undefined,
  139. updatedAt: updatedAt ?? undefined,
  140. };
  141. return (
  142. <GrowiSubNavigation
  143. page={currentPage}
  144. showDrawerToggler={isDrawerMode}
  145. showTagLabel={isAbleToShowTagLabel}
  146. showPageAuthors={isAbleToShowPageAuthors}
  147. tags={tagsInfoData?.tags || []}
  148. tagsUpdatedHandler={tagsUpdatedHandler}
  149. controls={ControlComponents}
  150. />
  151. );
  152. };
  153. /**
  154. * Wrapper component for using unstated
  155. */
  156. const GrowiContextualSubNavigationWrapper = withUnstatedContainers(GrowiContextualSubNavigation, [EditorContainer]);
  157. GrowiContextualSubNavigation.propTypes = {
  158. editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
  159. isCompactMode: PropTypes.bool,
  160. };
  161. export default GrowiContextualSubNavigationWrapper;