Sfoglia il codice sorgente

Merge pull request #8765 from weseek/fix/admin-customize-styles

fix: Admin customize screen
Yuki Takei 1 anno fa
parent
commit
09aaee909d

+ 17 - 0
apps/app/src/components/Admin/Common/AdminNavigation.module.scss

@@ -0,0 +1,17 @@
+// button layout
+.admin-navigation {
+  &:global {
+    & > a + a {
+      margin-top: 2px;
+    }
+  }
+}
+
+// sticky settings
+.admin-navigation {
+  &:global {
+    &.sticky-top {
+      top: 30px;
+    }
+  }
+}

+ 8 - 3
apps/app/src/components/Admin/Common/AdminNavigation.tsx

@@ -7,6 +7,11 @@ import urljoin from 'url-join';
 
 
 import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../../stores/context';
 import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../../stores/context';
 
 
+import styles from './AdminNavigation.module.scss';
+
+const moduleClass = styles['admin-navigation'];
+
+
 // eslint-disable-next-line react/prop-types
 // eslint-disable-next-line react/prop-types
 const MenuLabel = ({ menu }: { menu: string }) => {
 const MenuLabel = ({ menu }: { menu: string }) => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
@@ -82,7 +87,7 @@ export const AdminNavigation = (): JSX.Element => {
 
 
   }, [pathname]);
   }, [pathname]);
 
 
