Просмотр исходного кода

Merge pull request #8523 from weseek/imprv/141126-141349-design-of-input-field-for-slack-channel

imprv: design of input field for slack channel
Yuki Takei 2 лет назад
Родитель
Сommit
54ddbf0e91

+ 3 - 0
apps/app/public/images/icons/slack/slack-logo-background.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
+  <circle fill="white" cx="300" cy="300" r="300" />
+</svg>

+ 3 - 0
apps/app/public/images/icons/slack/slack-logo-dark-background.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600">
+  <circle fill="#370f38" cx="300" cy="300" r="300" />
+</svg>

+ 1 - 1
apps/app/public/static/locales/en_US/translation.json

@@ -311,7 +311,7 @@
     }
     }
   },
   },
   "page_edit": {
   "page_edit": {
-    "input_channels": "Input channels",
+    "input_channels": "Slack channel name...",
     "theme": "Theme",
     "theme": "Theme",
     "keymap": "Keymap",
     "keymap": "Keymap",
     "indent": "Indent",
     "indent": "Indent",

+ 3 - 15
apps/app/src/components/PageEditor/EditorNavbarBottom.module.scss

@@ -5,23 +5,11 @@
 @include mixins.editing() {
 @include mixins.editing() {
   .grw-editor-navbar-bottom :global {
   .grw-editor-navbar-bottom :global {
     .grw-grant-selector {
     .grw-grant-selector {
-      @include bs.media-breakpoint-down(sm) {
-        .btn .label {
-          display: none;
-        }
-      }
-      @include bs.media-breakpoint-up(md) {
-        .dropdown-toggle {
-          min-width: 100px;
-
-          // caret
-          &::after {
-            margin-left: 1em;
-          }
-        }
+      .material-symbols-outlined  {
+        padding-bottom: 2px;
+        font-size: 19px;
       }
       }
     }
     }
-
     .btn-submit {
     .btn-submit {
       width: 100px;
       width: 100px;
     }
     }

+ 5 - 98
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -1,114 +1,21 @@
-import React, { useCallback, useState, useEffect } from 'react';
-
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
-import { Collapse, Button } from 'reactstrap';
-
-
-import type { SavePageControlsProps } from '~/components/SavePageControls';
-import { useIsSlackConfigured } from '~/stores/context';
-import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
-import { useCurrentPagePath } from '~/stores/page';
-import {
-  useEditorMode, useIsDeviceLargerThanLg, useIsDeviceLargerThanMd,
-} from '~/stores/ui';
-
 
 
 import styles from './EditorNavbarBottom.module.scss';
 import styles from './EditorNavbarBottom.module.scss';
 
 
 const moduleClass = styles['grw-editor-navbar-bottom'];
 const moduleClass = styles['grw-editor-navbar-bottom'];
 
 
-
-const SavePageControls = dynamic<SavePageControlsProps>(() => import('~/components/SavePageControls').then(mod => mod.SavePageControls), { ssr: false });
-const SlackLogo = dynamic(() => import('~/components/SlackLogo').then(mod => mod.SlackLogo), { ssr: false });
-const SlackNotification = dynamic(() => import('~/components/SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
+const SavePageControls = dynamic(() => import('~/components/SavePageControls').then(mod => mod.SavePageControls), { ssr: false });
 const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false });
 const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false });
 
 
-
 const EditorNavbarBottom = (): JSX.Element => {
 const EditorNavbarBottom = (): JSX.Element => {
-
-  const [isSlackExpanded, setSlackExpanded] = useState(false);
-
-  const { data: editorMode } = useEditorMode();
-  const { data: isSlackConfigured } = useIsSlackConfigured();
-  const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
-  const { data: isDeviceLargerThanLg } = useIsDeviceLargerThanLg();
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
-
-  const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
-
-  const [slackChannelsStr, setSlackChannelsStr] = useState<string>('');
-
-  // DO NOT dependent on slackChannelsData directly: https://github.com/weseek/growi/pull/7332
-  const slackChannelsDataString = slackChannelsData?.toString();
-  useEffect(() => {
-    if (editorMode === 'editor') {
-      setSlackChannelsStr(slackChannelsDataString ?? '');
-      mutateIsSlackEnabled(false);
-    }
-  }, [editorMode, mutateIsSlackEnabled, slackChannelsDataString]);
-
-  const isSlackEnabledToggleHandler = (bool: boolean) => {
-    mutateIsSlackEnabled(bool, false);
-  };
-
-  const slackChannelsChangedHandler = useCallback((slackChannels: string) => {
-    setSlackChannelsStr(slackChannels);
-  }, []);
-
   return (
   return (
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
-      {/* Collapsed SlackNotification */}
-      {isSlackConfigured && (
-        <Collapse isOpen={isSlackExpanded && !isDeviceLargerThanLg}>
-          <nav className={`navbar navbar-expand-lg border-top ${moduleClass}`}>
-            {isSlackEnabled != null
-            && (
-              <SlackNotification
-                isSlackEnabled={isSlackEnabled}
-                slackChannels={slackChannelsStr}
-                onEnabledFlagChange={isSlackEnabledToggleHandler}
-                onChannelChange={slackChannelsChangedHandler}
-                id="idForEditorNavbarBottomForMobile"
-              />
-            )
-            }
-          </nav>
-        </Collapse>
-      )
-      }
       <div className={`flex-expand-horiz align-items-center px-2 py-1 py-md-2 px-md-3 ${moduleClass}`}>
       <div className={`flex-expand-horiz align-items-center px-2 py-1 py-md-2 px-md-3 ${moduleClass}`}>
-        <form>
-          <OptionsSelector collapsed={!isDeviceLargerThanMd} />
+        <form className="m-2 me-auto">
+          <OptionsSelector />
         </form>
         </form>
-        <form className="row row-cols-lg-auto g-3 align-items-center ms-auto">
-          {/* Responsive Design for the SlackNotification */}
-          {/* Button or the normal Slack banner */}
-          {isSlackConfigured && (!isDeviceLargerThanMd ? (
-            <Button
-              className="grw-btn-slack border me-2"
-              onClick={() => (setSlackExpanded(!isSlackExpanded))}
-            >
-              <div className="grw-slack-logo">
-                <SlackLogo />
-                <span className="grw-btn-slack-triangle material-symbols-outlined ms-2">arrow_drop_up</span>
-              </div>
-            </Button>
-          ) : (
-            <div className="me-2">
-              {isSlackEnabled != null
-              && (
-                <SlackNotification
-                  isSlackEnabled={isSlackEnabled}
-                  slackChannels={slackChannelsStr}
-                  onEnabledFlagChange={isSlackEnabledToggleHandler}
-                  onChannelChange={slackChannelsChangedHandler}
-                  id="idForEditorNavbarBottom"
-                />
-              )}
-            </div>
-          ))}
-          <SavePageControls slackChannels={slackChannelsStr} />
+        <form className="m-2">
+          <SavePageControls />
         </form>
         </form>
       </div>
       </div>
     </div>
     </div>

+ 9 - 4
apps/app/src/components/PageEditor/OptionsSelector.tsx

@@ -13,6 +13,9 @@ import {
 
 
 import { useIsIndentSizeForced } from '~/stores/context';
 import { useIsIndentSizeForced } from '~/stores/context';
 import { useEditorSettings, useCurrentIndentSize } from '~/stores/editor';
 import { useEditorSettings, useCurrentIndentSize } from '~/stores/editor';
+import {
+  useIsDeviceLargerThanMd,
+} from '~/stores/ui';
 
 
 type RadioListItemProps = {
 type RadioListItemProps = {
   onClick: () => void,
   onClick: () => void,
@@ -256,7 +259,7 @@ const OptionsStatus = {
 } as const;
 } as const;
 type OptionStatus = typeof OptionsStatus[keyof typeof OptionsStatus];
 type OptionStatus = typeof OptionsStatus[keyof typeof OptionsStatus];
 
 
-export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Element => {
+export const OptionsSelector = (): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
@@ -266,6 +269,7 @@ export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Eleme
   const { data: editorSettings } = useEditorSettings();
   const { data: editorSettings } = useEditorSettings();
   const { data: currentIndentSize } = useCurrentIndentSize();
   const { data: currentIndentSize } = useCurrentIndentSize();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
+  const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
 
 
   if (editorSettings == null || currentIndentSize == null || isIndentSizeForced == null) {
   if (editorSettings == null || currentIndentSize == null || isIndentSizeForced == null) {
     return <></>;
     return <></>;
@@ -275,14 +279,15 @@ export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Eleme
     <Dropdown isOpen={dropdownOpen} toggle={() => { setStatus(OptionsStatus.Home); setDropdownOpen(!dropdownOpen) }} direction="up" className="">
     <Dropdown isOpen={dropdownOpen} toggle={() => { setStatus(OptionsStatus.Home); setDropdownOpen(!dropdownOpen) }} direction="up" className="">
       <DropdownToggle
       <DropdownToggle
         className={`btn btn-sm btn-outline-neutral-secondary d-flex align-items-center justify-content-center
         className={`btn btn-sm btn-outline-neutral-secondary d-flex align-items-center justify-content-center
-              ${collapsed ? 'border-0' : 'border border-secondary'}
+              ${isDeviceLargerThanMd ? '' : 'border-0'}
               ${dropdownOpen ? 'active' : ''}
               ${dropdownOpen ? 'active' : ''}
               `}
               `}
       >
       >
         <span className="material-symbols-outlined py-0 fs-5"> settings </span>
         <span className="material-symbols-outlined py-0 fs-5"> settings </span>
         {
         {
-          collapsed ? <></>
-            : <label className="ms-1 me-1">{t('page_edit.editor_config')}</label>
+          isDeviceLargerThanMd
+            ? <label className="ms-1 me-1">{t('page_edit.editor_config')}</label>
+            : <></>
         }
         }
       </DropdownToggle>
       </DropdownToggle>
       <DropdownMenu container="body">
       <DropdownMenu container="body">

+ 204 - 54
apps/app/src/components/SavePageControls.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useState, useEffect } from 'react';
 
 
 import type EventEmitter from 'events';
 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 { useTranslation } from 'next-i18next';
 import {
 import {
   UncontrolledButtonDropdown, Button,
   UncontrolledButtonDropdown, Button,
-  DropdownToggle, DropdownMenu, DropdownItem,
+  DropdownToggle, DropdownMenu, DropdownItem, Modal,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { IPageGrantData } from '~/interfaces/page';
 import type { IPageGrantData } from '~/interfaces/page';
 import {
 import {
   useIsEditable, useIsAclEnabled,
   useIsEditable, useIsAclEnabled,
+  useIsSlackConfigured,
 } from '~/stores/context';
 } 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 { mutatePageTree } from '~/stores/page-listing';
-import { useSelectedGrant } from '~/stores/ui';
+import {
+  useSelectedGrant,
+  useEditorMode, useIsDeviceLargerThanMd,
+  EditorMode,
+} from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+
 import { unpublish } from '../client/services/page-operation';
 import { unpublish } from '../client/services/page-operation';
 
 
+
 import { GrantSelector } from './SavePageControls/GrantSelector';
 import { GrantSelector } from './SavePageControls/GrantSelector';
+import { SlackNotification } from './SlackNotification';
 
 
 
 
 declare global {
 declare global {
@@ -33,25 +41,19 @@ declare global {
 
 
 const logger = loggerFactory('growi:SavePageControls');
 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 { t } = useTranslation();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
-  const { data: isEditable } = useIsEditable();
-  const { data: isAclEnabled } = useIsAclEnabled();
-  const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { data: _isWaitingSaveProcessing } = useWaitingSaveProcessing();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   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> => {
   const save = useCallback(async(): Promise<void> => {
     // save
     // save
@@ -74,46 +76,21 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
       await unpublish(pageId);
       await unpublish(pageId);
       await mutateCurrentPage();
       await mutateCurrentPage();
       await mutatePageTree();
       await mutatePageTree();
+      await mutateEditorMode(EditorMode.View);
       toastSuccess(t('wip_page.success_save_as_wip'));
       toastSuccess(t('wip_page.success_save_as_wip'));
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
       toastError(t('wip_page.fail_save_as_wip'));
       toastError(t('wip_page.fail_save_as_wip'));
     }
     }
-  }, [currentPage?._id, mutateCurrentPage, t]);
-
-
-  if (isEditable == null || isAclEnabled == null || grantData == null) {
-    return null;
-  }
-
-  if (!isEditable) {
-    return null;
-  }
+  }, [currentPage?._id, mutateCurrentPage, mutateEditorMode, t]);
 
 
-  const { grant, userRelatedGrantedGroups } = grantData;
-
-  const isGrantSelectorDisabledPage = isTopPage(currentPage?.path ?? '') || isUsersProtectedPages(currentPage?.path ?? '');
   const labelSubmitButton = t('Update');
   const labelSubmitButton = t('Update');
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
   const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
   const labelUnpublishPage = t('wip_page.save_as_wip');
   const labelUnpublishPage = t('wip_page.save_as_wip');
 
 
   return (
   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">
       <UncontrolledButtonDropdown direction="up" size="sm">
         <Button
         <Button
           id="caret"
           id="caret"
@@ -128,17 +105,190 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
           )}
           )}
           {labelSubmitButton}
           {labelSubmitButton}
         </Button>
         </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>
       </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>
     </div>
   );
   );
 };
 };

+ 16 - 13
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -21,7 +21,7 @@ const AVAILABLE_GRANTS = [
     grant: PageGrant.GRANT_PUBLIC, iconName: 'group', btnStyleClass: 'outline-info', label: 'Public',
     grant: PageGrant.GRANT_PUBLIC, iconName: 'group', btnStyleClass: 'outline-info', label: 'Public',
   },
   },
   {
   {
-    grant: PageGrant.GRANT_RESTRICTED, iconName: 'link', btnStyleClass: 'outline-teal', label: 'Anyone with the link',
+    grant: PageGrant.GRANT_RESTRICTED, iconName: 'link', btnStyleClass: 'outline-success', label: 'Anyone with the link',
   },
   },
   // { grant: 3, iconClass: '', label: 'Specified users only' },
   // { grant: 3, iconClass: '', label: 'Specified users only' },
   {
   {
@@ -30,7 +30,7 @@ const AVAILABLE_GRANTS = [
   {
   {
     grant: PageGrant.GRANT_USER_GROUP,
     grant: PageGrant.GRANT_USER_GROUP,
     iconName: 'more_horiz',
     iconName: 'more_horiz',
-    btnStyleClass: 'outline-purple',
+    btnStyleClass: 'outline-warning',
     label: 'Only inside the group',
     label: 'Only inside the group',
     reselectLabel: 'Reselect the group',
     reselectLabel: 'Reselect the group',
   },
   },
@@ -39,6 +39,7 @@ const AVAILABLE_GRANTS = [
 
 
 type Props = {
 type Props = {
   disabled?: boolean,
   disabled?: boolean,
+  openInModal?: boolean,
   grant: PageGrant,
   grant: PageGrant,
   userRelatedGrantedGroups?: {
   userRelatedGrantedGroups?: {
     id: string,
     id: string,
@@ -57,6 +58,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
   const {
   const {
     disabled,
     disabled,
+    openInModal,
     userRelatedGrantedGroups,
     userRelatedGrantedGroups,
     onUpdateGrant,
     onUpdateGrant,
     grant: currentGrant,
     grant: currentGrant,
@@ -118,7 +120,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
         : opt.label;
         : opt.label;
 
 
       const labelElm = (
       const labelElm = (
-        <span>
+        <span className={openInModal ? 'py-2' : ''}>
           <span className="material-symbols-outlined me-2">{opt.iconName}</span>
           <span className="material-symbols-outlined me-2">{opt.iconName}</span>
           <span className="label">{t(label)}</span>
           <span className="label">{t(label)}</span>
         </span>
         </span>
@@ -158,17 +160,17 @@ export const GrantSelector = (props: Props): JSX.Element => {
 
 
     return (
     return (
       <div className="grw-grant-selector mb-0" data-testid="grw-grant-selector">
       <div className="grw-grant-selector mb-0" data-testid="grw-grant-selector">
-        <UncontrolledDropdown direction="up" size="sm">
-          <DropdownToggle color={dropdownToggleBtnColor} caret className="d-flex justify-content-between align-items-center" disabled={disabled}>
+        <UncontrolledDropdown direction={openInModal ? 'down' : 'up'} size="sm">
+          <DropdownToggle color={dropdownToggleBtnColor} caret className="w-100 d-flex justify-content-between align-items-center" disabled={disabled}>
             {dropdownToggleLabelElm}
             {dropdownToggleLabelElm}
           </DropdownToggle>
           </DropdownToggle>
-          <DropdownMenu container="body">
+          <DropdownMenu container={openInModal ? '' : 'body'}>
             {dropdownMenuElems}
             {dropdownMenuElems}
           </DropdownMenu>
           </DropdownMenu>
         </UncontrolledDropdown>
         </UncontrolledDropdown>
       </div>
       </div>
     );
     );
-  }, [changeGrantHandler, currentGrant, disabled, userRelatedGrantedGroups, t]);
+  }, [changeGrantHandler, currentGrant, disabled, userRelatedGrantedGroups, t, openInModal]);
 
 
   /**
   /**
    * Render select grantgroup modal.
    * Render select grantgroup modal.
@@ -199,7 +201,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
     }
     }
 
 
     return (
     return (
-      <>
+      <div className="d-flex flex-column">
         { myUserGroups.map((group) => {
         { myUserGroups.map((group) => {
           const groupIsGranted = userRelatedGrantedGroups?.find(g => g.id === group.item._id) != null;
           const groupIsGranted = userRelatedGrantedGroups?.find(g => g.id === group.item._id) != null;
           const activeClass = groupIsGranted ? 'active' : '';
           const activeClass = groupIsGranted ? 'active' : '';
@@ -212,14 +214,14 @@ export const GrantSelector = (props: Props): JSX.Element => {
               onClick={() => groupListItemClickHandler(group)}
               onClick={() => groupListItemClickHandler(group)}
             >
             >
               <span className="align-middle"><input type="checkbox" checked={groupIsGranted} /></span>
               <span className="align-middle"><input type="checkbox" checked={groupIsGranted} /></span>
-              <h5 className="d-inline-block ml-3">{group.item.name}</h5>
-              {group.type === GroupType.externalUserGroup && <span className="ml-2 badge badge-pill badge-info">{group.item.provider}</span>}
+              <h5 className="d-inline-block ms-3">{group.item.name}</h5>
+              {group.type === GroupType.externalUserGroup && <span className="ms-2 badge badge-pill badge-info">{group.item.provider}</span>}
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
               {/* TODO: Replace <div className="small">(TBD) List group members</div> */}
             </button>
             </button>
           );
           );
         }) }
         }) }
-        <button type="button" className="btn btn-primary mt-2 float-right" onClick={() => setIsSelectGroupModalShown(false)}>{t('Done')}</button>
-      </>
+        <button type="button" className="btn btn-primary mt-2 mx-auto" onClick={() => setIsSelectGroupModalShown(false)}>{t('Done')}</button>
+      </div>
     );
     );
 
 
   }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, userRelatedGrantedGroups]);
   }, [currentUser?.admin, groupListItemClickHandler, myUserGroups, shouldFetch, t, userRelatedGrantedGroups]);
