SavePageControls.tsx 9.0 KB

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