-  const getListGroupItemOrDropdownItemList = (isListGroupItems: boolean) => {
+  const getListGroupItemOrDropdownItemList = useCallback((isListGroupItems: boolean) => {
     return (
     return (
       <>
       <>
         {/* eslint-disable no-multi-spaces */}
         {/* eslint-disable no-multi-spaces */}
@@ -115,12 +120,12 @@ export const AdminNavigation = (): JSX.Element => {
         {/* eslint-enable no-multi-spaces */}
         {/* eslint-enable no-multi-spaces */}
       </>
       </>
     );
     );
-  };
+  }, [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       {/* List group */}
       {/* List group */}
-      <div className="list-group admin-navigation sticky-top d-none d-lg-block">
+      <div className={`list-group ${moduleClass} sticky-top d-none d-lg-block`}>
         {getListGroupItemOrDropdownItemList(true)}
         {getListGroupItemOrDropdownItemList(true)}
       </div>
       </div>
 
 

+ 17 - 8
apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -30,7 +30,6 @@ const CustomizeLayoutSetting = (): JSX.Element => {
   const { resolvedTheme } = useNextThemes();
   const { resolvedTheme } = useNextThemes();
 
 
   const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
   const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
-  const [retrieveError, setRetrieveError] = useState<any>();
 
 
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     if (isContainerFluid == null) { return }
     if (isContainerFluid == null) { return }
@@ -58,14 +57,19 @@ const CustomizeLayoutSetting = (): JSX.Element => {
           <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
           <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
 
 
           <div className="d-flex justify-content-around mt-5">
           <div className="d-flex justify-content-around mt-5">
-            <div id="layoutOptions" className="row row-cols-2">
+            <div className="row row-cols-2">
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${!isContainerFluid ? 'border-active' : ''}`}
+                  className={`card border border-4 ${!isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(false)}
                   onClick={() => setIsContainerFluid(false)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={`/images/customize-settings/default-${resolvedTheme}.svg`} />
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img
+                    className="card-img-top"
+                    src={`/images/customize-settings/default-${resolvedTheme}.svg`}
+                    alt={t('customize_settings.layout_options.default')}
+                  />
                   <div className="card-body text-center">
                   <div className="card-body text-center">
                     {t('customize_settings.layout_options.default')}
                     {t('customize_settings.layout_options.default')}
                   </div>
                   </div>
@@ -73,12 +77,17 @@ const CustomizeLayoutSetting = (): JSX.Element => {
               </div>
               </div>
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${isContainerFluid ? 'border-active' : ''}`}
+                  className={`card border border-4 ${isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(true)}
                   onClick={() => setIsContainerFluid(true)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={`/images/customize-settings/fluid-${resolvedTheme}.svg`} />
-                  <div className="card-body  text-center">
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img
+                    className="card-img-top"
+                    src={`/images/customize-settings/fluid-${resolvedTheme}.svg`}
+                    alt={t('customize_settings.layout_options.expanded')}
+                  />
+                  <div className="card-body text-center">
                     {t('customize_settings.layout_options.expanded')}
                     {t('customize_settings.layout_options.expanded')}
                   </div>
                   </div>
                 </div>
                 </div>
@@ -88,7 +97,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
 
 
           <div className="row my-3">
           <div className="row my-3">
             <div className="mx-auto">
             <div className="mx-auto">
-              <button type="button" className="btn btn-primary" onClick={onClickSubmit} disabled={retrieveError != null}>{ t('Update') }</button>
+              <button type="button" className="btn btn-primary" onClick={onClickSubmit}>{ t('Update') }</button>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>

+ 21 - 12
apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
@@ -11,7 +12,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
 
 
   const {
   const {
-    update, isSidebarCollapsedMode, setIsSidebarCollapsedMode,
+    data, update, setIsSidebarCollapsedMode, setIsSidebarClosedAtDockMode,
   } = useSWRxSidebarConfig();
   } = useSWRxSidebarConfig();
 
 
   const { resolvedTheme } = useNextThemes();
   const { resolvedTheme } = useNextThemes();
@@ -28,6 +29,12 @@ const CustomizeSidebarsetting = (): JSX.Element => {
     }
     }
   }, [t, update]);
   }, [t, update]);
 
 
+  if (data == null) {
+    return <LoadingSpinner />;
+  }
+
+  const { isSidebarCollapsedMode, isSidebarClosedAtDockMode } = data;
+
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
@@ -42,14 +49,15 @@ const CustomizeSidebarsetting = (): JSX.Element => {
           </Card>
           </Card>
 
 
           <div className="d-flex justify-content-around mt-5">
           <div className="d-flex justify-content-around mt-5">
-            <div id="layoutOptions" className="row row-cols-2">
+            <div className="row row-cols-2">
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  className={`card border border-4 ${isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(true)}
                   onClick={() => setIsSidebarCollapsedMode(true)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={drawerIconFileName} />
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img src={drawerIconFileName} alt="Drawer Mode" />
                   <div className="card-body text-center">
                   <div className="card-body text-center">
                     Drawer Mode
                     Drawer Mode
                   </div>
                   </div>
@@ -57,11 +65,12 @@ const CustomizeSidebarsetting = (): JSX.Element => {
               </div>
               </div>
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${!isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  className={`card border border-4 ${!isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(false)}
                   onClick={() => setIsSidebarCollapsedMode(false)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={dockIconFileName} />
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img src={dockIconFileName} alt="Dock Mode" />
                   <div className="card-body  text-center">
                   <div className="card-body  text-center">
                     Dock Mode
                     Dock Mode
                   </div>
                   </div>
@@ -82,9 +91,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 type="radio"
                 type="radio"
                 id="is-open"
                 id="is-open"
                 className="form-check-input"
                 className="form-check-input"
-                name="mailVisibility"
-                checked={isSidebarCollapsedMode === false}
-                onChange={() => setIsSidebarCollapsedMode(false)}
+                checked={isSidebarCollapsedMode === false && isSidebarClosedAtDockMode === false}
+                disabled={isSidebarCollapsedMode}
+                onChange={() => setIsSidebarClosedAtDockMode(false)}
               />
               />
               <label className="form-label form-check-label" htmlFor="is-open">
               <label className="form-label form-check-label" htmlFor="is-open">
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
@@ -95,9 +104,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 type="radio"
                 type="radio"
                 id="is-closed"
                 id="is-closed"
                 className="form-check-input"
                 className="form-check-input"
-                name="mailVisibility"
-                checked={isSidebarCollapsedMode === true}
-                onChange={() => setIsSidebarCollapsedMode(true)}
+                checked={isSidebarCollapsedMode === false && isSidebarClosedAtDockMode === true}
+                disabled={isSidebarCollapsedMode}
+                onChange={() => setIsSidebarClosedAtDockMode(true)}
               />
               />
               <label className="form-label form-check-label" htmlFor="is-closed">
               <label className="form-label form-check-label" htmlFor="is-closed">
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_close')}
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_close')}

+ 7 - 4
apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -25,11 +25,12 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
   }, [availableThemes]);
   }, [availableThemes]);
 
 
   return (
   return (
-    <div id="themeOptions">
+    <>
+
       {/* Light and Dark Themes */}
       {/* Light and Dark Themes */}
       <div>
       <div>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
-        <div className="d-flex flex-wrap">
+        <div className="hstack gap-3">
           {lightNDarkThemes.map((theme) => {
           {lightNDarkThemes.map((theme) => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
@@ -42,10 +43,11 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
           })}
         </div>
         </div>
       </div>
       </div>
+
       {/* Only one mode Theme */}
       {/* Only one mode Theme */}
       <div className="mt-3">
       <div className="mt-3">
         <h3>{t('customize_settings.theme_desc.unique')}</h3>
         <h3>{t('customize_settings.theme_desc.unique')}</h3>
-        <div className="d-flex flex-wrap">
+        <div className="hstack gap-3">
           {oneModeThemes.map((theme) => {
           {oneModeThemes.map((theme) => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
@@ -58,7 +60,8 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
           })}
         </div>
         </div>
       </div>
       </div>
-    </div>
+
+    </>
   );
   );
 
 
 };
 };

+ 6 - 0
apps/app/src/components/Admin/Customize/ThemeColorBox.module.scss

@@ -0,0 +1,6 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+// layout
+.theme-option-container :global {
+  min-width: 100px;
+}

+ 15 - 5
apps/app/src/components/Admin/Customize/ThemeColorBox.tsx

@@ -2,6 +2,10 @@ import React from 'react';
 
 
 import type { GrowiThemeMetadata } from '@growi/core';
 import type { GrowiThemeMetadata } from '@growi/core';
 
 
+import styles from './ThemeColorBox.module.scss';
+
+const themeOptionClass = styles['theme-option-container'];
+
 
 
 type Props = {
 type Props = {
   isSelected: boolean,
   isSelected: boolean,
@@ -19,13 +23,19 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
   } = metadata;
   } = metadata;
 
 
   return (
   return (
-    // TODO: Display a primary color border when icon is selected
     <div
     <div
       id={`theme-option-${name}`}
       id={`theme-option-${name}`}
-      className={`theme-option-container d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
+      className={`${themeOptionClass} d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
       onClick={onSelected}
       onClick={onSelected}
     >
     >
-      <a id={name} role="button" className={`m-0 rounded ${name} theme-button`}>
+      <a
+        id={name}
+        role="button"
+        className={`
+          m-0 rounded rounded-3
+          border border-4 border-primary ${isSelected ? '' : 'border-opacity-10'}`
+        }
+      >
         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
           <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
@@ -45,8 +55,8 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
           <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
           <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
         </svg>
         </svg>
       </a>
       </a>
-      <span className="theme-option-name mt-2"><b>{ name }</b></span>
-      { !isPresetTheme && <span className="theme-option-badge badge bg-primary mt-1">Plugin</span> }
+      <span className={`mt-2 ${isSelected ? '' : 'opacity-25'}`}><b>{ name }</b></span>
+      { !isPresetTheme && <span className={`badge bg-primary mt-1 ${isSelected ? '' : 'opacity-25'}`}>Plugin</span> }
     </div>
     </div>
   );
   );
 
 

+ 0 - 51
apps/app/src/components/Layout/Admin.module.scss

@@ -226,49 +226,6 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
   }
   }
 
 
-  #layoutOptions {
-    .customize-layout-card {
-      border: 4px solid $border-color;
-    }
-  }
-
-  // theme selector
-  #themeOptions {
-    // layout
-    .theme-option-container {
-      min-width: 100px;
-      a {
-        padding: 3px;
-        margin-right: 10px;
-        margin-bottom: 10px;
-
-        svg {
-          display: block;
-        }
-      }
-    }
-
-    &.disabled {
-      cursor: not-allowed;
-      opacity: 0.5;
-    }
-
-    // style
-    .theme-option-container a {
-      background-color: $gray-100;
-      border: 1px solid $border-color;
-    }
-    .theme-option-name, .theme-option-badge {
-      opacity: 0.3;
-    }
-    // style (active)
-    .theme-option-container.active {
-      .theme-option-name, .theme-option-badge {
-        opacity: 1;
-      }
-    }
-  }
-
   .settings-table {
   .settings-table {
     table-layout: fixed;
     table-layout: fixed;
 
 
@@ -285,14 +242,6 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
   }
   }
 
 
-  .admin-navigation {
-    & > a + a {
-      margin-top: 2px;
-    }
-    &.sticky-top {
-      top: 30px;
-    }
-  }
 }
 }
 
 
 
 

