Browse Source

Merge branch 'master' into feat/141574-143040-apply-locale-to-defualt-page-title

Yuki Takei 1 year ago
parent
commit
fb8ee8cbb1
100 changed files with 474 additions and 357 deletions
  1. 1 1
      apps/app/public/static/locales/zh_CN/translation.json
  2. 2 2
      apps/app/src/client/services/layout.ts
  3. 4 3
      apps/app/src/components/Admin/App/ConfirmModal.tsx
  4. 17 0
      apps/app/src/components/Admin/Common/AdminNavigation.module.scss
  5. 8 3
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  6. 17 8
      apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  7. 21 12
      apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  8. 7 4
      apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx
  9. 6 0
      apps/app/src/components/Admin/Customize/ThemeColorBox.module.scss
  10. 15 5
      apps/app/src/components/Admin/Customize/ThemeColorBox.tsx
  11. 1 1
      apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  12. 1 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  13. 1 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  14. 1 1
      apps/app/src/components/Admin/Notification/NotificationDeleteModal.jsx
  15. 1 1
      apps/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx
  16. 1 1
      apps/app/src/components/Admin/Security/LdapAuthTestModal.jsx
  17. 1 1
      apps/app/src/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx
  18. 1 1
      apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx
  19. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  20. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  21. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  22. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx
  23. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx
  24. 1 1
      apps/app/src/components/Admin/Users/PasswordResetModal.jsx
  25. 1 1
      apps/app/src/components/Admin/Users/UserInviteModal.jsx
  26. 17 6
      apps/app/src/components/AuthorInfo/AuthorInfo.tsx
  27. 3 4
      apps/app/src/components/Common/ImageCropModal.tsx
  28. 1 1
      apps/app/src/components/CompleteUserRegistrationForm.tsx
  29. 1 1
      apps/app/src/components/ContentLinkButtons.tsx
  30. 1 1
      apps/app/src/components/CreateTemplateModal.tsx
  31. 1 1
      apps/app/src/components/DeleteBookmarkFolderModal.tsx
  32. 3 2
      apps/app/src/components/EmptyTrashModal.tsx
  33. 0 51
      apps/app/src/components/Layout/Admin.module.scss
  34. 1 1
      apps/app/src/components/Me/AssociateModal.tsx
  35. 1 1
      apps/app/src/components/Me/DisassociateModal.tsx
  36. 1 1
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  37. 2 2
      apps/app/src/components/PageAlert/FixPageGrantAlert.tsx
  38. 2 2
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  39. 2 2
      apps/app/src/components/PageAuthorInfo/PageAuthorInfo.tsx
  40. 1 1
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  41. 5 7
      apps/app/src/components/PageContentFooter.tsx
  42. 6 5
      apps/app/src/components/PageControls/PageControls.tsx
  43. 1 1
      apps/app/src/components/PageCreateModal.tsx
  44. 1 1
      apps/app/src/components/PageDuplicateModal.tsx
  45. 2 2
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx
  46. 5 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss
  47. 4 3
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  48. 12 0
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  49. 1 1
      apps/app/src/components/PageEditor/GridEditModal.jsx
  50. 1 1
      apps/app/src/components/PageEditor/LinkEditModal.tsx
  51. 1 0
      apps/app/src/components/PageEditor/PageEditor.tsx
  52. 31 4
      apps/app/src/components/PageHeader/PageHeader.tsx
  53. 5 1
      apps/app/src/components/PageHeader/PagePathHeader.module.scss
  54. 27 18
      apps/app/src/components/PageHeader/PagePathHeader.tsx
  55. 5 1
      apps/app/src/components/PageHeader/PageTitleHeader.module.scss
  56. 24 19
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  57. 1 1
      apps/app/src/components/PageRenameModal.tsx
  58. 13 10
      apps/app/src/components/PageSelectModal/PageSelectModal.tsx
  59. 1 1
      apps/app/src/components/PageTags/TagsInput.tsx
  60. 1 1
      apps/app/src/components/PrivateLegacyPages.tsx
  61. 1 1
      apps/app/src/components/PrivateLegacyPagesMigrationModal.tsx
  62. 1 1
      apps/app/src/components/PutbackPageModal.jsx
  63. 1 1
      apps/app/src/components/SearchPage/SearchOptionModal.tsx
  64. 1 1
      apps/app/src/components/ShortcutsModal.tsx
  65. 1 1
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  66. 5 4
      apps/app/src/components/User/Username.tsx
  67. 1 1
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/SyncExecution.tsx
  68. 1 1
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginDeleteModal.tsx
  69. 1 0
      apps/app/src/interfaces/sidebar-config.ts
  70. 0 1
      apps/app/src/interfaces/user-ui-settings.ts
  71. 8 1
      apps/app/src/pages/[[...path]].page.tsx
  72. 3 0
      apps/app/src/pages/_private-legacy-pages.page.tsx
  73. 5 1
      apps/app/src/pages/_search.page.tsx
  74. 4 0
      apps/app/src/pages/me/[[...path]].page.tsx
  75. 4 0
      apps/app/src/pages/tags.page.tsx
  76. 4 0
      apps/app/src/pages/trash.page.tsx
  77. 7 8
      apps/app/src/pages/user-activation.page.tsx
  78. 0 4
      apps/app/src/pages/utils/commons.ts
  79. 3 1
      apps/app/src/server/models/config.ts
  80. 0 3
      apps/app/src/server/models/user-ui-settings.ts
  81. 5 1
      apps/app/src/server/routes/apiv3/customize-setting.js
  82. 9 3
      apps/app/src/server/routes/apiv3/page-listing.ts
  83. 4 3
      apps/app/src/server/routes/apiv3/page/update-page.ts
  84. 2 2
      apps/app/src/server/routes/user-activation.ts
  85. 16 37
      apps/app/src/server/service/file-uploader/aws.ts
  86. 2 4
      apps/app/src/server/service/mail.ts
  87. 14 4
      apps/app/src/server/service/page/index.ts
  88. 32 25
      apps/app/src/stores/admin/sidebar-config.tsx
  89. 4 4
      apps/app/src/stores/remote-latest-page.ts
  90. 2 13
      apps/app/src/stores/ui.tsx
  91. 4 0
      apps/app/src/styles/_layout.scss
  92. 2 2
      packages/core/src/interfaces/common.ts
  93. 4 8
      packages/core/src/interfaces/page.ts
  94. 2 1
      packages/core/src/interfaces/revision.ts
  95. 3 2
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  96. 1 0
      packages/editor/src/components/playground/Playground.tsx
  97. 6 2
      packages/editor/src/services/editor-theme/material.ts
  98. 9 5
      packages/editor/src/services/editor-theme/nord.ts
  99. 3 2
      packages/editor/src/services/list-util/insert-newline-continue-markup.ts
  100. 2 2
      packages/editor/src/services/table-util/insert-new-row-to-table-markdown.ts

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

@@ -27,7 +27,7 @@
   "Description": "描述",
   "Admin": "管理",
   "administrator": "管理员",
-  "Tags": "Tags",
+  "Tags": "标签",
   "Close": "Close",
   "Shortcuts": "快捷方式",
   "CustomSidebar": "Custom Sidebar",

+ 2 - 2
apps/app/src/client/services/layout.ts

@@ -1,4 +1,4 @@
-import type { IPage } from '@growi/core';
+import type { IPage, IPagePopulatedToShowRevision } from '@growi/core';
 
 import { useIsContainerFluid } from '~/stores/context';
 import { useEditorMode } from '~/stores/ui';
@@ -16,7 +16,7 @@ const useDetermineExpandContent = (expandContentWidth?: boolean | null): boolean
   return expandContentWidth ?? isContainerFluidDefault ?? false;
 };
 
