SavePageControls.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import React, { useCallback, useState, useEffect } from 'react';
  2. import type EventEmitter from 'events';
  3. import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
  4. import { LoadingSpinner } from '@growi/ui/dist/components';
  5. import { useTranslation } from 'next-i18next';
  6. import {
  7. UncontrolledButtonDropdown, Button,
  8. DropdownToggle, DropdownMenu, DropdownItem, Modal,
  9. } from 'reactstrap';
  10. import {
  11. useIsEditable, useIsAclEnabled,
  12. useIsSlackConfigured,
  13. } from '~/stores/context';
  14. import { useWaitingSaveProcessing, useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
  15. import { useSWRxCurrentPage, useCurrentPagePath } from '~/stores/page';
  16. import {
  17. useEditorMode, useIsDeviceLargerThanMd,
  18. } from '~/stores/ui';
  19. import loggerFactory from '~/utils/logger';
  20. import { GrantSelector } from './SavePageControls/GrantSelector';
  21. import { SlackNotification } from './SlackNotification';
  22. declare global {
  23. // eslint-disable-next-line vars-on-top, no-var
  24. var globalEmitter: EventEmitter;
  25. }
  26. const logger = loggerFactory('growi:SavePageControls');
  27. const SavePageButton = (props: {slackChannels: string, isDeviceLargerThanMd?: boolean}) => {
  28. const { t } = useTranslation();
  29. const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
  30. const [isSavePageModalShown, setIsSavePageModalShown] = useState<boolean>(false);
  31. const { slackChannels, isDeviceLargerThanMd } = props;
  32. const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
  33. const save = useCallback(async(): Promise<void> => {
  34. // save
  35. globalEmitter.emit('saveAndReturnToView', { wip: false, slackChannels });
  36. }, [slackChannels]);
  37. const saveAndOverwriteScopesOfDescendants = useCallback(() => {
  38. // save
  39. globalEmitter.emit('saveAndReturnToView', { wip: false, overwriteScopesOfDescendants: true, slackChannels });
  40. }, [slackChannels]);
  41. const saveAndMakeWip = useCallback(() => {
  42. // save
  43. globalEmitter.emit('saveAndReturnToView', { wip: true, slackChannels });
  44. }, [slackChannels]);
  45. const labelSubmitButton = t('Update');
  46. const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
  47. const labelUnpublishPage = t('wip_page.save_as_wip');
  48. return (
  49. <>
  50. <UncontrolledButtonDropdown direction="up" size="sm">
  51. <Button
  52. id="caret"
  53. data-testid="save-page-btn"
  54. color="primary"
  55. className="btn-submit"
  56. onClick={save}
  57. disabled={isWaitingSaveProcessing}
  58. >
  59. {isWaitingSaveProcessing && (
  60. <LoadingSpinner />
  61. )}
  62. {labelSubmitButton}
  63. </Button>
  64. {
  65. isDeviceLargerThanMd ? (
  66. <>
  67. <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} />
  68. <DropdownMenu container="body" end>
  69. <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
  70. {labelOverwriteScopes}
  71. </DropdownItem>
  72. <DropdownItem onClick={saveAndMakeWip}>
  73. {labelUnpublishPage}
  74. </DropdownItem>
  75. </DropdownMenu>
  76. </>
  77. ) : (
  78. <>
  79. <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} onClick={() => setIsSavePageModalShown(true)} />
  80. <Modal
  81. centered
  82. isOpen={isSavePageModalShown}
  83. toggle={() => setIsSavePageModalShown(false)}
  84. >
  85. <div className="d-flex flex-column pt-4 pb-3 px-4 gap-4">
  86. <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndOverwriteScopesOfDescendants() }}>
  87. {labelOverwriteScopes}
  88. </button>
  89. <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndMakeWip() }}>
  90. {labelUnpublishPage}
  91. </button>
  92. <button type="button" className="btn btn-outline-neutral-secondary mx-auto mt-1" onClick={() => setIsSavePageModalShown(false)}>
  93. <label className="mx-2">
  94. {t('Cancel')}
  95. </label>
  96. </button>
  97. </div>
  98. </Modal>
  99. </>
  100. )
  101. }
  102. </UncontrolledButtonDropdown>
  103. </>
  104. );
  105. };
  106. export const SavePageControls = (): JSX.Element | null => {
  107. const { t } = useTranslation('commons');
  108. const { data: currentPage } = useSWRxCurrentPage();
  109. const { data: isEditable } = useIsEditable();
  110. const { data: isAclEnabled } = useIsAclEnabled();
  111. const { data: editorMode } = useEditorMode();
  112. const { data: currentPagePath } = useCurrentPagePath();
  113. const { data: isSlackConfigured } = useIsSlackConfigured();
  114. const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
  115. const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
  116. const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
  117. const [slackChannels, setSlackChannels] = useState<string>('');
  118. const [isSavePageControlsModalShown, setIsSavePageControlsModalShown] = useState<boolean>(false);
  119. // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
  120. const slackChannelsDataString = slackChannelsData?.toString();
  121. useEffect(() => {
  122. if (editorMode === 'editor') {
  123. setSlackChannels(slackChannelsDataString ?? '');
  124. mutateIsSlackEnabled(false);
  125. }
  126. }, [editorMode, mutateIsSlackEnabled, slackChannelsDataString]);
  127. const isSlackEnabledToggleHandler = (bool: boolean) => {
  128. mutateIsSlackEnabled(bool, false);
  129. };
  130. const slackChannelsChangedHandler = useCallback((slackChannels: string) => {
  131. setSlackChannels(slackChannels);
  132. }, []);
  133. if (isEditable == null || isAclEnabled == null) {
  134. return null;
  135. }
  136. if (!isEditable) {
  137. return null;
  138. }
  139. const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
  140. return (
  141. <div className="d-flex align-items-center flex-nowrap">
  142. {
  143. isDeviceLargerThanMd ? (
  144. <>
  145. {
  146. isSlackConfigured && (
  147. <div className="me-2">
  148. {isSlackEnabled != null && (
  149. <SlackNotification
  150. isSlackEnabled={isSlackEnabled}
  151. slackChannels={slackChannels}
  152. onEnabledFlagChange={isSlackEnabledToggleHandler}
  153. onChannelChange={slackChannelsChangedHandler}
  154. id="idForEditorNavbarBottom"
  155. />
  156. )}
  157. </div>
  158. )
  159. }
  160. {
  161. isAclEnabled && (
  162. <div className="me-2">
  163. <GrantSelector disabled={isGrantSelectorDisabledPage} />
  164. </div>
  165. )
  166. }
  167. <SavePageButton slackChannels={slackChannels} isDeviceLargerThanMd />
  168. </>
  169. ) : (
  170. <>
  171. <SavePageButton slackChannels={slackChannels} />
  172. <button
  173. type="button"
  174. className="btn btn-outline-neutral-secondary border-0 fs-5 p-0 ms-1 text-muted"
  175. onClick={() => setIsSavePageControlsModalShown(true)}
  176. >
  177. <span className="material-symbols-outlined">more_vert</span>
  178. </button>
  179. <Modal
  180. className="save-page-controls-modal"
  181. centered
  182. isOpen={isSavePageControlsModalShown}
  183. >
  184. <div className="d-flex flex-column pt-5 pb-3 px-4 gap-3">
  185. {
  186. isAclEnabled && (
  187. <>
  188. <GrantSelector
  189. disabled={isGrantSelectorDisabledPage}
  190. openInModal
  191. />
  192. </>
  193. )
  194. }
  195. {
  196. isSlackConfigured && isSlackEnabled != null && (
  197. <>
  198. <SlackNotification
  199. isSlackEnabled={isSlackEnabled}
  200. slackChannels={slackChannels}
  201. onEnabledFlagChange={isSlackEnabledToggleHandler}
  202. onChannelChange={slackChannelsChangedHandler}
  203. id="idForEditorNavbarBottom"
  204. />
  205. </>
  206. )
  207. }
  208. <div className="d-flex">
  209. <button type="button" className="mx-auto btn btn-primary rounded-1" onClick={() => setIsSavePageControlsModalShown(false)}>
  210. {t('Done')}
  211. </button>
  212. </div>
  213. </div>
  214. </Modal>
  215. </>
  216. )
  217. }
  218. </div>
  219. );
  220. };