+ 1 - 0
apps/app/src/interfaces/sidebar-config.ts

@@ -1,4 +1,5 @@
 
 
 export interface ISidebarConfig {
 export interface ISidebarConfig {
   isSidebarCollapsedMode: boolean,
   isSidebarCollapsedMode: boolean,
+  isSidebarClosedAtDockMode?: boolean,
 }
 }

+ 8 - 1
apps/app/src/pages/[[...path]].page.tsx

@@ -26,6 +26,7 @@ import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { SupportedAction, type SupportedActionType } from '~/interfaces/activity';
 import { SupportedAction, type SupportedActionType } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import {
 import {
@@ -47,7 +48,6 @@ import {
 } from '~/stores/page';
 } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
-import { useSelectedGrant } from '~/stores/ui';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -149,6 +149,8 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   isEnabledMarp: boolean,
   isEnabledMarp: boolean,
 
 
+  sidebarConfig: ISidebarConfig,
+
   isSlackConfigured: boolean,
   isSlackConfigured: boolean,
   // isMailerSetup: boolean,
   // isMailerSetup: boolean,
   isAclEnabled: boolean,
   isAclEnabled: boolean,
@@ -530,6 +532,11 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.isEnabledAttachTitleHeader = configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader');
   props.isEnabledAttachTitleHeader = configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader');
 
 
+  props.sidebarConfig = {
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+  };
+
   props.rendererConfig = {
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),

+ 3 - 0
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -10,6 +10,7 @@ import Head from 'next/head';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
@@ -34,6 +35,7 @@ type Props = CommonProps & {
   // Render config
   // Render config
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 
 
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
@@ -97,6 +99,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 5 - 1
apps/app/src/pages/_search.page.tsx

@@ -11,6 +11,7 @@ import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
 import {
   useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL, useGrowiCloudUri, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL, useGrowiCloudUri, useCurrentPathname,
@@ -42,6 +43,7 @@ type Props = CommonProps & {
 
 
   isContainerFluid: boolean,
   isContainerFluid: boolean,
 
 
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
@@ -88,7 +90,8 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 };
 };
 
 
 type LayoutProps = Props & {
 type LayoutProps = Props & {
-  children?: ReactNode
+  sidebarConfig: ISidebarConfig,
+  children?: ReactNode,
 }
 }
 
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
@@ -123,6 +126,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 4 - 0
apps/app/src/pages/me/[[...path]].page.tsx

@@ -13,6 +13,7 @@ import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
 import {
   useCurrentUser, useIsSearchPage, useGrowiCloudUri,
   useCurrentUser, useIsSearchPage, useGrowiCloudUri,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
@@ -41,6 +42,8 @@ type Props = CommonProps & {
 
 
   // config
   // config
   registrationWhitelist: string[],
   registrationWhitelist: string[],
+
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
@@ -178,6 +181,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 4 - 0
apps/app/src/pages/tags.page.tsx

@@ -12,6 +12,7 @@ import Head from 'next/head';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IDataTagCount } from '~/interfaces/tag';
 import type { IDataTagCount } from '~/interfaces/tag';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxTagsList } from '~/stores/tag';
 import { useSWRxTagsList } from '~/stores/tag';
@@ -38,6 +39,8 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
+
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const TagList = dynamic(() => import('~/components/TagList'), { ssr: false });
 const TagList = dynamic(() => import('~/components/TagList'), { ssr: false });
@@ -153,6 +156,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
 }
 }