@@ -233,8 +235,9 @@ export const GrantSelector = (props: Props): JSX.Element => {
         <Modal
         <Modal
           isOpen={isSelectGroupModalShown}
           isOpen={isSelectGroupModalShown}
           toggle={() => setIsSelectGroupModalShown(false)}
           toggle={() => setIsSelectGroupModalShown(false)}
+          centered
         >
         >
-          <ModalHeader tag="h4" toggle={() => setIsSelectGroupModalShown(false)} className="bg-purple text-light">
+          <ModalHeader tag="h4" toggle={() => setIsSelectGroupModalShown(false)} className="bg-purple text-muted">
             {t('user_group.select_group')}
             {t('user_group.select_group')}
           </ModalHeader>
           </ModalHeader>
           <ModalBody>
           <ModalBody>

+ 0 - 19
apps/app/src/components/SlackLogo.jsx

@@ -1,19 +0,0 @@
-import React from 'react';
-
-export const SlackLogo = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 448 448"
-    height="20"
-    width="20"
-  >
-    <path
-      d="M94.12,283.1A47.06,47.06,0,1,1,47.06,236H94.12Zm23.72,
-      0a47.06,47.06,0,1,1,94.12,0V400.94a47.06,47.06,0,1,1-94.12,0Zm47.06-189A47.06,
-      47.06,0,1,1,212,47.06V94.12Zm0,23.72a47.06,47.06,0,0,1,0,94.12H47.06a47.06,47.06,
-      0,0,1,0-94.12Zm189,47.06A47.06,47.06,0,1,1,400.94,212H353.88V164.9Zm-23.72,0a47.06,
-      47.06,0,1,1-94.12,0V47.06a47.06,47.06,0,1,1,94.12,0V164.9Zm-47.06,189A47.06,47.06,
-      0,1,1,236,400.94V353.88Zm0-23.72a47.06,47.06,0,0,1,0-94.12H400.94a47.06,47.06,0,0,1,0,94.12Z"
-    />
-  </svg>
-);

