|
|
@@ -1,4 +1,4 @@
|
|
|
-import React, { useCallback } from 'react';
|
|
|
+import React, { useCallback, useState, useEffect } from 'react';
|
|
|
|
|
|
import type EventEmitter from 'events';
|
|
|
|
|
|
@@ -6,23 +6,31 @@ import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-pa
|
|
|
import { useTranslation } from 'next-i18next';
|
|
|
import {
|
|
|
UncontrolledButtonDropdown, Button,
|
|
|
- DropdownToggle, DropdownMenu, DropdownItem,
|
|
|
+ DropdownToggle, DropdownMenu, DropdownItem, Modal,
|
|
|
} from 'reactstrap';
|
|
|
|
|
|
import { toastSuccess, toastError } from '~/client/util/toastr';
|
|
|
import type { IPageGrantData } from '~/interfaces/page';
|
|
|
import {
|
|
|
useIsEditable, useIsAclEnabled,
|
|
|
+ useIsSlackConfigured,
|
|
|
} from '~/stores/context';
|
|
|
-import { useWaitingSaveProcessing } from '~/stores/editor';
|
|
|
-import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
|
|
|
+import { useWaitingSaveProcessing, useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
|
|
|
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage, useCurrentPagePath } from '~/stores/page';
|
|
|
import { mutatePageTree } from '~/stores/page-listing';
|
|
|
-import { useSelectedGrant } from '~/stores/ui';
|
|
|
+import {
|
|
|
+ useSelectedGrant,
|
|
|
+ useEditorMode, useIsDeviceLargerThanMd,
|
|
|
+ EditorMode,
|
|
|
+} from '~/stores/ui';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
|
|
|
+
|
|
|
import { unpublish } from '../client/services/page-operation';
|
|
|
|
|
|
+import { LoadingSpinner } from './LoadingSpinner';
|
|
|
import { GrantSelector } from './SavePageControls/GrantSelector';
|
|
|
+import { SlackNotification } from './SlackNotification';
|
|
|
|
|
|
|
|
|
declare global {
|
|
|
@@ -33,25 +41,19 @@ declare global {
|
|
|
|
|
|
const logger = loggerFactory('growi:SavePageControls');
|
|
|
|
|
|
-export type SavePageControlsProps = {
|
|
|
- slackChannels: string
|
|
|
-}
|
|
|
|
|
|
-export const SavePageControls = (props: SavePageControlsProps): JSX.Element | null => {
|
|
|
- const { slackChannels } = props;
|
|
|
+const SavePageButton = (props: {slackChannels: string, isDeviceLargerThanMd?: boolean}) => {
|
|
|
+
|
|
|
const { t } = useTranslation();
|
|
|
const { data: currentPage } = useSWRxCurrentPage();
|
|
|
- const { data: isEditable } = useIsEditable();
|
|
|
- const { data: isAclEnabled } = useIsAclEnabled();
|
|
|
- const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
|
|
|
const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
|
|
|
const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
|
|
|
+ const { mutate: mutateEditorMode } = useEditorMode();
|
|
|
+ const [isSavePageModalShown, setIsSavePageModalShown] = useState<boolean>(false);
|
|
|
|
|
|
- const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
|
|
|
+ const { slackChannels, isDeviceLargerThanMd } = props;
|
|
|
|
|
|
- const updateGrantHandler = useCallback((grantData: IPageGrantData): void => {
|
|
|
- mutateGrant(grantData);
|
|
|
- }, [mutateGrant]);
|
|
|
+ const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
|
|
|
|
|
|
const save = useCallback(async(): Promise<void> => {
|
|
|
// save
|
|
|
@@ -74,46 +76,21 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
|
|
|
await unpublish(pageId);
|
|
|
await mutateCurrentPage();
|
|
|
await mutatePageTree();
|
|
|
+ await mutateEditorMode(EditorMode.View);
|
|
|
toastSuccess(t('wip_page.success_save_as_wip'));
|
|
|
}
|
|
|
catch (err) {
|
|
|
logger.error(err);
|
|
|
toastError(t('wip_page.fail_save_as_wip'));
|
|
|
}
|
|
|
- }, [currentPage?._id, mutateCurrentPage, t]);
|
|
|
-
|
|
|
-
|
|
|
- if (isEditable == null || isAclEnabled == null || grantData == null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ }, [currentPage?._id, mutateCurrentPage, mutateEditorMode, t]);
|
|
|
|
|
|
- if (!isEditable) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- const { grant, userRelatedGrantedGroups } = grantData;
|
|
|
-
|
|
|
- const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
|
|
|
const labelSubmitButton = t('Update');
|
|
|
const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
|
|
|
const labelUnpublishPage = t('wip_page.save_as_wip');
|
|
|
|
|
|
return (
|
|
|
- <div className="d-flex align-items-center flex-nowrap">
|
|
|
-
|
|
|
- {isAclEnabled
|
|
|
- && (
|
|
|
- <div className="me-2">
|
|
|
- <GrantSelector
|
|
|
- grant={grant}
|
|
|
- disabled={isGrantSelectorDisabledPage}
|
|
|
- userRelatedGrantedGroups={userRelatedGrantedGroups}
|
|
|
- onUpdateGrant={updateGrantHandler}
|
|
|
- />
|
|
|
- </div>
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
+ <>
|
|
|
<UncontrolledButtonDropdown direction="up" size="sm">
|
|
|
<Button
|
|
|
id="caret"
|
|
|
@@ -124,21 +101,194 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
|
|
|
disabled={isWaitingSaveProcessing}
|
|
|
>
|
|
|
{isWaitingSaveProcessing && (
|
|
|
- <i className="fa fa-spinner fa-pulse me-1"></i>
|
|
|
+ <LoadingSpinner />
|
|
|
)}
|
|
|
{labelSubmitButton}
|
|
|
</Button>
|
|
|
- <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} />
|
|
|
- <DropdownMenu container="body" end>
|
|
|
- <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
|
|
|
- {labelOverwriteScopes}
|
|
|
- </DropdownItem>
|
|
|
- <DropdownItem onClick={clickUnpublishButtonHandler}>
|
|
|
- {labelUnpublishPage}
|
|
|
- </DropdownItem>
|
|
|
- </DropdownMenu>
|
|
|
+ {
|
|
|
+ isDeviceLargerThanMd ? (
|
|
|
+ <>
|
|
|
+ <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} />
|
|
|
+ <DropdownMenu container="body" end>
|
|
|
+ <DropdownItem onClick={saveAndOverwriteScopesOfDescendants}>
|
|
|
+ {labelOverwriteScopes}
|
|
|
+ </DropdownItem>
|
|
|
+ <DropdownItem onClick={clickUnpublishButtonHandler}>
|
|
|
+ {labelUnpublishPage}
|
|
|
+ </DropdownItem>
|
|
|
+ </DropdownMenu>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} onClick={() => setIsSavePageModalShown(true)} />
|
|
|
+ <Modal
|
|
|
+ centered
|
|
|
+ isOpen={isSavePageModalShown}
|
|
|
+ toggle={() => setIsSavePageModalShown(false)}
|
|
|
+ >
|
|
|
+ <div className="d-flex flex-column pt-4 pb-3 px-4 gap-4">
|
|
|
+ <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); saveAndOverwriteScopesOfDescendants() }}>
|
|
|
+ {labelOverwriteScopes}
|
|
|
+ </button>
|
|
|
+ <button type="button" className="btn btn-primary" onClick={() => { setIsSavePageModalShown(false); clickUnpublishButtonHandler() }}>
|
|
|
+ {labelUnpublishPage}
|
|
|
+ </button>
|
|
|
+ <button type="button" className="btn btn-outline-neutral-secondary mx-auto mt-1" onClick={() => setIsSavePageModalShown(false)}>
|
|
|
+ <label className="mx-2">
|
|
|
+ {t('Cancel')}
|
|
|
+ </label>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+ </>
|
|
|
+ )
|
|
|
+ }
|
|
|
</UncontrolledButtonDropdown>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
|
|
|
+export const SavePageControls = (): JSX.Element | null => {
|
|
|
+ const { t } = useTranslation('commons');
|
|
|
+ const { data: currentPage } = useSWRxCurrentPage();
|
|
|
+ const { data: isEditable } = useIsEditable();
|
|
|
+ const { data: isAclEnabled } = useIsAclEnabled();
|
|
|
+ const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
|
|
|
+
|
|
|
+ const { data: editorMode } = useEditorMode();
|
|
|
+ const { data: currentPagePath } = useCurrentPagePath();
|
|
|
+ const { data: isSlackConfigured } = useIsSlackConfigured();
|
|
|
+ const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
|
|
|
+ const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
|
|
|
+ const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
|
|
|
+
|
|
|
+ const [slackChannels, setSlackChannels] = useState<string>('');
|
|
|
+ const [isSavePageControlsModalShown, setIsSavePageControlsModalShown] = useState<boolean>(false);
|
|
|
+
|
|
|
+ // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
|
|
|
+ const slackChannelsDataString = slackChannelsData?.toString();
|
|
|
+ useEffect(() => {
|
|
|
+ if (editorMode === 'editor') {
|
|
|
+ setSlackChannels(slackChannelsDataString ?? '');
|
|
|
+ mutateIsSlackEnabled(false);
|
|
|
+ }
|
|
|
+ }, [editorMode, mutateIsSlackEnabled, slackChannelsDataString]);
|
|
|
+
|
|
|
+
|
|
|
+ const isSlackEnabledToggleHandler = (bool: boolean) => {
|
|
|
+ mutateIsSlackEnabled(bool, false);
|
|
|
+ };
|
|
|
+
|
|
|
+ const slackChannelsChangedHandler = useCallback((slackChannels: string) => {
|
|
|
+ setSlackChannels(slackChannels);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const updateGrantHandler = useCallback((grantData: IPageGrantData): void => {
|
|
|
+ mutateGrant(grantData);
|
|
|
+ }, [mutateGrant]);
|
|
|
+
|
|
|
+ if (isEditable == null || isAclEnabled == null || grantData == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isEditable) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const { grant, userRelatedGrantedGroups } = grantData;
|
|
|
+
|
|
|
+ const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="d-flex align-items-center flex-nowrap">
|
|
|
+ {
|
|
|
+ isDeviceLargerThanMd ? (
|
|
|
+ <>
|
|
|
+ {
|
|
|
+ isSlackConfigured && (
|
|
|
+ <div className="me-2">
|
|
|
+ {isSlackEnabled != null && (
|
|
|
+ <SlackNotification
|
|
|
+ isSlackEnabled={isSlackEnabled}
|
|
|
+ slackChannels={slackChannels}
|
|
|
+ onEnabledFlagChange={isSlackEnabledToggleHandler}
|
|
|
+ onChannelChange={slackChannelsChangedHandler}
|
|
|
+ id="idForEditorNavbarBottom"
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ isAclEnabled && (
|
|
|
+ <div className="me-2">
|
|
|
+ <GrantSelector
|
|
|
+ grant={grant}
|
|
|
+ disabled={isGrantSelectorDisabledPage}
|
|
|
+ userRelatedGrantedGroups={userRelatedGrantedGroups}
|
|
|
+ onUpdateGrant={updateGrantHandler}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ <SavePageButton slackChannels={slackChannels} isDeviceLargerThanMd />
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <SavePageButton slackChannels={slackChannels} />
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ className="btn btn-outline-neutral-secondary border-0 fs-5 p-0 ms-1 text-muted"
|
|
|
+ onClick={() => setIsSavePageControlsModalShown(true)}
|
|
|
+ >
|
|
|
+ <span className="material-symbols-outlined">more_vert</span>
|
|
|
+ </button>
|
|
|
+ <Modal
|
|
|
+ className="save-page-controls-modal"
|
|
|
+ centered
|
|
|
+ isOpen={isSavePageControlsModalShown}
|
|
|
+ >
|
|
|
+ <div className="d-flex flex-column pt-5 pb-3 px-4 gap-3">
|
|
|
+ {
|
|
|
+ isAclEnabled && (
|
|
|
+ <>
|
|
|
+ <GrantSelector
|
|
|
+ grant={grant}
|
|
|
+ disabled={isGrantSelectorDisabledPage}
|
|
|
+ openInModal
|
|
|
+ userRelatedGrantedGroups={userRelatedGrantedGroups}
|
|
|
+ onUpdateGrant={updateGrantHandler}
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ isSlackConfigured && isSlackEnabled != null && (
|
|
|
+ <>
|
|
|
+ <SlackNotification
|
|
|
+ isSlackEnabled={isSlackEnabled}
|
|
|
+ slackChannels={slackChannels}
|
|
|
+ onEnabledFlagChange={isSlackEnabledToggleHandler}
|
|
|
+ onChannelChange={slackChannelsChangedHandler}
|
|
|
+ id="idForEditorNavbarBottom"
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ <div className="d-flex">
|
|
|
+ <button type="button" className="mx-auto btn btn-primary rounded-1" onClick={() => setIsSavePageControlsModalShown(false)}>
|
|
|
+ {t('Done')}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+ </>
|
|
|
+ )
|
|
|
+ }
|
|
|
</div>
|
|
|
);
|
|
|
};
|