+ 4 - 0
apps/app/src/pages/trash.page.tsx

@@ -11,6 +11,7 @@ import { PagePathNavSticky } from '~/components/Common/PagePathNav';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
@@ -38,6 +39,8 @@ type Props = CommonProps & {
   showPageLimitationXL: number,
   showPageLimitationXL: number,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
+
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
@@ -119,6 +122,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
 }
 }

+ 0 - 4
apps/app/src/pages/utils/commons.ts

@@ -36,7 +36,6 @@ export type CommonProps = {
   isAccessDeniedForNonAdminUser?: boolean,
   isAccessDeniedForNonAdminUser?: boolean,
   currentUser?: IUserHasId,
   currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
   forcedColorScheme?: ColorScheme,
-  sidebarConfig: ISidebarConfig,
   userUISettings?: IUserUISettings
   userUISettings?: IUserUISettings
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
@@ -101,9 +100,6 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     isDefaultLogo,
     isDefaultLogo,
     forcedColorScheme,
     forcedColorScheme,
     growiCloudUri: configManager.getConfig('crowi', 'app:growiCloudUri'),
     growiCloudUri: configManager.getConfig('crowi', 'app:growiCloudUri'),
-    sidebarConfig: {
-      isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    },
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
   };
   };
 
 

+ 3 - 1
apps/app/src/server/models/config.ts