+ 55 - 39
apps/app/src/components/SlackNotification.module.scss

@@ -1,45 +1,61 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '@growi/core/scss/bootstrap/init' as bs;
 
 
-// TODO: activate (https://redmine.weseek.co.jp/issues/128307)
-.grw-slack-notification :global {
-  // $input-height-slack: bs.$form-check-indicator-size * 1.5;
-  // border-color: bs.$gray-200;
 
 
-  // border-style: solid;
-  // border-width: 1px;
-  // border-radius: $input-height-slack/2 2px 2px $input-height-slack/2;
+.grw-slack-switch :global {
+  .input-group-text {
+    background-color: inherit;
+  }
+  .form-check-input {
+    cursor: pointer;
+    background-repeat: no-repeat;
+    background-attachment:scroll;
+    background-clip: border-box;
+    background-origin: padding-box;
+    background-size: 30%, 45%;
+    box-shadow: none;
+    transition: all 0.4s ease-out;
+  }
+  .form-control::placeholder {
+    color: bs.$gray-500
+  }
+}
+
+:root[data-bs-theme='light'] {
+  .grw-slack-switch :global {
+    .form-check-input:not(:checked) {
+      background-color: bs.$gray-200;
+      background-image:
+        url('/images/icons/slack/slack-logo-off.svg'),
+        url('/images/icons/slack/slack-logo-background.svg');
+      background-position: 15%, 5%, 50%, 50%;
+    }
+
+    .form-check-input:checked {
+      background-color: #E7A9E8;
+      background-image:
+        url('/images/icons/slack/slack-logo-on.svg'),
+        url('/images/icons/slack/slack-logo-background.svg');
+      background-position: 85%, 95%, 50%, 50%;
+    }
+  }
+}
 
 
-  // .form-control {
-  //   height: $input-height-slack;
-  //   border: transparent;
-  //   @include bs.media-breakpoint-up(sm) {
-  //     width: 130px;
-  //   }
-  //   @include bs.media-breakpoint-up(md) {
-  //     width: 180px;
-  //   }
-  // }
-  // // height settings for slack button's responsive design
-  // // in the input and form-control element
-  // .grw-form-control-slack-notification.form-control {
-  //   height: $input-height-slack;
-  // }
-  // .grw-input-group-slack-notification {
-  //   height: $input-height-slack;
-  //   label {
-  //     display: flex;
-  //     align-items: center;
-  //     justify-content: center;
-  //     margin-bottom: 0;
-  //   }
-  // }
+:root[data-bs-theme='dark'] {
+  .grw-slack-switch :global {
+    .form-check-input:not(:checked) {
+      background-color: bs.$gray-200;
+      background-image:
+        url('/images/icons/slack/slack-logo-dark-off.svg'),
+        url('/images/icons/slack/slack-logo-dark-background.svg');
+      background-position: 14%, 4%, 50%, 50%;
+    }
 
 
-  // .form-check-label {
-  //   &::before {
-  //     border: transparent;
-  //   }
-  // }
+    .form-check-input:checked {
+      background-color: #731f74;
+      background-image:
+        url('/images/icons/slack/slack-logo-dark-on.svg'),
+        url('/images/icons/slack/slack-logo-dark-background.svg');
+      background-position: 86%, 95%, 50%, 50%;
+    }
+  }
 }
 }