-export const useShouldExpandContent = (data?: IPage | boolean | null): boolean => {
+export const useShouldExpandContent = (data?: IPage | IPagePopulatedToShowRevision | boolean | null): boolean => {
   const expandContentWidth = (() => {
     // when data is null
     if (data == null) {

+ 4 - 3
apps/app/src/components/Admin/App/ConfirmModal.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 import {
@@ -31,8 +32,8 @@ export const ConfirmModal: FC<ConfirmModalProps> = (props: ConfirmModalProps) =>
 
   return (
     <Modal isOpen={props.isModalOpen} toggle={onCancel}>
-      <ModalHeader tag="h4" toggle={onCancel} className="bg-danger">
-        <span className="material-symbols-outlined">help</span>
+      <ModalHeader tag="h4" toggle={onCancel} className="text-danger">
+        <span className="material-symbols-outlined me-1">warning</span>
         {t('Warning')}
       </ModalHeader>
       <ModalBody>

+ 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 styles from './AdminNavigation.module.scss';
+
+const moduleClass = styles['admin-navigation'];
+
+
 // eslint-disable-next-line react/prop-types
 const MenuLabel = ({ menu }: { menu: string }) => {
   const { t } = useTranslation(['admin', 'commons']);
@@ -82,7 +87,7 @@ export const AdminNavigation = (): JSX.Element => {
 
   }, [pathname]);
 
-  const getListGroupItemOrDropdownItemList = (isListGroupItems: boolean) => {
+  const getListGroupItemOrDropdownItemList = useCallback((isListGroupItems: boolean) => {
     return (
       <>
         {/* eslint-disable no-multi-spaces */}
@@ -115,12 +120,12 @@ export const AdminNavigation = (): JSX.Element => {
         {/* eslint-enable no-multi-spaces */}
       </>
     );
-  };
+  }, [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname]);
 
   return (
     <React.Fragment>
       {/* 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)}
       </div>
 

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

@@ -30,7 +30,6 @@ const CustomizeLayoutSetting = (): JSX.Element => {
   const { resolvedTheme } = useNextThemes();
 
   const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
-  const [retrieveError, setRetrieveError] = useState<any>();
 
   const onClickSubmit = useCallback(async() => {
     if (isContainerFluid == null) { return }
@@ -58,14 +57,19 @@ const CustomizeLayoutSetting = (): JSX.Element => {
           <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
 
           <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={`card customize-layout-card ${!isContainerFluid ? 'border-active' : ''}`}
+                  className={`card border border-4 ${!isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(false)}
                   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">
                     {t('customize_settings.layout_options.default')}
                   </div>
@@ -73,12 +77,17 @@ const CustomizeLayoutSetting = (): JSX.Element => {
               </div>
               <div className="col">
                 <div
-                  className={`card customize-layout-card ${isContainerFluid ? 'border-active' : ''}`}
+                  className={`card border border-4 ${isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(true)}
                   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')}
                   </div>
                 </div>
@@ -88,7 +97,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
 
           <div className="row my-3">
             <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>

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

@@ -1,5 +1,6 @@
 import React, { useCallback } from 'react';
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
@@ -11,7 +12,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
 
   const {
-    update, isSidebarCollapsedMode, setIsSidebarCollapsedMode,
+    data, update, setIsSidebarCollapsedMode, setIsSidebarClosedAtDockMode,
   } = useSWRxSidebarConfig();
 
   const { resolvedTheme } = useNextThemes();
@@ -28,6 +29,12 @@ const CustomizeSidebarsetting = (): JSX.Element => {
     }
   }, [t, update]);
 
+  if (data == null) {
+    return <LoadingSpinner />;
+  }
+
+  const { isSidebarCollapsedMode, isSidebarClosedAtDockMode } = data;
+
   return (
     <React.Fragment>
       <div className="row">
@@ -42,14 +49,15 @@ const CustomizeSidebarsetting = (): JSX.Element => {
           </Card>
 
           <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={`card customize-layout-card ${isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  className={`card border border-4 ${isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(true)}
                   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">
                     Drawer Mode
                   </div>
@@ -57,11 +65,12 @@ const CustomizeSidebarsetting = (): JSX.Element => {
               </div>
               <div className="col">
                 <div
-                  className={`card customize-layout-card ${!isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  className={`card border border-4 ${!isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(false)}
                   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">
                     Dock Mode
                   </div>
@@ -82,9 +91,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 type="radio"
                 id="is-open"
                 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">
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
@@ -95,9 +104,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 type="radio"
                 id="is-closed"
                 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">
                 {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]);
 
   return (
-    <div id="themeOptions">
+    <>
+
       {/* Light and Dark Themes */}
       <div>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
-        <div className="d-flex flex-wrap">
+        <div className="hstack gap-3">
           {lightNDarkThemes.map((theme) => {
             return (
               <ThemeColorBox
@@ -42,10 +43,11 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
         </div>
       </div>
+
       {/* Only one mode Theme */}
       <div className="mt-3">
         <h3>{t('customize_settings.theme_desc.unique')}</h3>
-        <div className="d-flex flex-wrap">
+        <div className="hstack gap-3">
           {oneModeThemes.map((theme) => {
             return (
               <ThemeColorBox
@@ -58,7 +60,8 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
         </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 styles from './ThemeColorBox.module.scss';
+
+const themeOptionClass = styles['theme-option-container'];
+
 
 type Props = {
   isSelected: boolean,
@@ -19,13 +23,19 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
   } = metadata;
 
   return (
-    // TODO: Display a primary color border when icon is selected
     <div
       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}
     >
-      <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">
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <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} />
         </svg>
       </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>
   );
 

+ 1 - 1
apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -157,7 +157,7 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
 
   return (
     <Modal isOpen={isOpen} toggle={onClose}>
-      <ModalHeader tag="h4" toggle={onClose} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={onClose} className="text-info">
         {t('admin:export_management.export_collections')}
       </ModalHeader>
 

+ 1 - 1
apps/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx

@@ -21,7 +21,7 @@ const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
 
   return (
     <Modal isOpen={props.isOpen} toggle={props.onClose} size="lg">
-      <ModalHeader tag="h4" toggle={props.onClose} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={props.onClose} className="text-danger">
         Errors
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -190,7 +190,7 @@ class ImportCollectionConfigurationModal extends React.Component {
 
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} onEnter={this.initialize}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-info text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
           {`'${collectionName}'`} Configuration
         </ModalHeader>
 

+ 1 - 1
apps/app/src/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -12,7 +12,7 @@ class NotificationDeleteModal extends React.PureComponent {
     const { t, notificationForConfiguration } = this.props;
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-danger">
           <span className="material-symbols-outlined">delete_forever</span>Delete Global Notification Setting
         </ModalHeader>
         <ModalBody>

+ 1 - 1
apps/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx

@@ -20,7 +20,7 @@ const DeleteAllShareLinksModal = React.memo((props) => {
 
   return (
     <Modal isOpen={props.isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
-      <ModalHeader tag="h4" toggle={closeButtonHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={closeButtonHandler} className="text-danger">
         <span>
           <span className="material-symbols-outlined">delete_forever</span>
           {t('security_settings.delete_all_share_links')}

+ 1 - 1
apps/app/src/components/Admin/Security/LdapAuthTestModal.jsx

@@ -44,7 +44,7 @@ class LdapAuthTestModal extends React.Component {
 
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-info text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
           Test LDAP Account
         </ModalHeader>
         <ModalBody>

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx

@@ -25,7 +25,7 @@ const ConfirmBotChangeModal = (props) => {
     <Modal isOpen={props.isOpen} centered>
       <ModalHeader
         toggle={handleCancelButton}
-        className="bg-danger"
+        className="text-danger"
       >
         {t('slack_integration.modal.warning')}
       </ModalHeader>

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx

@@ -31,7 +31,7 @@ export const DeleteSlackBotSettingsModal = React.memo((props: DeleteSlackBotSett
 
   return (
     <Modal isOpen={isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
-      <ModalHeader tag="h4" toggle={closeButtonHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={closeButtonHandler} className="text-danger">
         <span>
           {isResetAll && (
             <>

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -177,7 +177,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 
   return (
     <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>
-      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={toggleHandler}>
         <span className="material-symbols-outlined">delete_forever</span> {t('admin:user_group_management.delete_modal.header')}
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx

@@ -116,7 +116,7 @@ export const UserGroupForm: FC<Props> = (props: Props) => {
             <button
               type="button"
               id="dropdownMenuButton"
-              data-toggle="dropdown"
+              data-bs-toggle="dropdown"
               className="btn btn-outline-secondary dropdown-toggle mb-3"
               disabled={isExternalGroup || !isSelectableParentUserGroups}
             >

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -69,7 +69,7 @@ export const UserGroupModal: FC<Props> = (props: Props) => {
   return (
     <Modal className="modal-md" isOpen={isShow} toggle={onHide}>
       <form onSubmit={onSubmitHandler}>
-        <ModalHeader tag="h4" toggle={onHide} className="bg-primary text-light">
+        <ModalHeader tag="h4" toggle={onHide}>
           {t('user_group_management.basic_info')}
         </ModalHeader>
 

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx

@@ -27,7 +27,7 @@ export const UpdateParentConfirmModal: FC = () => {
 
   return (
     <Modal className="modal-md" isOpen={isOpened} toggle={closeModal}>
-      <ModalHeader tag="h4" toggle={closeModal} className="bg-warning text-light">
+      <ModalHeader tag="h4" toggle={closeModal} className="text-warning">
         <span className="material-symbols-outlined">warning</span> {t('admin:user_group_management.update_parent_confirm_modal.header')}
       </ModalHeader>
       {

+ 1 - 1
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx

@@ -45,7 +45,7 @@ const UserGroupUserModal = (props: Props): JSX.Element => {
 
   return (
     <Modal isOpen={isOpen} toggle={onClose}>
-      <ModalHeader tag="h4" toggle={onClose} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={onClose} className="text-info">
         {t('admin:user_group_management.add_modal.add_user') }
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -187,7 +187,7 @@ class PasswordResetModal extends React.Component {
 
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-warning text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-warning">
           {t('user_management.reset_password') }
         </ModalHeader>
         <ModalBody>

+ 1 - 1
apps/app/src/components/Admin/Users/UserInviteModal.jsx

@@ -261,7 +261,7 @@ class UserInviteModal extends React.Component {
 
     return (
       <Modal isOpen={adminUsersContainer.state.isUserInviteModalShown}>
-        <ModalHeader tag="h4" toggle={this.onToggleModal} className="bg-info text-light">
+        <ModalHeader tag="h4" toggle={this.onToggleModal} className="text-info">
           {t('admin:user_management.invite_users') }
         </ModalHeader>
         <ModalBody>

+ 17 - 6
apps/app/src/components/AuthorInfo/AuthorInfo.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 
-import type { IUser } from '@growi/core';
+import type { IUserHasId } from '@growi/core';
+import { isPopulated, type IUser, type Ref } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { format } from 'date-fns/format';
@@ -10,10 +11,22 @@ import Link from 'next/link';
 
 import styles from './AuthorInfo.module.scss';
 
+const UserLabel = ({ user }: { user: IUserHasId | Ref<IUser> }): JSX.Element => {
+  if (isPopulated(user)) {
+    return (
+      <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>
+        {user.name}
+      </Link>
+    );
+  }
+
+  return <i>(anyone)</i>;
+};
+
 
-export type AuthorInfoProps = {
+type AuthorInfoProps = {
   date: Date,
-  user: IUser,
+  user?: IUserHasId | Ref<IUser>,
   mode: 'create' | 'update',
   locate: 'subnav' | 'footer',
 }
@@ -37,9 +50,7 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
     : t('author_info.last_revision_posted_at');
   const userLabel = user != null
     ? (
-      <Link href={pagePathUtils.userHomepagePath(user)} prefetch={false}>
-        {user.name}
-      </Link>
+      <UserLabel user={user} />
     )
     : <i>Unknown</i>;
 

+ 3 - 4
apps/app/src/components/Common/ImageCropModal.tsx

@@ -1,6 +1,5 @@
-import React, {
-  FC, useCallback, useEffect, useState,
-} from 'react';
+import type { FC } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 import canvasToBlob from 'async-canvas-to-blob';
 import { useTranslation } from 'react-i18next';
@@ -137,7 +136,7 @@ const ImageCropModal: FC<Props> = (props: Props) => {
 
   return (
     <Modal isOpen={isShow} toggle={onModalCloseHandler}>
-      <ModalHeader tag="h4" toggle={onModalCloseHandler} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={onModalCloseHandler} className="text-info">
         {t('crop_image_modal.image_crop')}
       </ModalHeader>
       <ModalBody className="my-4">

+ 1 - 1
apps/app/src/components/CompleteUserRegistrationForm.tsx

@@ -182,7 +182,7 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
 
               <div className="input-group justify-content-center mt-4">
                 <button
-                  type="button"
+                  type="submit"
                   disabled={forceDisableForm || disableForm}
                   className="btn btn-secondary btn-register col-6 mx-auto d-flex"
                 >

+ 1 - 1
apps/app/src/components/ContentLinkButtons.tsx

@@ -40,7 +40,7 @@ RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
 
 
 export type ContentLinkButtonsProps = {
-  author: IUserHasId | null,
+  author?: IUserHasId,
 }
 
 export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element => {

+ 1 - 1
apps/app/src/components/CreateTemplateModal.tsx

@@ -86,7 +86,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
 
   return (
     <Modal isOpen={isOpen} toggle={onClose} data-testid="page-template-modal">
-      <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={onClose}>
         {t('template.modal_label.Create/Edit Template Page')}
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/DeleteBookmarkFolderModal.tsx

@@ -42,7 +42,7 @@ const DeleteBookmarkFolderModal: FC = () => {
 
   return (
     <Modal size="md" isOpen={isOpened} toggle={closeBookmarkFolderDeleteModal} data-testid="page-delete-modal" className="grw-create-page">
-      <ModalHeader tag="h4" toggle={closeBookmarkFolderDeleteModal} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={closeBookmarkFolderDeleteModal} className="text-danger">
         <span className="material-symbols-outlined">delete</span>
         {t('bookmark_folder.delete_modal.modal_header_label')}
       </ModalHeader>

+ 3 - 2
apps/app/src/components/EmptyTrashModal.tsx

@@ -1,5 +1,6 @@
+import type { FC } from 'react';
 import React, {
-  useState, FC,
+  useState,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -60,7 +61,7 @@ const EmptyTrashModal: FC = () => {
 
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal">
-      <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="text-danger">
         <span className="material-symbols-outlined">delete_forever</span>
         {t('modal_empty.empty_the_trash')}
       </ModalHeader>

+ 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 {
     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 - 1
apps/app/src/components/Me/AssociateModal.tsx

@@ -56,7 +56,7 @@ const AssociateModal = (props: Props): JSX.Element => {
 
   return (
     <Modal isOpen={isOpen} toggle={closeModalHandler} size="lg" data-testid="grw-associate-modal">
-      <ModalHeader className="bg-primary text-light" toggle={onClose}>
+      <ModalHeader toggle={onClose}>
         { t('admin:user_management.create_external_account') }
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/Me/DisassociateModal.tsx

@@ -45,7 +45,7 @@ const DisassociateModal = (props: Props): JSX.Element => {
 
   return (
     <Modal isOpen={props.isOpen} toggle={props.onClose}>
-      <ModalHeader className="bg-info text-light" toggle={props.onClose}>
+      <ModalHeader className="text-info" toggle={props.onClose}>
         {t('personal_settings.disassociate_external_account')}
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -309,7 +309,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
           <nav
             className={`${styles['grw-contextual-sub-navigation']}
-              d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
+              d-flex align-items-center justify-content-end pe-2 pe-sm-3 pe-md-4 py-1 gap-2 gap-md-4 d-print-none
             `}
             data-testid="grw-contextual-sub-nav"
             id="grw-contextual-sub-nav"

+ 2 - 2
apps/app/src/components/PageAlert/FixPageGrantAlert.tsx

@@ -235,7 +235,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
   return (
     <>
       <Modal size="lg" isOpen={isOpen} toggle={close}>
-        <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+        <ModalHeader tag="h4" toggle={close}>
           { t('fix_page_grant.modal.title') }
         </ModalHeader>
         {renderModalBodyAndFooter()}
@@ -245,7 +245,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
           isOpen={isGroupSelectModalShown}
           toggle={() => setIsGroupSelectModalShown(false)}
         >
-          <ModalHeader tag="h4" toggle={() => setIsGroupSelectModalShown(false)} className="bg-purple text-light">
+          <ModalHeader tag="h4" toggle={() => setIsGroupSelectModalShown(false)}>
             {t('user_group.select_group')}
           </ModalHeader>
           <ModalBody>

+ 2 - 2
apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -76,7 +76,7 @@ export const DeleteAttachmentModal: React.FC = () => {
           <span className="material-symbols-outlined">{iconByFormat(attachment.fileFormat)}</span> {attachment.originalName}
         </p>
         <p>
-          uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator as IUser}></Username>
+          uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator}></Username>
         </p>
         {content}
       </div>
@@ -101,7 +101,7 @@ export const DeleteAttachmentModal: React.FC = () => {
       aria-labelledby="contained-modal-title-lg"
       fade={false}
     >
-      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={toggleHandler} className="text-danger">
         <span id="contained-modal-title-lg">{t('delete_attachment_modal.confirm_delete_attachment')}</span>
       </ModalHeader>
       <ModalBody>

+ 2 - 2
apps/app/src/components/PageAuthorInfo/PageAuthorInfo.tsx

@@ -33,12 +33,12 @@ export const PageAuthorInfo = memo((): JSX.Element => {
     <ul className={`grw-page-author-info ${styles['grw-page-author-info']} text-nowrap border-start d-none d-lg-block d-edit-none py-2 ps-4 mb-0 ms-3`}>
       <li className="pb-1">
         {currentPage != null && (
-          <AuthorInfo user={currentPage.creator as IUser} date={currentPage.createdAt} mode="create" locate="subnav" />
+          <AuthorInfo user={currentPage.creator} date={currentPage.createdAt} mode="create" locate="subnav" />
         )}
       </li>
       <li className="mt-1 pt-1 border-top">
         {currentPage != null && (
-          <AuthorInfo user={currentPage.lastUpdateUser as IUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
+          <AuthorInfo user={currentPage.lastUpdateUser} date={currentPage.updatedAt} mode="update" locate="subnav" />
         )}
       </li>
     </ul>

+ 1 - 1
apps/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -85,7 +85,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
 
   return (
     <Modal isOpen={isShown} toggle={cancelToDelete} className={`${styles['page-comment-delete-modal']}`}>
-      <ModalHeader tag="h4" toggle={cancelToDelete} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={cancelToDelete} className="text-danger">
         {headerContent()}
       </ModalHeader>
       <ModalBody>

+ 5 - 7
apps/app/src/components/PageContentFooter.tsx

@@ -1,16 +1,14 @@
 import React from 'react';
 
-import type { IPage, IUser } from '@growi/core';
+import type { IPage, IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import type { AuthorInfoProps } from './AuthorInfo';
-
 import styles from './PageContentFooter.module.scss';
 
-const AuthorInfo = dynamic<AuthorInfoProps>(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
+const AuthorInfo = dynamic(() => import('./AuthorInfo').then(mod => mod.AuthorInfo), { ssr: false });
 
 export type PageContentFooterProps = {
-  page: IPage,
+  page: IPage | IPagePopulatedToShowRevision,
 }
 
 export const PageContentFooter = (props: PageContentFooterProps): JSX.Element => {
@@ -29,8 +27,8 @@ export const PageContentFooter = (props: PageContentFooterProps): JSX.Element =>
     <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
       <div className="container-lg grw-container-convertible">
         <div className="page-meta">
-          <AuthorInfo user={creator as IUser} date={createdAt} mode="create" locate="footer" />
-          <AuthorInfo user={lastUpdateUser as IUser} date={updatedAt} mode="update" locate="footer" />
+          <AuthorInfo user={creator} date={createdAt} mode="create" locate="footer" />
+          <AuthorInfo user={lastUpdateUser} date={updatedAt} mode="update" locate="footer" />
         </div>
       </div>
     </div>

+ 6 - 5
apps/app/src/components/PageControls/PageControls.tsx

@@ -49,6 +49,7 @@ type TagsProps = {
 
 const Tags = (props: TagsProps): JSX.Element => {
   const { onClickEditTagsButton } = props;
+  const { t } = useTranslation();
 
   return (
     <div className="grw-tag-labels-container d-flex align-items-center">
@@ -57,8 +58,8 @@ const Tags = (props: TagsProps): JSX.Element => {
         className="btn btn-sm btn-outline-neutral-secondary"
         onClick={onClickEditTagsButton}
       >
-        <span className="material-symbols-outlined me-1">local_offer</span>
-        Tags
+        <span className="material-symbols-outlined">local_offer</span>
+        <span className="d-none d-sm-inline ms-1">{t('Tags')}</span>
       </button>
     </div>
   );
@@ -138,7 +139,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
 
-  const { mutateAndSave: mutatePageControlsX } = usePageControlsX();
+  const { mutate: mutatePageControlsX } = usePageControlsX();
 
   const pageControlsRef = useRef<HTMLDivElement>(null);
   const [pageControlsRect] = useRect(pageControlsRef);
@@ -273,7 +274,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
 
   return (
     <div className={`${styles['grw-page-controls']} hstack gap-2`} ref={pageControlsRef}>
-      { isDeviceLargerThanMd && (
+      { isViewMode && isDeviceLargerThanMd && (
         <SearchButton />
       )}
 
@@ -284,7 +285,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
       )}
 
       { !hideSubControls && (
-        <div className="hstack gap-1">
+        <div className={`hstack gap-1 ${!isViewMode && 'd-none d-lg-flex'}`}>
           {revisionId != null && _isIPageInfoForOperation && (
             <SubscribeButton
               status={pageInfo.subscriptionStatus}

+ 1 - 1
apps/app/src/components/PageCreateModal.tsx

@@ -292,7 +292,7 @@ const PageCreateModal: React.FC = () => {
       className={`grw-create-page ${styles['grw-create-page']}`}
       autoFocus={false}
     >
-      <ModalHeader tag="h4" toggle={() => closeCreateModal()} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={() => closeCreateModal()}>
         {t('New Page')}
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/PageDuplicateModal.tsx

@@ -279,7 +279,7 @@ const PageDuplicateModal = (): JSX.Element => {
 
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} data-testid="page-duplicate-modal" className="grw-duplicate-page" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeDuplicateModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={closeDuplicateModal}>
         { t('modal_duplicate.label.Duplicate page') }
       </ModalHeader>
       <ModalBody>

+ 2 - 2
apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx

@@ -27,8 +27,8 @@ export const EditingUserList: FC<Props> = ({ userList }) => {
   }
 
   return (
-    <div className="d-flex flex-column justify-content-end">
-      <div className="d-flex justify-content-end">
+    <div className="d-flex flex-column justify-content-start justify-content-sm-end">
+      <div className="d-flex justify-content-start justify-content-sm-end">
         {firstFourUsers.map(user => (
           <div className="ms-1">
             <UserPicture

+ 5 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss

@@ -1,3 +1,8 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
 .editor-navbar :global {
   min-height: 72px;
+  @include bs.media-breakpoint-down(sm) {
+    min-height: 96px;
+  }
 }

+ 4 - 3
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx

@@ -12,11 +12,12 @@ export const EditorNavbar = (): JSX.Element => {
   const { data: editingUsers } = useEditingUsers();
 
   return (
-    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1`}>
-      <PageHeader />
-      <EditingUserList
+    <div className={`${moduleClass} d-flex flex-column flex-sm-row justify-content-between ps-3 ps-md-5 ps-xl-4 pe-4 py-1 align-items-sm-end`}>
+      <div className="order-2 order-sm-1"><PageHeader /></div>
+      <div className="order-1 order-sm-2"><EditingUserList
         userList={editingUsers?.userList ?? []}
       />
+      </div>
     </div>
   );
 };

+ 12 - 0
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -1,5 +1,7 @@
 import dynamic from 'next/dynamic';
 
+import { useDrawerOpened } from '~/stores/ui';
+
 import styles from './EditorNavbarBottom.module.scss';
 
 const moduleClass = styles['grw-editor-navbar-bottom'];
@@ -8,9 +10,19 @@ const SavePageControls = dynamic(() => import('~/components/SavePageControls').t
 const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSelector').then(mod => mod.OptionsSelector), { ssr: false });
 
 const EditorNavbarBottom = (): JSX.Element => {
+
+  const { mutate: mutateDrawerOpened } = useDrawerOpened();
+
   return (
     <div className="border-top" data-testid="grw-editor-navbar-bottom">
       <div className={`flex-expand-horiz align-items-center p-2 ps-md-3 pe-md-4 ${moduleClass}`}>
+        <a
+          role="button"
+          className="nav-link btn-lg p-2 d-md-none me-3 opacity-50"
+          onClick={() => mutateDrawerOpened(true)}
+        >
+          <span className="material-symbols-outlined fs-2">reorder</span>
+        </a>
         <form className="me-auto">
           <OptionsSelector />
         </form>

+ 1 - 1
apps/app/src/components/PageEditor/GridEditModal.jsx

@@ -191,7 +191,7 @@ class GridEditModal extends React.Component {
     const { t } = this.props;
     return (
       <Modal isOpen={this.state.show} toggle={this.cancel} size="xl" className={`grw-grid-edit-modal ${styles['grw-grid-edit-modal']}`}>
-        <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
+        <ModalHeader tag="h4" toggle={this.cancel}>
           {t('grid_edit.create_bootstrap_4_grid')}
         </ModalHeader>
         <ModalBody className="container">

+ 1 - 1
apps/app/src/components/PageEditor/LinkEditModal.tsx

@@ -338,7 +338,7 @@ export const LinkEditModal = (): JSX.Element => {
 
   return (
     <Modal className="link-edit-modal" isOpen={linkEditModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={close}>
         {t('link_edit.edit_link')}
       </ModalHeader>
 

+ 1 - 0
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -353,6 +353,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
         <div className="page-editor-editor-container flex-expand-vert border-end">
           <CodeMirrorEditorMain
+            isEditorMode={editorMode === EditorMode.Editor}
             onChange={markdownChangedHandler}
             onSave={saveWithShortcut}
             onUpload={uploadHandler}

+ 31 - 4
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -1,6 +1,9 @@
-import type { FC } from 'react';
+import {
+  useCallback, useEffect, useRef, useState,
+} from 'react';
 
 import { useSWRxCurrentPage } from '~/stores/page';
+import { usePageControlsX } from '~/stores/ui';
 
 import { PagePathHeader } from './PagePathHeader';
 import { PageTitleHeader } from './PageTitleHeader';
@@ -9,21 +12,45 @@ import styles from './PageHeader.module.scss';
 
 const moduleClass = styles['page-header'] ?? '';
 
-export const PageHeader: FC = () => {
+export const PageHeader = (): JSX.Element => {
+
   const { data: currentPage } = useSWRxCurrentPage();
+  const { data: pageControlsX } = usePageControlsX();
+  const pageHeaderRef = useRef<HTMLDivElement>(null);
+
+  const [maxWidth, setMaxWidth] = useState<number>();
+
+  const calcMaxWidth = useCallback(() => {
+    if (pageControlsX == null || pageHeaderRef.current == null) {
+      // Length that allows users to use PageHeader functionality.
+      setMaxWidth(300);
+      return;
+    }
+    // At least 10px space between PageHeader and PageControls
+    const maxWidth = pageControlsX - pageHeaderRef.current.getBoundingClientRect().x - 10;
+    setMaxWidth(maxWidth);
+  }, [pageControlsX]);
+
+  useEffect(() => {
+    calcMaxWidth();
+  }, [calcMaxWidth]);
 
   if (currentPage == null) {
     return <></>;
   }
 
   return (
-    <div className={`${moduleClass} w-100`}>
+    <div className={`${moduleClass} w-100`} ref={pageHeaderRef}>
       <PagePathHeader
         currentPage={currentPage}
+        maxWidth={maxWidth}
+        onRenameTerminated={calcMaxWidth}
       />
-      <div className="mt-1">
+      <div className="mt-0 mt-md-1">
         <PageTitleHeader
           currentPage={currentPage}
+          maxWidth={maxWidth}
+          onMoveTerminated={calcMaxWidth}
         />
       </div>
     </div>

+ 5 - 1
apps/app/src/components/PageHeader/PagePathHeader.module.scss

@@ -1,5 +1,4 @@
 .page-path-header :global {
-  max-width: calc(100vw - 650px);
   input {
     min-width: 20px;
     min-height: unset;
@@ -17,4 +16,9 @@
       transform: translateY(12px);
     }
   }
+
+  // Make Truncated elements horizontally scrollable and hide the scroll bar
+  .page-path-header-input {
+    scrollbar-width: none;
+  }
 }

+ 27 - 18
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -1,7 +1,6 @@
 import {
   useState, useCallback, memo,
 } from 'react';
-import type { FC } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
@@ -25,11 +24,15 @@ const moduleClass = styles['page-path-header'];
 type Props = {
   currentPage: IPagePopulatedToShowRevision,
   className?: string,
+  maxWidth?: number,
+  onRenameTerminated?: () => void,
 }
 
-export const PagePathHeader: FC<Props> = memo((props: Props) => {
+export const PagePathHeader = memo((props: Props): JSX.Element => {
   const { t } = useTranslation();
-  const { currentPage, className } = props;
+  const {
+    currentPage, className, maxWidth, onRenameTerminated,
+  } = props;
 
   const dPagePath = new DevidedPagePath(currentPage.path, true);
   const parentPagePath = dPagePath.former;
@@ -49,7 +52,8 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
 
   const onRenameFinish = useCallback(() => {
     setRenameInputShown(false);
-  }, []);
+    onRenameTerminated?.();
+  }, [onRenameTerminated]);
 
   const onRenameFailure = useCallback(() => {
     setRenameInputShown(true);
@@ -104,25 +108,28 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
     <div
       id="page-path-header"
       className={`d-flex ${moduleClass} ${className ?? ''} small position-relative ms-2`}
+      style={{ maxWidth }}
       onMouseEnter={() => setHover(true)}
       onMouseLeave={() => setHover(false)}
     >
       <div
-        id="grw-page-path-header-container"
-        className="me-2 d-inline-block overflow-hidden"
+        className="page-path-header-input d-inline-block overflow-x-scroll"
       >
         { isRenameInputShown && (
-          <div className="position-absolute w-100">
-            <ClosableTextInput
-              value={editingParentPagePath}
-              placeholder={t('Input parent page path')}
-              inputClassName="form-control-sm"
-              onPressEnter={onPressEnter}
-              onPressEscape={onPressEscape}
-              onChange={onInputChange}
-              validationTarget={ValidationTarget.PAGE}
-              onClickOutside={onPressEscape}
-            />
+          <div className="position-relative">
+            <div className="position-absolute w-100">
+              <ClosableTextInput
+                value={editingParentPagePath}
+                placeholder={t('Input parent page path')}
+                inputClassName="form-control-sm"
+                onPressEnter={onPressEnter}
+                onPressEscape={onPressEscape}
+                onChange={onInputChange}
+                validationTarget={ValidationTarget.PAGE}
+                onClickOutside={onPressEscape}
+                useAutosizeInput
+              />
+            </div>
           </div>
         ) }
         <div
@@ -136,7 +143,9 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
         </div>
       </div>
 
-      <div className={`page-path-header-buttons d-flex align-items-center ${isHover && !isRenameInputShown ? '' : 'invisible'}`}>
+      <div
+        className={`page-path-header-buttons d-flex align-items-center ${isHover && !isRenameInputShown ? '' : 'invisible'}`}
+      >
         <button
           type="button"
           className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"

+ 5 - 1
apps/app/src/components/PageHeader/PageTitleHeader.module.scss

@@ -1,5 +1,4 @@
 .page-title-header :global {
-  max-width: calc(100vw - 650px);
   input {
     min-width: 20px;
     min-height: unset;
@@ -7,6 +6,11 @@
     line-height: 1em;
     transform: translateX(0.05rem) translateY(0.05rem);
   }
+
+  // Make Truncated elements horizontally scrollable and hide the scroll bar
+  .page-title-header-input {
+    scrollbar-width: none;
+  }
 }
 
 .page-title-header-border-color {

+ 24 - 19
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -1,5 +1,4 @@
-import type { FC } from 'react';
-import { useState, useCallback, useEffect } from 'react';
+import { useState, useCallback } from 'react';
 
 import nodePath from 'path';
 
@@ -24,11 +23,13 @@ const borderColorClass = styles['page-title-header-border-color'] ?? '';
 type Props = {
   currentPage: IPagePopulatedToShowRevision,
   className?: string,
+  maxWidth?: number,
+  onMoveTerminated?: () => void,
 };
 
-export const PageTitleHeader: FC<Props> = (props) => {
+export const PageTitleHeader = (props: Props): JSX.Element => {
   const { t } = useTranslation();
-  const { currentPage } = props;
+  const { currentPage, maxWidth, onMoveTerminated } = props;
 
   const currentPagePath = currentPage.path;
 
@@ -52,7 +53,8 @@ export const PageTitleHeader: FC<Props> = (props) => {
 
   const onRenameFinish = useCallback(() => {
     setRenameInputShown(false);
-  }, []);
+    onMoveTerminated?.();
+  }, [onMoveTerminated]);
 
   const onRenameFailure = useCallback(() => {
     setRenameInputShown(true);
@@ -93,24 +95,27 @@ export const PageTitleHeader: FC<Props> = (props) => {
   // }, [currentPage._id, isNewlyCreatedPage]);
 
   return (
-    <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`}>
-      <div className="me-1 d-inline-block overflow-hidden">
+    <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`} style={{ maxWidth }}>
+      <div className="page-title-header-input me-1 d-inline-block overflow-x-scroll">
         { isRenameInputShown && (
-          <div className="position-absolute w-100">
-            <ClosableTextInput
-              value={isNewlyCreatedPage ? '' : editedPageTitle}
-              placeholder={t('Input page name')}
-              inputClassName="fs-4"
-              onPressEnter={onPressEnter}
-              onPressEscape={onPressEscape}
-              onChange={onInputChange}
-              onClickOutside={() => { setRenameInputShown(false) }}
-              validationTarget={ValidationTarget.PAGE}
-            />
+          <div className="position-relative">
+            <div className="position-absolute w-100">
+              <ClosableTextInput
+                value={isNewlyCreatedPage ? '' : editedPageTitle}
+                placeholder={t('Input page name')}
+                inputClassName="fs-4"
+                onPressEnter={onPressEnter}
+                onPressEscape={onPressEscape}
+                onChange={onInputChange}
+                onClickOutside={() => { setRenameInputShown(false) }}
+                validationTarget={ValidationTarget.PAGE}
+                useAutosizeInput
+              />
+            </div>
           </div>
         ) }
         <h1
-          className={`mb-0 px-2 fs-4
+          className={`mb-0 mb-sm-1 px-2 fs-4
             ${isRenameInputShown ? 'invisible' : ''} text-truncate
             ${isMovable ? 'border border-2 rounded-2' : ''} ${borderColorClass}
           `}

+ 1 - 1
apps/app/src/components/PageRenameModal.tsx

@@ -351,7 +351,7 @@ const PageRenameModal = (): JSX.Element => {
 
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} data-testid="page-rename-modal" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeRenameModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={closeRenameModal}>
         { t('modal_rename.label.Move/Rename page') }
       </ModalHeader>
       <ModalBody>

+ 13 - 10
apps/app/src/components/PageSelectModal/PageSelectModal.tsx

@@ -1,5 +1,5 @@
 import type { FC } from 'react';
-import { useState, useCallback } from 'react';
+import { Suspense, useState, useCallback } from 'react';
 
 import nodePath from 'path';
 
@@ -14,6 +14,7 @@ import { usePageSelectModal } from '~/stores/modal';
 import { useCurrentPagePath, useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 import { ItemsTree } from '../ItemsTree';
+import ItemsTreeContentSkeleton from '../ItemsTree/ItemsTreeContentSkeleton';
 import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
 
 import { TreeItemForModal } from './TreeItemForModal';
@@ -83,15 +84,17 @@ export const PageSelectModal: FC = () => {
     >
       <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
       <ModalBody>
-        <ItemsTree
-          CustomTreeItem={TreeItemForModal}
-          isEnableActions={!isGuestUser}
-          isReadOnlyUser={!!isReadOnlyUser}
-          targetPath={path}
-          targetPathOrId={targetPathOrId}
-          targetAndAncestorsData={targetAndAncestorsData}
-          onClickTreeItem={onClickTreeItem}
-        />
+        <Suspense fallback={<ItemsTreeContentSkeleton />}>
+          <ItemsTree
+            CustomTreeItem={TreeItemForModal}
+            isEnableActions={!isGuestUser}
+            isReadOnlyUser={!!isReadOnlyUser}
+            targetPath={path}
+            targetPathOrId={targetPathOrId}
+            targetAndAncestorsData={targetAndAncestorsData}
+            onClickTreeItem={onClickTreeItem}
+          />
+        </Suspense>
       </ModalBody>
       <ModalFooter>
         <Button color="secondary" onClick={onClickCancel}>{t('Cancel')}</Button>

+ 1 - 1
apps/app/src/components/PageTags/TagsInput.tsx

@@ -39,7 +39,7 @@ export const TagsInput: FC<Props> = (props: Props) => {
   }, [tagsSearch?.tags]);
 
   const keyDownHandler = useCallback((event: KeyboardEvent<HTMLElement>) => {
-    if (event.key === ' ') {
+    if (event.code === 'Space') {
       event.preventDefault();
 
       // fix: https://redmine.weseek.co.jp/issues/140689

+ 1 - 1
apps/app/src/components/PrivateLegacyPages.tsx

@@ -151,7 +151,7 @@ const ConvertByPathModal = React.memo((props: ConvertByPathModalProps): JSX.Elem
 
   return (
     <Modal size="lg" isOpen={props.isOpen} toggle={props.close}>
-      <ModalHeader tag="h4" toggle={props.close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={props.close}>
         { t('private_legacy_pages.by_path_modal.title') }
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/PrivateLegacyPagesMigrationModal.tsx

@@ -74,7 +74,7 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
 
   return (
     <Modal size="lg" isOpen={isOpened} toggle={close}>
-      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={close}>
         { t('private_legacy_pages.modal.title') }
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/PutbackPageModal.jsx

@@ -115,7 +115,7 @@ const PutBackPageModal = () => {
 
   return (
     <Modal isOpen={isOpened} toggle={closeModalHandler} data-testid="put-back-page-modal">
-      <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={closeModalHandler} className="text-info">
         <HeaderContent />
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/SearchPage/SearchOptionModal.tsx

@@ -47,7 +47,7 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
 
   return (
     <Modal size="lg" isOpen={isOpen} toggle={onCloseModal} autoFocus={false}>
-      <ModalHeader tag="h4" toggle={onCloseModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={onCloseModal}>
         Search Option
       </ModalHeader>
       <ModalBody>

+ 1 - 1
apps/app/src/components/ShortcutsModal.tsx

@@ -163,7 +163,7 @@ const ShortcutsModal = (): JSX.Element => {
     <>
       { status != null && (
         <Modal id="shortcuts-modal" size="lg" isOpen={status.isOpened} toggle={close} className={`shortcuts-modal ${styles['shortcuts-modal']}`}>
-          <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+          <ModalHeader tag="h4" toggle={close}>
             {t('Shortcuts')}
           </ModalHeader>
           <ModalBody>

+ 1 - 1
apps/app/src/components/TemplateModal/TemplateModal.tsx

@@ -177,7 +177,7 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
 
   return (
     <div data-testid="template-modal">
-      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={close}>
         {t('template.modal_label.Select template')}
       </ModalHeader>
       <ModalBody className="container">

+ 5 - 4
apps/app/src/components/User/Username.tsx

@@ -1,13 +1,14 @@
 import React from 'react';
 
-import type { IUser } from '@growi/core';
+import type { IUserHasId } from '@growi/core';
+import { isPopulated, type IUser, type Ref } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import Link from 'next/link';
 
-export const Username: React.FC<{ user?: IUser }> = ({ user }): JSX.Element => {
+export const Username: React.FC<{ user?: IUserHasId | Ref<IUser> }> = ({ user }): JSX.Element => {
 
-  if (user == null) {
-    return <span>anyone</span>;
+  if (user == null || !isPopulated(user)) {
+    return <i>(anyone)</i>;
   }
 
   const name = user.name || '(no name)';

+ 1 - 1
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/SyncExecution.tsx

@@ -151,7 +151,7 @@ export const SyncExecution = ({
         isOpen={isAlertModalOpen}
         toggle={() => setIsAlertModalOpen(false)}
       >
-        <ModalHeader tag="h4" toggle={() => setIsAlertModalOpen(false)} className="bg-purple text-light">
+        <ModalHeader tag="h4" toggle={() => setIsAlertModalOpen(false)} className="text-info">
           <span className="material-symbols-outlined me-1 align-middle">error</span>
           <span className="align-middle">{t('external_user_group.confirmation_before_sync')}</span>
         </ModalHeader>

+ 1 - 1
apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginDeleteModal.tsx

@@ -42,7 +42,7 @@ export const PluginDeleteModal: React.FC = () => {
 
   return (
     <Modal isOpen={isOpen} toggle={toggleHandler}>
-      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light" name={name}>
+      <ModalHeader tag="h4" toggle={toggleHandler} className="text-danger" name={name}>
         <span>
           <span className="material-symbols-outlined">delete_forever</span>
           {t('plugins.confirm')}

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

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

+ 0 - 1
apps/app/src/interfaces/user-ui-settings.ts

@@ -2,7 +2,6 @@ import type { SidebarContentsType } from './ui';
 
 export interface IUserUISettings {
   currentSidebarContents: SidebarContentsType,
-  currentPageControlsX: number,
   currentProductNavWidth: number,
   preferCollapsedModeByUser: 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 type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import {
@@ -47,7 +48,6 @@ import {
 } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
-import { useSelectedGrant } from '~/stores/ui';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
@@ -149,6 +149,8 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   isEnabledMarp: boolean,
 
+  sidebarConfig: ISidebarConfig,
+
   isSlackConfigured: boolean,
   // isMailerSetup: boolean,
   isAclEnabled: boolean,
@@ -530,6 +532,11 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
   props.isEnabledAttachTitleHeader = configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader');
 
+  props.sidebarConfig = {
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+  };
+
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     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 type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
@@ -34,6 +35,7 @@ type Props = CommonProps & {
   // Render config
   rendererConfig: RendererConfig,
 
+  sidebarConfig: ISidebarConfig,
 };
 
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
@@ -97,6 +99,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
 
   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 type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
   useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL, useGrowiCloudUri, useCurrentPathname,
@@ -42,6 +43,7 @@ type Props = CommonProps & {
 
   isContainerFluid: boolean,
 
+  sidebarConfig: ISidebarConfig,
 };
 
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
@@ -88,7 +90,8 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 };
 
 type LayoutProps = Props & {
-  children?: ReactNode
+  sidebarConfig: ISidebarConfig,
+  children?: ReactNode,
 }
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
@@ -123,6 +126,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
 
   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 type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
   useCurrentUser, useIsSearchPage, useGrowiCloudUri,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
@@ -41,6 +42,8 @@ type Props = CommonProps & {
 
   // config
   registrationWhitelist: string[],
+
+  sidebarConfig: ISidebarConfig,
 };
 
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
@@ -178,6 +181,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
 
   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 type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IDataTagCount } from '~/interfaces/tag';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxTagsList } from '~/stores/tag';
@@ -38,6 +39,8 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
 
   rendererConfig: RendererConfig,
+
+  sidebarConfig: ISidebarConfig,
 };
 
 const TagList = dynamic(() => import('~/components/TagList'), { ssr: false });
@@ -153,6 +156,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
   props.sidebarConfig = {
     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 type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
@@ -38,6 +39,8 @@ type Props = CommonProps & {
   showPageLimitationXL: number,
 
   rendererConfig: RendererConfig,
+
+  sidebarConfig: ISidebarConfig,
 };
 
 const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
@@ -119,6 +122,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
 
 }

+ 7 - 8
apps/app/src/pages/user-activation.page.tsx

@@ -1,4 +1,4 @@
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Head from 'next/head';
@@ -8,10 +8,11 @@ import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
 import type { RegistrationMode } from '~/interfaces/registration-mode';
-import { IUserRegistrationOrder } from '~/server/models/user-registration-order';
+import type { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
 
+import type { CommonProps } from './utils/commons';
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, CommonProps,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle,
 } from './utils/commons';
 
 type Props = CommonProps & {
@@ -56,18 +57,17 @@ async function injectNextI18NextConfigurations(context: GetServerSidePropsContex
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const result = await getServerSideCommonProps(context);
-  const req: CrowiRequest = context.req as CrowiRequest;
+  const req = context.req as ReqWithUserRegistrationOrder & CrowiRequest;
 
   // check for presence
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   if (!('props' in result)) {
     throw new Error('invalid getSSP result');
   }
-
   const props: Props = result.props as Props;
 
-  if (context.query.userRegistrationOrder != null) {
-    const userRegistrationOrder = context.query.userRegistrationOrder as unknown as IUserRegistrationOrder;
+  if (req.userRegistrationOrder != null) {
+    const userRegistrationOrder = req.userRegistrationOrder;
     props.email = userRegistrationOrder.email;
     props.token = userRegistrationOrder.token;
   }
@@ -75,7 +75,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   if (typeof context.query.errorCode === 'string') {
     props.errorCode = context.query.errorCode as UserActivationErrorCode;
   }
-
   props.registrationMode = req.crowi.configManager.getConfig('crowi', 'security:registrationMode');
   props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
 

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

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

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

@@ -1,5 +1,6 @@
 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 { RehypeSanitizeOption } from '../../interfaces/rehype';
@@ -136,6 +137,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isEnabledMarp': false,
   'customize:isSidebarCollapsedMode': false,
+  'customize:isSidebarClosedAtDockMode': false,
 
   'notification:owner-page:isEnabled': false,
   'notification:group-page:isEnabled': false,

+ 0 - 3
apps/app/src/server/models/user-ui-settings.ts

@@ -23,9 +23,6 @@ const schema = new Schema<UserUISettingsDocument, UserUISettingsModel>({
     enum: SidebarContentsType,
     default: SidebarContentsType.RECENT,
   },
-  currentPageControlsX: {
-    type: Number,
-  },
   currentProductNavWidth: { type: Number },
   preferCollapsedModeByUser: { type: Boolean, default: false },
 });

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

@@ -113,6 +113,7 @@ module.exports = (crowi) => {
     ],
     sidebar: [
       body('isSidebarCollapsedMode').isBoolean(),
+      body('isSidebarClosedAtDockMode').optional().isBoolean(),
     ],
     function: [
       body('isEnabledTimeline').isBoolean(),
@@ -342,7 +343,8 @@ module.exports = (crowi) => {
 
     try {
       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) {
       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) => {
     const requestParams = {
       'customize:isSidebarCollapsedMode': req.body.isSidebarCollapsedMode,
+      'customize:isSidebarClosedAtDockMode': req.body.isSidebarClosedAtDockMode,
     };
 
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
         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 });

+ 9 - 3
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -1,11 +1,11 @@
 import type {
   IPageInfoForListing, IPageInfo,
 } from '@growi/core';
-import { isIPageInfoForEntity } from '@growi/core';
+import { getIdForRef, isIPageInfoForEntity } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, Router } from 'express';
 import express from 'express';
-import { query, oneOf, validationResult } from 'express-validator';
+import { query, oneOf } from 'express-validator';
 import mongoose from 'mongoose';
 
 
@@ -157,7 +157,13 @@ const routerFactory = (crowi: Crowi): Router => {
         const basicPageInfo = pageService.constructBasicPageInfo(page, isGuestUser);
 
         // TODO: use pageService.getCreatorIdForCanDelete to get creatorId (https://redmine.weseek.co.jp/issues/140574)
-        const canDeleteCompletely = pageService.canDeleteCompletely(page, page.creator, req.user, false, userRelatedGroups); // use normal delete config
+        const canDeleteCompletely = pageService.canDeleteCompletely(
+          page,
+          page.creator == null ? null : getIdForRef(page.creator),
+          req.user,
+          false,
+          userRelatedGroups,
+        ); // use normal delete config
 
         const pageInfo = (!isIPageInfoForEntity(basicPageInfo))
           ? basicPageInfo

+ 4 - 3
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -1,4 +1,4 @@
-import { Origin, allOrigin } from '@growi/core';
+import { Origin, allOrigin, getIdForRef } from '@growi/core';
 import type {
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
@@ -89,6 +89,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     }
 
     // persist activity
+    const creator = updatedPage.creator != null ? getIdForRef(updatedPage.creator) : undefined;
     const parameters = {
       targetModel: SupportedTargetModel.MODEL_PAGE,
       target: updatedPage,
@@ -97,7 +98,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     const activityEvent = crowi.event('activity');
     activityEvent.emit(
       'update', res.locals.activity._id, parameters,
-      { path: updatedPage.path, creator: updatedPage.creator._id.toString() },
+      { path: updatedPage.path, creator },
       preNotifyService.generatePreNotify,
     );
 
@@ -157,7 +158,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         return res.apiv3Err(new ErrorV3('Posted param "revisionId" is outdated.', PageUpdateErrorCode.CONFLICT, undefined, { returnLatestRevision }), 409);
       }
 
-      let updatedPage;
+      let updatedPage: PageDocument;
       try {
         const {
           grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,

+ 2 - 2
apps/app/src/server/routes/user-activation.ts

@@ -1,7 +1,7 @@
-import { Response, NextFunction } from 'express';
+import type { Response, NextFunction } from 'express';
 
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
-import { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
+import type { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
 
 type Crowi = {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any

+ 16 - 37
apps/app/src/server/service/file-uploader/aws.ts

@@ -1,3 +1,4 @@
+import type { GetObjectCommandInput, HeadObjectCommandInput } from '@aws-sdk/client-s3';
 import {
   S3Client,
   HeadObjectCommand,
@@ -6,7 +7,6 @@ import {
   PutObjectCommand,
   DeleteObjectCommand,
   ListObjectsCommand,
-  type GetObjectCommandInput,
   ObjectCannedACL,
 } from '@aws-sdk/client-s3';
 import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
@@ -35,19 +35,7 @@ interface FileMeta {
   size: number;
 }
 
-type AwsCredential = {
-  accessKeyId: string,
-  secretAccessKey: string
-}
-type AwsConfig = {
-  credentials: AwsCredential,
-  region: string,
-  endpoint: string,
-  bucket: string,
-  forcePathStyle?: boolean
-}
-
-const isFileExists = async(s3: S3Client, params) => {
+const isFileExists = async(s3: S3Client, params: HeadObjectCommandInput) => {
   try {
     await s3.send(new HeadObjectCommand(params));
   }
@@ -60,22 +48,20 @@ const isFileExists = async(s3: S3Client, params) => {
   return true;
 };
 
-const getAwsConfig = (): AwsConfig => {
-  return {
+const getS3Bucket = (): string | undefined => {
+  return configManager.getConfig('crowi', 'aws:s3Bucket') ?? undefined; // return undefined when getConfig() returns null
+};
+
+const S3Factory = (): S3Client => {
+  return new S3Client({
     credentials: {
       accessKeyId: configManager.getConfig('crowi', 'aws:s3AccessKeyId'),
       secretAccessKey: configManager.getConfig('crowi', 'aws:s3SecretAccessKey'),
     },
     region: configManager.getConfig('crowi', 'aws:s3Region'),
     endpoint: configManager.getConfig('crowi', 'aws:s3CustomEndpoint'),
-    bucket: configManager.getConfig('crowi', 'aws:s3Bucket'),
     forcePathStyle: configManager.getConfig('crowi', 'aws:s3CustomEndpoint') != null, // s3ForcePathStyle renamed to forcePathStyle in v3
-  };
-};
-
-const S3Factory = (): S3Client => {
-  const config = getAwsConfig();
-  return new S3Client(config);
+  });
 };
 
 const getFilePathOnStorage = (attachment) => {
@@ -148,11 +134,10 @@ class AwsFileUploader extends AbstractFileUploader {
     }
 
     const s3 = S3Factory();
-    const awsConfig = getAwsConfig();
     const filePath = getFilePathOnStorage(attachment);
 
     const params = {
-      Bucket: awsConfig.bucket,
+      Bucket: getS3Bucket(),
       Key: filePath,
     };
 
@@ -191,7 +176,6 @@ class AwsFileUploader extends AbstractFileUploader {
     }
 
     const s3 = S3Factory();
-    const awsConfig = getAwsConfig();
     const filePath = getFilePathOnStorage(attachment);
     const lifetimeSecForTemporaryUrl = configManager.getConfig('crowi', 'aws:lifetimeSecForTemporaryUrl');
 
@@ -200,7 +184,7 @@ class AwsFileUploader extends AbstractFileUploader {
     const isDownload = opts?.download ?? false;
     const contentHeaders = new ContentHeaders(attachment, { inline: !isDownload });
     const params: GetObjectCommandInput = {
-      Bucket: awsConfig.bucket,
+      Bucket: getS3Bucket(),
       Key: filePath,
       ResponseContentType: contentHeaders.contentType?.value.toString(),
       ResponseContentDisposition: contentHeaders.contentDisposition?.value.toString(),
@@ -241,14 +225,13 @@ module.exports = (crowi) => {
       throw new Error('AWS is not configured.');
     }
     const s3 = S3Factory();
-    const awsConfig = getAwsConfig();
 
     const filePaths = attachments.map((attachment) => {
       return { Key: getFilePathOnStorage(attachment) };
     });
 
     const totalParams = {
-      Bucket: awsConfig.bucket,
+      Bucket: getS3Bucket(),
       Delete: { Objects: filePaths },
     };
     return s3.send(new DeleteObjectsCommand(totalParams));
@@ -259,10 +242,9 @@ module.exports = (crowi) => {
       throw new Error('AWS is not configured.');
     }
     const s3 = S3Factory();
-    const awsConfig = getAwsConfig();
 
     const params = {
-      Bucket: awsConfig.bucket,
+      Bucket: getS3Bucket(),
       Key: filePath,
     };
 
@@ -284,13 +266,12 @@ module.exports = (crowi) => {
     logger.debug(`File uploading: fileName=${attachment.fileName}`);
 
     const s3 = S3Factory();
-    const awsConfig = getAwsConfig();
 
     const filePath = getFilePathOnStorage(attachment);
     const contentHeaders = new ContentHeaders(attachment);
 
     return s3.send(new PutObjectCommand({
-      Bucket: awsConfig.bucket,
+      Bucket: getS3Bucket(),
       Key: filePath,
       Body: fileStream,
       ACL: ObjectCannedACL.public_read,
@@ -302,10 +283,9 @@ module.exports = (crowi) => {
 
   lib.saveFile = async function({ filePath, contentType, data }) {
     const s3 = S3Factory();
-    const awsConfig = getAwsConfig();
 
     return s3.send(new PutObjectCommand({
-      Bucket: awsConfig.bucket,
+      Bucket: getS3Bucket(),
       ContentType: contentType,
       Key: filePath,
       Body: data,
@@ -329,9 +309,8 @@ module.exports = (crowi) => {
 
     const files: FileMeta[] = [];
     const s3 = S3Factory();
-    const awsConfig = getAwsConfig();
     const params = {
-      Bucket: awsConfig.bucket,
+      Bucket: getS3Bucket(),
     };
     let shouldContinue = true;
     let nextMarker: string | undefined;

+ 2 - 4
apps/app/src/server/service/mail.ts

@@ -7,7 +7,7 @@ import loggerFactory from '~/utils/logger';
 
 import S2sMessage from '../models/vo/s2s-message';
 
-import { S2sMessageHandlable } from './s2s-messaging/handlable';
+import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 
 const logger = loggerFactory('growi:service:mail');
 
@@ -194,13 +194,11 @@ class MailService implements S2sMessageHandlable {
       throw new Error('Mailer is not completed to set up. Please set up SMTP or AWS setting.');
     }
 
-    const renderFilePromisified = promisify(ejs.renderFile);
+    const renderFilePromisified = promisify<string, ejs.Data, string>(ejs.renderFile);
 
     const templateVars = config.vars || {};
     const output = await renderFilePromisified(
       config.template,
-      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-      // @ts-ignore
       templateVars,
     );
 

+ 14 - 4
apps/app/src/server/service/page/index.ts

@@ -263,10 +263,14 @@ class PageService implements IPageService {
     if (page.isEmpty) {
       const Page = mongoose.model<IPage, PageModel>('Page');
       const notEmptyClosestAncestor = await Page.findNonEmptyClosestAncestor(page.path);
-      return notEmptyClosestAncestor?.creator ?? null;
+      return notEmptyClosestAncestor?.creator == null
+        ? null
+        : getIdForRef(notEmptyClosestAncestor.creator);
     }
 
-    return page.creator ?? null;
+    return page.creator == null
+      ? null
+      : getIdForRef(page.creator);
   }
 
   // Use getCreatorIdForCanDelete before execution of canDelete to get creatorId.
@@ -351,13 +355,19 @@ class PageService implements IPageService {
       user: IUserHasId,
       isRecursively: boolean,
       canDeleteFunction: (
-        page: PageDocument, creatorId: ObjectIdLike, operator: any, isRecursively: boolean, userRelatedGroups: PopulatedGrantedGroup[]
+        page: PageDocument, creatorId: ObjectIdLike | null, operator: any, isRecursively: boolean, userRelatedGroups: PopulatedGrantedGroup[]
       ) => boolean,
   ): Promise<PageDocument[]> {
     const userRelatedGroups = await this.pageGrantService.getUserRelatedGroups(user);
     const filteredPages = pages.filter(async(p) => {
       if (p.isEmpty) return true;
-      const canDelete = canDeleteFunction(p, p.creator, user, isRecursively, userRelatedGroups);
+      const canDelete = canDeleteFunction(
+        p,
+        p.creator == null ? null : getIdForRef(p.creator),
+        user,
+        isRecursively,
+        userRelatedGroups,
+      );
       return canDelete;
     });
 

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

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

+ 4 - 4
apps/app/src/stores/remote-latest-page.ts

@@ -1,6 +1,6 @@
 import { useMemo, useCallback } from 'react';
 
-import type { IUser } from '@growi/core';
+import type { IUserHasId } from '@growi/core';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 
@@ -13,8 +13,8 @@ export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string,
   return useSWRStatic<string, Error>('remoteRevisionBody', initialData);
 };
 
-export const useRemoteRevisionLastUpdateUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
-  return useSWRStatic<IUser, Error>('remoteRevisionLastUpdateUser', initialData);
+export const useRemoteRevisionLastUpdateUser = (initialData?: IUserHasId): SWRResponse<IUserHasId, Error> => {
+  return useSWRStatic<IUserHasId, Error>('remoteRevisionLastUpdateUser', initialData);
 };
 
 export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<Date, Error> => {
@@ -24,7 +24,7 @@ export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<
 export type RemoteRevisionData = {
   remoteRevisionId: string,
   remoteRevisionBody: string,
-  remoteRevisionLastUpdateUser: IUser,
+  remoteRevisionLastUpdateUser?: IUserHasId,
   remoteRevisionLastUpdatedAt: Date,
 }
 

+ 2 - 13
apps/app/src/stores/ui.tsx

@@ -272,19 +272,8 @@ export const useCurrentSidebarContents = (
   return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const usePageControlsX = (
-    initialData?: number,
-): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<number>, number> => {
-  const swrResponse = useSWRStatic('pageControlsX', initialData, { fallbackData: 1000 });
-
-  const { mutate } = swrResponse;
-
-  const mutateAndSave: MutateAndSaveUserUISettings<number> = useCallback((data, opt?) => {
-    scheduleToPut({ currentPageControlsX: data });
-    return mutate(data, opt);
-  }, [mutate]);
-
-  return withUtils(swrResponse, { mutateAndSave });
+export const usePageControlsX = (initialData?: number): SWRResponse<number> => {
+  return useSWRStatic('pageControlsX', initialData);
 };
 
 export const useCurrentProductNavWidth = (initialData?: number): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<number>, number> => {

+ 4 - 0
apps/app/src/styles/_layout.scss

@@ -2,6 +2,10 @@
 
 @use './variables' as var;
 
+body {
+  min-height: 100vh;
+}
+
 .dynamic-layout-root {
   @extend .flex-expand-vert;
 }

+ 2 - 2
packages/core/src/interfaces/common.ts

@@ -10,11 +10,11 @@ export type Ref<T> = string | T & HasObjectId;
 
 export type Nullable<T> = T | null | undefined;
 
-export const isPopulated = <T>(ref: Ref<T>): ref is T & HasObjectId => {
+export const isPopulated = <T>(ref: T & HasObjectId | Ref<T>): ref is T & HasObjectId => {
   return !(typeof ref === 'string');
 };
 
-export const getIdForRef = <T>(ref: Ref<T>): string => {
+export const getIdForRef = <T>(ref: T & HasObjectId | Ref<T>): string => {
   return isPopulated(ref)
     ? ref._id
     : ref;

+ 4 - 8
packages/core/src/interfaces/page.ts

@@ -20,7 +20,7 @@ export type IPage = {
   status: string,
   revision?: Ref<IRevision>,
   tags: Ref<ITag>[],
-  creator: any,
+  creator?: Ref<IUser>,
   createdAt: Date,
   updatedAt: Date,
   seenUsers: Ref<IUser>[],
@@ -30,7 +30,7 @@ export type IPage = {
   grant: PageGrant,
   grantedUsers: Ref<IUser>[],
   grantedGroups: IGrantedGroup[],
-  lastUpdateUser: Ref<IUser>,
+  lastUpdateUser?: Ref<IUser>,
   liker: Ref<IUser>[],
   commentCount: number
   slackChannels: string,
@@ -43,13 +43,9 @@ export type IPage = {
   ttlTimestamp?: Date
 }
 
-export type IPagePopulatedToList = Omit<IPageHasId, 'lastUpdateUser'> & {
-  lastUpdateUser: IUserHasId,
-}
-
 export type IPagePopulatedToShowRevision = Omit<IPageHasId, 'lastUpdateUser'|'creator'|'deleteUser'|'grantedGroups'|'revision'|'author'> & {
-  lastUpdateUser: IUserHasId,
-  creator: IUserHasId | null,
+  lastUpdateUser?: IUserHasId,
+  creator?: IUserHasId,
   deleteUser: IUserHasId,
   grantedGroups: { type: GroupType, item: IUserGroupHasId }[],
   revision?: IRevisionHasId,

+ 2 - 1
packages/core/src/interfaces/revision.ts

@@ -1,3 +1,4 @@
+import type { Ref } from './common';
 import type { HasObjectId } from './has-object-id';
 import type { IUser } from './user';
 
@@ -12,7 +13,7 @@ export const allOrigin = Object.values(Origin);
 
 export type IRevision = {
   body: string,
-  author: IUser,
+  author: Ref<IUser>,
   hasDiffToPrev: boolean;
   createdAt: Date,
   updatedAt: Date,

+ 3 - 2
packages/editor/src/components/CodeMirrorEditorMain.tsx

@@ -21,18 +21,19 @@ type Props = CodeMirrorEditorProps & {
   user?: IUserHasId,
   pageId?: string,
   initialValue?: string,
+  isEditorMode: boolean,
   onEditorsUpdated?: (userList: IUserHasId[]) => void,
 }
 
 export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
   const {
-    user, pageId, initialValue,
+    user, pageId, initialValue, isEditorMode,
     onSave, onEditorsUpdated, ...otherProps
   } = props;
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
-  useCollaborativeEditorMode(user, pageId, initialValue, onEditorsUpdated, codeMirrorEditor);
+  useCollaborativeEditorMode(isEditorMode, user, pageId, initialValue, onEditorsUpdated, codeMirrorEditor);
 
   // setup additional extensions
   useEffect(() => {

+ 1 - 0
packages/editor/src/components/playground/Playground.tsx

@@ -70,6 +70,7 @@ export const Playground = (): JSX.Element => {
       <div className="flex-expand-horiz">
         <div className="flex-expand-vert">
           <CodeMirrorEditorMain
+            isEditorMode
             onSave={saveHandler}
             onChange={setMarkdownToPreview}
             onUpload={uploadHandler}

+ 6 - 2
packages/editor/src/services/editor-theme/material.ts

@@ -35,6 +35,8 @@ const tooltipBackground = base01;
 const selection = base01;
 // Change color
 const cursor = base05;
+// Create New color
+const activeLineBackground = '#00000020';
 
 // / The editor theme styles for Material Dark.
 export const materialDarkTheme = EditorView.theme(
@@ -64,7 +66,8 @@ export const materialDarkTheme = EditorView.theme(
       backgroundColor: highlightBackground,
     },
 
-    '.cm-activeLine': { backgroundColor: highlightBackground },
+    // Customize
+    '.cm-activeLine': { backgroundColor: activeLineBackground },
     '.cm-selectionMatch': {
       backgroundColor: darkBackground,
       outline: `1px solid ${base_teal}`,
@@ -85,8 +88,9 @@ export const materialDarkTheme = EditorView.theme(
       color: base02,
     },
 
+    // Customize
     '.cm-activeLineGutter': {
-      backgroundColor: highlightBackground,
+      backgroundColor: activeLineBackground,
       color: base07,
     },
 

+ 9 - 5
packages/editor/src/services/editor-theme/nord.ts

@@ -33,13 +33,15 @@ const base0F = '#b48ead'; // purple
 
 const invalid = '#d30102';
 const darkBackground = '#252a33';
-// Cutomize
+// Customize
 const highlightBackground = base02;
 const background = base00;
 const tooltipBackground = base01;
 const selection = base03;
-// Cutomize
+// Customize
 const cursor = base06;
+// Create New color
+const activeLineBackground = '#00000020';
 
 // / The editor theme styles for Nord.
 export const nordTheme = EditorView.theme(
@@ -70,7 +72,8 @@ export const nordTheme = EditorView.theme(
       color: base00,
     },
 
-    '.cm-activeLine': { backgroundColor: highlightBackground },
+    // Custom
+    '.cm-activeLine': { backgroundColor: activeLineBackground },
     '.cm-selectionMatch': {
       backgroundColor: base05,
       color: base01,
@@ -82,7 +85,7 @@ export const nordTheme = EditorView.theme(
     },
 
     '&.cm-focused .cm-matchingBracket': {
-      // Cutomize
+      // Customize
       backgroundColor: base02,
       color: base02,
     },
@@ -93,8 +96,9 @@ export const nordTheme = EditorView.theme(
       border: 'none',
     },
 
+    // Custom
     '.cm-activeLineGutter': {
-      backgroundColor: highlightBackground,
+      backgroundColor: activeLineBackground,
       color: base04,
     },
 

+ 3 - 2
packages/editor/src/services/list-util/insert-newline-continue-markup.ts

@@ -1,8 +1,9 @@
 import type { ChangeSpec } from '@codemirror/state';
 import { EditorView } from '@codemirror/view';
 
-// https://regex101.com/r/7BN2fR/5
-const indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
+// https://regex101.com/r/r9plEA/1
+const indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]\s))(\s*)/;
+// https://regex101.com/r/HFYoFN/1
 const indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
 export const insertNewlineContinueMarkup = (editor: EditorView): void => {

+ 2 - 2
packages/editor/src/services/table-util/insert-new-row-to-table-markdown.ts

@@ -130,8 +130,6 @@ const removeRow = (editor: EditorView) => {
   const bolPos = editor.state.doc.line(curLine).from;
   const eolPos = editor.state.doc.line(curLine).to;
 
-  const nextCurPos = editor.state.doc.lineAt(getCurPos(editor)).to + 1;
-
   editor.dispatch({
     changes: {
       from: bolPos,
@@ -139,6 +137,8 @@ const removeRow = (editor: EditorView) => {
     },
   });
 
+  const nextCurPos = editor.state.doc.lineAt(getCurPos(editor)).to + 1;
+
   editor.dispatch({
     selection: { anchor: nextCurPos },
   });

Some files were not shown because too many files changed in this diff