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