-// TODO デザインの使用が確定して実装、本タスクのスコープ外
-// .grw-slack-notification-xd {
-// }

+ 31 - 31
apps/app/src/components/SlackNotification.tsx

@@ -1,9 +1,11 @@
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 import type { FC } from 'react';
 import type { FC } from 'react';
-import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import { PopoverBody, PopoverHeader, UncontrolledPopover } from 'reactstrap';
+import {
+  FormGroup, Input, InputGroup, InputGroupText,
+  PopoverBody, PopoverHeader, UncontrolledPopover,
+} from 'reactstrap';
 
 
 import styles from './SlackNotification.module.scss';
 import styles from './SlackNotification.module.scss';
 
 
@@ -19,6 +21,7 @@ type SlackNotificationProps = {
 export const SlackNotification: FC<SlackNotificationProps> = ({
 export const SlackNotification: FC<SlackNotificationProps> = ({
   id, isSlackEnabled, slackChannels, onEnabledFlagChange, onChannelChange,
   id, isSlackEnabled, slackChannels, onEnabledFlagChange, onChannelChange,
 }) => {
 }) => {
+
   const { t } = useTranslation();
   const { t } = useTranslation();
   const idForSlackPopover = `${id}ForSlackPopover`;
   const idForSlackPopover = `${id}ForSlackPopover`;
 
 
@@ -38,34 +41,31 @@ export const SlackNotification: FC<SlackNotificationProps> = ({
 
 
 
 
   return (
   return (
-    <div className={`grw-slack-notification ${styles['grw-slack-notification']} w-100`}>
-      <div className="grw-input-group-slack-notification input-group extended-setting">
-        <label className="form-label input-group-addon">
-          <div className="form-check form-switch form-switch-lg form-switch-slack">
-            <input
-              type="checkbox"
-              className="form-check-input border-0"
-              id={id}
-              checked={isSlackEnabled}
-              onChange={updateCheckboxHandler}
-            />
-            <label className="form-label form-check-label align-center" htmlFor={id}></label>
-          </div>
-        </label>
-        <input
-          className="grw-form-control-slack-notification form-control align-top ps-0"
-          id={idForSlackPopover}
-          type="text"
-          value={slackChannels}
-          placeholder={t('page_edit.input_channels', 'Input channels')}
-          onChange={updateSlackChannelsHandler}
-        />
-        <UncontrolledPopover trigger="focus" placement="top" target={idForSlackPopover}>
-          <PopoverHeader>{t('slack_notification.popover_title')}</PopoverHeader>
-          <PopoverBody>{t('slack_notification.popover_desc')}</PopoverBody>
-        </UncontrolledPopover>
-      </div>
-    </div>
-
+    <InputGroup className={`d-flex align-items-center ${styles['grw-slack-switch']}`}>
+      <InputGroupText className="rounded-pill rounded-end border-end-0 p-0 pe-1 grw-slack-switch">
+        <FormGroup switch className="position-relative pe-4 py-3 m-0 me-2">
+          <Input
+            className="position-absolute bottom-0 start-0 p-0 m-0 w-100 h-100 border-0"
+            type="switch"
+            role="switch"
+            id={id}
+            checked={isSlackEnabled}
+            onChange={updateCheckboxHandler}
+          />
+        </FormGroup>
+      </InputGroupText>
+      <Input
+        className="rounded-pill rounded-start border-start-0 py-1"
+        id={idForSlackPopover}
+        type="text"
+        value={slackChannels}
+        placeholder="Input channels"
+        onChange={updateSlackChannelsHandler}
+      />
+      <UncontrolledPopover trigger="focus" placement="top" target={idForSlackPopover}>
+        <PopoverHeader>{t('slack_notification.popover_title')}</PopoverHeader>
+        <PopoverBody>{t('slack_notification.popover_desc')}</PopoverBody>
+      </UncontrolledPopover>
+    </InputGroup>
   );
   );
 };
 };

+ 95 - 92
apps/app/src/styles/atoms/_custom_control.scss

@@ -2,95 +2,98 @@
 
 
 // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
 // TODO: activate (https://redmine.weseek.co.jp/issues/128307)
 
 
-// .form-check .form-check-label::before {
-//   border-radius: $border-radius !important;
-// }
-
-// label.form-check-label {
-//   font-weight: normal;
-// }
-
-// .form-switch.form-switch-sm {
-//   $form-check-indicator-size-sm: $form-check-indicator-size * 0.8;
-//   $form-switch-width-sm: $form-check-indicator-size-sm * 1.75;
-//   $form-check-gutter-sm: $form-check-gutter * 0.8;
-//   $form-check-indicator-size-sm: $form-check-indicator-size * 0.8;
-//   $form-switch-indicator-size-sm: subtract($form-check-indicator-size-sm, $form-check-indicator-border-width * 4);
-
-//   padding-left: $form-switch-width-sm + $form-check-gutter-sm;
-
-//   .form-check-label {
-//     &::before {
-//       left: -($form-switch-width-sm + $form-check-gutter-sm);
-//       width: $form-switch-width-sm;
-//       height: $form-check-indicator-size-sm;
-//     }
-
-//     &::after {
-//       top: add(($font-size-base * $line-height-base - $form-check-indicator-size) / 2, $form-check-indicator-border-width * 2);
-//       left: add(-($form-switch-width-sm + $form-check-gutter-sm), $form-check-indicator-border-width * 2);
-//       width: $form-switch-indicator-size-sm;
-//       height: $form-switch-indicator-size-sm;
-//     }
-//   }
-
-//   .form-check-input:checked ~ .form-check-label {
-//     &::after {
-//       transform: translateX($form-switch-width-sm - $form-check-indicator-size-sm);
-//     }
-//   }
-// }
-
-// //lg
-// .form-switch.form-switch-lg {
-//   $form-check-indicator-size-lg: $form-check-indicator-size * 1.5;
-//   $form-switch-width-lg: $form-check-indicator-size-lg * 1.75;
-//   $form-check-gutter-lg: $form-check-gutter * 1.5;
-//   $form-check-indicator-size-lg: $form-check-indicator-size * 1.5;
-//   $form-switch-indicator-size-lg: subtract($form-check-indicator-size-lg, $form-check-indicator-border-width * 4);
-
-//   padding-left: $form-switch-width-lg + $form-check-gutter-lg;
-
-//   line-height: $form-check-indicator-size-lg;
-//   .form-check-label {
-//     &::before {
-//       top: ($font-size-base * $line-height-base - $form-check-indicator-size-lg) / 2;
-
-//       left: -($form-switch-width-lg + $form-check-gutter-lg);
-//       width: $form-switch-width-lg;
-//       height: $form-check-indicator-size-lg;
-//       border-radius: $form-check-indicator-size-lg/2;
-//     }
-
-//     &::after {
-//       top: add(($font-size-base * $line-height-base - $form-check-indicator-size-lg) / 2, $form-check-indicator-border-width * 2);
-//       left: add(-($form-switch-width-lg + $form-check-gutter-lg), $form-check-indicator-border-width * 2);
-//       width: $form-switch-indicator-size-lg;
-//       height: $form-switch-indicator-size-lg;
-//       border-radius: $form-check-indicator-size-lg/2;
-//     }
-//   }
-
-//   .form-check-input:checked ~ .form-check-label {
-//     &::after {
-//       transform: translateX($form-switch-width-lg - $form-check-indicator-size-lg);
-//     }
-//   }
-// }
-
-// .form-switch.form-switch-slack {
-//   .form-check-label {
-//     &::before {
-//       background-color: $gray-200;
-//       border-color: transparent;
-//     }
-//     &::after {
-//       background-size: 15px;
-//     }
-//   }
-//   .input-group-addon {
-//     input {
-//       vertical-align: middle;
-//     }
-//   }
-// }
+$form-check-gutter: .5rem;
+$form-check-indicator-border-width: 1px;
+
+.form-check .form-check-label::before {
+  border-radius: $border-radius !important;
+}
+
+label.form-check-label {
+  font-weight: normal;
+}
+
+.form-switch.form-switch-sm {
+  $form-check-indicator-size-sm: $form-check-indicator-size * 0.8;
+  $form-switch-width-sm: $form-check-indicator-size-sm * 1.75;
+  $form-check-gutter-sm: $form-check-gutter * 0.8;
+  $form-check-indicator-size-sm: $form-check-indicator-size * 0.8;
+  $form-switch-indicator-size-sm: subtract($form-check-indicator-size-sm, $form-check-indicator-border-width * 4);
+
+  padding-left: $form-switch-width-sm + $form-check-gutter-sm;
+
+  .form-check-label {
+    &::before {
+      left: -($form-switch-width-sm + $form-check-gutter-sm);
+      width: $form-switch-width-sm;
+      height: $form-check-indicator-size-sm;
+    }
+
+    &::after {
+      top: add(($font-size-base * $line-height-base - $form-check-indicator-size) / 2, $form-check-indicator-border-width * 2);
+      left: add(-($form-switch-width-sm + $form-check-gutter-sm), $form-check-indicator-border-width * 2);
+      width: $form-switch-indicator-size-sm;
+      height: $form-switch-indicator-size-sm;
+    }
+  }
+
+  .form-check-input:checked ~ .form-check-label {
+    &::after {
+      transform: translateX($form-switch-width-sm - $form-check-indicator-size-sm);
+    }
+  }
+}
+
+//lg
+.form-switch.form-switch-lg {
+  $form-check-indicator-size-lg: $form-check-indicator-size * 1.5;
+  $form-switch-width-lg: $form-check-indicator-size-lg * 1.75;
+  $form-check-gutter-lg: $form-check-gutter * 1.5;
+  $form-check-indicator-size-lg: $form-check-indicator-size * 1.5;
+  $form-switch-indicator-size-lg: subtract($form-check-indicator-size-lg, $form-check-indicator-border-width * 4);
+
+  padding-left: $form-switch-width-lg + $form-check-gutter-lg;
+
+  line-height: $form-check-indicator-size-lg;
+  .form-check-label {
+    &::before {
+      top: ($font-size-base * $line-height-base - $form-check-indicator-size-lg) / 2;
+
+      left: -($form-switch-width-lg + $form-check-gutter-lg);
+      width: $form-switch-width-lg;
+      height: $form-check-indicator-size-lg;
+      border-radius: $form-check-indicator-size-lg/2;
+    }
+
+    &::after {
+      top: add(($font-size-base * $line-height-base - $form-check-indicator-size-lg) / 2, $form-check-indicator-border-width * 2);
+      left: add(-($form-switch-width-lg + $form-check-gutter-lg), $form-check-indicator-border-width * 2);
+      width: $form-switch-indicator-size-lg;
+      height: $form-switch-indicator-size-lg;
+      border-radius: $form-check-indicator-size-lg/2;
+    }
+  }
+
+  .form-check-input:checked ~ .form-check-label {
+    &::after {
+      transform: translateX($form-switch-width-lg - $form-check-indicator-size-lg);
+    }
+  }
+}
+
+.form-switch.form-switch-slack {
+  .form-check-label {
+    &::before {
+      background-color: $gray-200;
+      border-color: transparent;
+    }
+    &::after {
+      background-size: 15px;
+    }
+  }
+  .input-group-addon {
+    input {
+      vertical-align: middle;
+    }
+  }
+}

+ 1 - 1
packages/core/scss/bootstrap/_variables.scss

@@ -164,4 +164,4 @@ $pre-color: dummyinvalildcolor; // disable pre color specification with invalid
 //== Custom Checkbox
 //== Custom Checkbox
 // $form-check-indicator-border-radius: 0px;
 // $form-check-indicator-border-radius: 0px;
 // $form-check-indicator-focus-box-shadow: none;
 // $form-check-indicator-focus-box-shadow: none;
-// $form-check-indicator-size: 1.2rem;
+$form-check-indicator-size: 1.2rem;