@@ -1,5 +1,6 @@
 import { PresetThemes } from '@growi/preset-themes';
 import { PresetThemes } from '@growi/preset-themes';
-import { Types, Schema } from 'mongoose';
+import type { Types } from 'mongoose';
+import { Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 import uniqueValidator from 'mongoose-unique-validator';
 
 
 import { RehypeSanitizeOption } from '../../interfaces/rehype';
 import { RehypeSanitizeOption } from '../../interfaces/rehype';
@@ -136,6 +137,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isEnabledMarp': false,
   'customize:isEnabledMarp': false,
   'customize:isSidebarCollapsedMode': false,
   'customize:isSidebarCollapsedMode': false,
+  'customize:isSidebarClosedAtDockMode': false,
 
 
   'notification:owner-page:isEnabled': false,
   'notification:owner-page:isEnabled': false,
   'notification:group-page:isEnabled': false,
   'notification:group-page:isEnabled': false,

+ 5 - 1
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -113,6 +113,7 @@ module.exports = (crowi) => {
     ],
     ],
     sidebar: [
     sidebar: [
       body('isSidebarCollapsedMode').isBoolean(),
       body('isSidebarCollapsedMode').isBoolean(),
+      body('isSidebarClosedAtDockMode').optional().isBoolean(),
     ],
     ],
     function: [
     function: [
       body('isEnabledTimeline').isBoolean(),
       body('isEnabledTimeline').isBoolean(),
@@ -342,7 +343,8 @@ module.exports = (crowi) => {
 
 
     try {
     try {
       const isSidebarCollapsedMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode');
       const isSidebarCollapsedMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode');
-      return res.apiv3({ isSidebarCollapsedMode });
+      const isSidebarClosedAtDockMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode');
+      return res.apiv3({ isSidebarCollapsedMode, isSidebarClosedAtDockMode });
     }
     }
     catch (err) {
     catch (err) {
       const msg = 'Error occurred in getting sidebar';
       const msg = 'Error occurred in getting sidebar';
@@ -354,12 +356,14 @@ module.exports = (crowi) => {
   router.put('/sidebar', loginRequiredStrictly, adminRequired, validator.sidebar, apiV3FormValidator, addActivity, async(req, res) => {
   router.put('/sidebar', loginRequiredStrictly, adminRequired, validator.sidebar, apiV3FormValidator, addActivity, async(req, res) => {
     const requestParams = {
     const requestParams = {
       'customize:isSidebarCollapsedMode': req.body.isSidebarCollapsedMode,
       'customize:isSidebarCollapsedMode': req.body.isSidebarCollapsedMode,
+      'customize:isSidebarClosedAtDockMode': req.body.isSidebarClosedAtDockMode,
     };
     };
 
 
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
       const customizedParams = {
         isSidebarCollapsedMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
         isSidebarCollapsedMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+        isSidebarClosedAtDockMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
       };
       };
 
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SIDEBAR_UPDATE });
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SIDEBAR_UPDATE });

+ 32 - 25
apps/app/src/stores/admin/sidebar-config.tsx

@@ -1,3 +1,5 @@
+import { useCallback } from 'react';
+
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -6,47 +8,52 @@ import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 
 
 type SidebarConfigOption = {
 type SidebarConfigOption = {
   update: () => Promise<void>,
   update: () => Promise<void>,
-  isSidebarCollapsedMode: boolean|undefined,
+
   setIsSidebarCollapsedMode: (isSidebarCollapsedMode: boolean) => void,
   setIsSidebarCollapsedMode: (isSidebarCollapsedMode: boolean) => void,
+  setIsSidebarClosedAtDockMode: (isSidebarClosedAtDockMode: boolean | undefined) => void,
 }
 }
 
 
 export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & SidebarConfigOption => {
 export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & SidebarConfigOption => {
   const swrResponse = useSWRImmutable<ISidebarConfig>(
   const swrResponse = useSWRImmutable<ISidebarConfig>(
     '/customize-setting/sidebar',
     '/customize-setting/sidebar',
     endpoint => apiv3Get<ISidebarConfig>(endpoint).then(result => result.data),
     endpoint => apiv3Get<ISidebarConfig>(endpoint).then(result => result.data),
+    {
+      keepPreviousData: true,
+    },
   );
   );
+
+  const { data, mutate } = swrResponse;
+
   return {
   return {
     ...swrResponse,
     ...swrResponse,
-    update: async() => {
-      const { data } = swrResponse;
-
+    update: useCallback(async() => {
       if (data == null) {
       if (data == null) {
         return;
         return;
       }
       }
 
 
-      const { isSidebarCollapsedMode } = data;
-
-      const updateData = {
-        isSidebarCollapsedMode,
-      };
-
       // invoke API
       // invoke API
-      await apiv3Put('/customize-setting/sidebar', updateData);
-    },
-    isSidebarCollapsedMode: swrResponse.data?.isSidebarCollapsedMode,
-    setIsSidebarCollapsedMode: (isSidebarCollapsedMode) => {
-      const { data, mutate } = swrResponse;
-
-      if (data == null) {
-        return;
-      }
-
-      const updateData = {
-        isSidebarCollapsedMode,
-      };
+      await apiv3Put<ISidebarConfig>('/customize-setting/sidebar', data);
+    }, [data]),
 
 
+    setIsSidebarCollapsedMode: useCallback((isSidebarCollapsedMode) => {
       // update isSidebarCollapsedMode in cache, not revalidate
       // update isSidebarCollapsedMode in cache, not revalidate
-      mutate({ ...data, ...updateData }, false);
-    },
+      mutate((prevData) => {
+        if (prevData == null) {
+          return;
+        }
+
+        return { ...prevData, isSidebarCollapsedMode };
+      }, false);
+    }, [mutate]),
+
+    setIsSidebarClosedAtDockMode: useCallback((isSidebarClosedAtDockMode) => {
+      // update isSidebarClosedAtDockMode in cache, not revalidate
+      mutate((prevData) => {
+        if (prevData == null) {
+          return;
+        }
+        return { ...prevData, isSidebarClosedAtDockMode };
+      }, false);
+    }, [mutate]),
   };
   };
 };
 };