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

Merge branch 'dev/7.0.x' into fix/134173-fix-tag-edit

soumaeda 2 лет назад
Родитель
Сommit
b6cc985daf
65 измененных файлов с 777 добавлено и 343 удалено
  1. 3 0
      .github/workflows/reusable-app-prod.yml
  2. 0 17
      apps/app/_obsolete/src/components/Sidebar/NavigationResizeHexagon.tsx
  3. 1 0
      apps/app/package.json
  4. 13 0
      apps/app/public/static/locales/en_US/commons.json
  5. 0 2
      apps/app/public/static/locales/en_US/translation.json
  6. 13 0
      apps/app/public/static/locales/ja_JP/commons.json
  7. 0 2
      apps/app/public/static/locales/ja_JP/translation.json
  8. 13 0
      apps/app/public/static/locales/zh_CN/commons.json
  9. 0 2
      apps/app/public/static/locales/zh_CN/translation.json
  10. 17 1
      apps/app/src/client/services/page-operation.ts
  11. 2 3
      apps/app/src/client/services/user-ui-settings.ts
  12. 3 3
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx
  13. 8 0
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.module.scss
  14. 0 2
      apps/app/src/components/Common/DrawerToggler/DrawerToggler.module.scss
  15. 7 0
      apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss
  16. 1 2
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  17. 0 0
      apps/app/src/components/Common/PageViewLayout.module.scss
  18. 0 0
      apps/app/src/components/Common/PageViewLayout.tsx
  19. 1 1
      apps/app/src/components/Layout/BasicLayout.tsx
  20. 1 1
      apps/app/src/components/Page/PageView.tsx
  21. 9 9
      apps/app/src/components/PageDeleteModal.tsx
  22. 2 2
      apps/app/src/components/PageRenameModal.tsx
  23. 1 1
      apps/app/src/components/SearchPage/SearchPageBase.tsx
  24. 3 1
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  25. 1 1
      apps/app/src/components/ShareLinkPageView.tsx
  26. 0 147
      apps/app/src/components/Sidebar/PageCreateButton.tsx
  27. 46 0
      apps/app/src/components/Sidebar/PageCreateButton/CreateButton.module.scss
  28. 24 0
      apps/app/src/components/Sidebar/PageCreateButton/CreateButton.tsx
  29. 69 0
      apps/app/src/components/Sidebar/PageCreateButton/DropendMenu.tsx
  30. 60 0
      apps/app/src/components/Sidebar/PageCreateButton/DropendToggle.module.scss
  31. 24 0
      apps/app/src/components/Sidebar/PageCreateButton/DropendToggle.tsx
  32. 18 0
      apps/app/src/components/Sidebar/PageCreateButton/Hexagon.tsx
  33. 2 0
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.module.scss
  34. 205 0
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  35. 1 0
      apps/app/src/components/Sidebar/PageCreateButton/index.ts
  36. 2 5
      apps/app/src/components/Sidebar/Sidebar.tsx
  37. 1 1
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.module.scss
  38. 1 1
      apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx
  39. 3 6
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.module.scss
  40. 2 4
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx
  41. 2 5
      apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.module.scss
  42. 2 2
      apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.tsx
  43. 2 2
      apps/app/src/components/Sidebar/SidebarNav/SkeletonItem.module.scss
  44. 0 1
      apps/app/src/components/Sidebar/SidebarNav/_variables.scss
  45. 10 3
      apps/app/src/components/Sidebar/_button-styles.scss
  46. 1 0
      apps/app/src/components/Sidebar/_variables.scss
  47. 1 1
      apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  48. 1 1
      apps/app/src/migrations/20230213090921-remove-presentation-configurations.js
  49. 1 1
      apps/app/src/migrations/20230731075753-add_installed_date_to_config.js
  50. 34 0
      apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js
  51. 8 8
      apps/app/src/pages/[[...path]].page.tsx
  52. 18 4
      apps/app/src/pages/me/[[...path]].page.tsx
  53. 2 4
      apps/app/src/server/routes/apiv3/pages.js
  54. 47 14
      apps/app/src/stores/ui.tsx
  55. 2 1
      apps/app/src/styles/_editor.scss
  56. 1 11
      apps/app/src/styles/_layout.scss
  57. 60 0
      apps/app/turbo.json
  58. 17 4
      packages/core/scss/_flex-expand.scss
  59. 1 0
      packages/core/scss/bootstrap/_variables.scss
  60. 0 17
      packages/core/scss/placeholders/_flex-expand.scss
  61. 1 1
      packages/editor/src/components/playground/Playground.tsx
  62. 1 0
      packages/editor/turbo.json
  63. 2 0
      packages/ui/scss/atoms/_btn-muted.scss
  64. 1 47
      turbo.json
  65. 5 2
      yarn.lock

+ 3 - 0
.github/workflows/reusable-app-prod.yml

@@ -27,6 +27,9 @@ jobs:
 
     steps:
     - uses: actions/checkout@v3
+      with:
+        # retrieve local font files
+        lfs: true
 
     - uses: actions/setup-node@v3
       with:

+ 0 - 17
apps/app/_obsolete/src/components/Sidebar/NavigationResizeHexagon.tsx

@@ -1,17 +0,0 @@
-import React from 'react';
-
-export const NavigationResizeHexagon = React.memo((): JSX.Element => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 27.691 23.999"
-  >
-    <g className="background" transform="translate(0 0)">
-      <path d="M20.768,0l6.923,12L20.768,24H6.923L0,12,6.923,0Z" transform="translate(0)"></path>
-    </g>
-    <g className="icon" transform="translate(10 6)">
-      { /* eslint-disable-next-line max-len */ }
-      <path d="M2.124,9.114l5.28,5.34a.647.647,0,0,0,.922,0l.616-.623a.665.665,0,0,0,0-.932L4.759,8.648,8.943,4.4a.665.665,0,0,0,0-.932l-.616-.623a.647.647,0,0,0-.922,0l-5.28,5.34A.665.665,0,0,0,2.124,9.114Z" transform="translate(-1.933 -2.648)"></path>
-    </g>
-  </svg>
-));
-NavigationResizeHexagon.displayName = 'NavigationResizeHexagon';

+ 1 - 0
apps/app/package.json

@@ -218,6 +218,7 @@
     "@types/express": "^4.17.11",
     "@types/jest": "^29.5.2",
     "@types/react-scroll": "^1.8.4",
+    "@types/throttle-debounce": "^5.0.1",
     "@types/url-join": "^4.0.2",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",

+ 13 - 0
apps/app/public/static/locales/en_US/commons.json

@@ -68,6 +68,19 @@
     "feedback": "Feedback"
   },
 
+  "create_page_dropdown": {
+    "new_page": "Create New Page",
+    "todays": {
+      "desc": "Create today's ...",
+      "memo": "memo"
+    },
+    "template": {
+      "desc": "Create/Edit template page",
+      "children": "Template for children",
+      "descendants": "Template for descendants"
+    }
+  },
+
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",

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

@@ -106,8 +106,6 @@
   "Disclose E-mail": "Disclose E-mail",
   "page exists": "this page already exists",
   "Error occurred": "Error occurred",
-  "Create today's": "Create today's ...",
-  "Memo": "memo",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New page",

+ 13 - 0
apps/app/public/static/locales/ja_JP/commons.json

@@ -70,6 +70,19 @@
     "feedback": "ご意見・ご要望"
   },
 
+  "create_page_dropdown": {
+    "new_page": "新規ページ作成",
+    "todays": {
+      "desc": "今日の◯◯を作成",
+      "memo": "メモ"
+    },
+    "template": {
+      "desc": "テンプレートページの作成/編集",
+      "children": "同一階層テンプレート",
+      "decendants": "下位層テンプレート"
+    }
+  },
+
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",

+ 0 - 2
apps/app/public/static/locales/ja_JP/translation.json

@@ -105,8 +105,6 @@
   "Disclose E-mail": "メールアドレスの公開",
   "page exists": "このページはすでに存在しています",
   "Error occurred": "エラーが発生しました",
-  "Create today's": "今日の◯◯を作成",
-  "Memo": "メモ",
   "Input page name": "ページ名を入力",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "New Page": "新規ページ",

+ 13 - 0
apps/app/public/static/locales/zh_CN/commons.json

@@ -71,6 +71,19 @@
     "feedback": "意见和要求"
   },
 
+  "create_page_dropdown": {
+    "new_page": "新页面",
+    "todays": {
+      "desc": "Create today's ...",
+      "memo": "memo"
+    },
+    "template": {
+      "desc": "创建/编辑模板页",
+      "children": "子模板",
+      "descendants": "子代模板"
+    }
+  },
+
 	"copy_to_clipboard": {
 		"Copy to clipboard": "复制到剪贴板",
 		"Page path": "页面路径",

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

@@ -111,8 +111,6 @@
 	"Disclose E-mail": "显示邮箱",
 	"page exists": "页面已存在",
 	"Error occurred": "Error occurred",
-	"Create today's": "Create today's ...",
-	"Memo": "memo",
 	"Input page name": "Input page name",
 	"Input page name (optional)": "Input page name (optional)",
 	"New Page": "新页面",

+ 17 - 1
apps/app/src/client/services/page-operation.ts

@@ -9,7 +9,7 @@ import { useCurrentPageId, useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stor
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 
-import { apiPost } from '../util/apiv1-client';
+import { apiGet, apiPost } from '../util/apiv1-client';
 import { apiv3Post, apiv3Put } from '../util/apiv3-client';
 import { toastError } from '../util/toastr';
 
@@ -207,3 +207,19 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
 export const unlink = async(path: string): Promise<void> => {
   await apiPost('/pages.unlink', { path });
 };
+
+
+interface PageExistRequest {
+  pagePaths: string;
+}
+
+interface PageExistResponse {
+  pages: Record<string, boolean>;
+  ok: boolean
+}
+
+export const exist = async(pagePaths: string): Promise<PageExistResponse> => {
+  const request: PageExistRequest = { pagePaths };
+  const res = await apiGet<PageExistResponse>('/pages.exist', request);
+  return res;
+};

+ 2 - 3
apps/app/src/client/services/user-ui-settings.ts

@@ -17,12 +17,11 @@ const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> =>
 
 const _putUserUISettingsInBulkDebounced = debounce(1500, _putUserUISettingsInBulk);
 
-type ScheduleToPutFunction = (settings: Partial<IUserUISettings>) => Promise<AxiosResponse<IUserUISettings>>;
-export const scheduleToPut: ScheduleToPutFunction = (settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {
+export const scheduleToPut = (settings: Partial<IUserUISettings>): void => {
   settingsForBulk = {
     ...settingsForBulk,
     ...settings,
   };
 
-  return _putUserUISettingsInBulkDebounced();
+  _putUserUISettingsInBulkDebounced();
 };

+ 3 - 3
apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx

@@ -110,10 +110,10 @@ export const CopyDropdown = (props) => {
 
   return (
     <>
-      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown d-print-none`} isOpen={dropdownOpen} toggle={toggleDropdown}>
+      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown d-print-none`} isOpen={dropdownOpen} size="sm" toggle={toggleDropdown}>
         <DropdownToggle
-          caret
-          className={dropdownToggleClassName}
+          caret={isShareLinkMode}
+          className={`btn-copy ${dropdownToggleClassName}`}
         >
           <span id={dropdownToggleId}>{children}</span>
         </DropdownToggle>

+ 8 - 0
apps/app/src/components/Common/CopyDropdown/CopyDropdown.module.scss

@@ -1,4 +1,12 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '@growi/ui/scss/atoms/btn-muted';
+
 .grw-copy-dropdown :global {
+  .btn.btn-copy {
+    @include btn-muted.colorize(bs.$gray-500);
+  }
+
   .dropdown-menu {
     min-width: 310px;
 

+ 0 - 2
apps/app/src/components/Common/DrawerToggler/DrawerToggler.module.scss

@@ -1,7 +1,5 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
-@use '@growi/ui/scss/atoms/btn-muted';
-
 @use '~/styles/variables' as var;
 
 

+ 7 - 0
apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss

@@ -1,5 +1,7 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
+@use '@growi/ui/scss/atoms/btn-muted';
+
 .grw-mr-02em {
   margin-right: 0.2em;
 }
@@ -19,3 +21,8 @@
   }
 }
 
+.grw-page-path-nav :global {
+  .btn-copy {
+    @include btn-muted.colorize(bs.$orange);
+  }
+}

+ 1 - 2
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -76,7 +76,6 @@ export const PagePathNav: FC<Props> = (props: Props) => {
   }
 
   const copyDropdownId = `copydropdown-${pageId}`;
-  const copyDropdownToggleClassName = 'd-block btn-outline-secondary btn-copy border-0 text-muted p-2';
 
   return (
     <div>
@@ -87,7 +86,7 @@ export const PagePathNav: FC<Props> = (props: Props) => {
         </h1>
         { pageId != null && !isNotFound && (
           <div className="mx-2">
-            <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName={copyDropdownToggleClassName}>
+            <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
               <i className="ti ti-clipboard"></i>
             </CopyDropdown>
           </div>

+ 0 - 0
apps/app/src/components/Layout/PageViewLayout.module.scss → apps/app/src/components/Common/PageViewLayout.module.scss


+ 0 - 0
apps/app/src/components/Layout/PageViewLayout.tsx → apps/app/src/components/Common/PageViewLayout.tsx


+ 1 - 1
apps/app/src/components/Layout/BasicLayout.tsx

@@ -33,7 +33,7 @@ type Props = {
 
 export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
-    <RawLayout className={className ?? ''}>
+    <RawLayout className={`${className ?? ''}`}>
       <DndProvider backend={HTML5Backend}>
 
         <div className="page-wrapper flex-row">

+ 1 - 1
apps/app/src/components/Page/PageView.tsx

@@ -18,7 +18,7 @@ import { useIsMobile } from '~/stores/ui';
 
 import type { CommentsProps } from '../Comments';
 import { PagePathNavSticky } from '../Common/PagePathNav';
-import { PageViewLayout } from '../Layout/PageViewLayout';
+import { PageViewLayout } from '../Common/PageViewLayout';
 import { PageAlerts } from '../PageAlert/PageAlerts';
 import { PageContentFooter } from '../PageContentFooter';
 import type { PageSideContentsProps } from '../PageSideContents';

+ 9 - 9
apps/app/src/components/PageDeleteModal.tsx

@@ -32,12 +32,12 @@ const logger = loggerFactory('growi:cli:PageDeleteModal');
 const deleteIconAndKey = {
   completely: {
     color: 'danger',
-    icon: 'fire',
+    icon: 'delete_forever',
     translationKey: 'completely',
   },
   temporary: {
-    color: 'primary',
-    icon: 'trash',
+    color: 'warning',
+    icon: 'delete',
     translationKey: 'page',
   },
 };
@@ -245,10 +245,10 @@ const PageDeleteModal: FC = () => {
     }
 
     return (
-      <>
-        <i className={`icon-fw icon-${deleteIconAndKey[deleteMode].icon}`}></i>
-        { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
-      </>
+      <span className={`text-${deleteIconAndKey[deleteMode].color} d-flex align-items-center`}>
+        <span className="material-symbols-outlined me-1">{deleteIconAndKey[deleteMode].icon}</span>
+        <b>{ t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }</b>
+      </span>
     );
   };
 
@@ -280,7 +280,7 @@ const PageDeleteModal: FC = () => {
         <ApiErrorMessageList errs={errs} />
         <button
           type="button"
-          className={`btn btn-${deleteIconAndKey[deleteMode].color}`}
+          className={`btn btn-outline-${deleteIconAndKey[deleteMode].color}`}
           disabled={!isDeletable}
           onClick={deleteButtonHandler}
           data-testid="delete-page-button"
@@ -294,7 +294,7 @@ const PageDeleteModal: FC = () => {
 
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeDeleteModal} data-testid="page-delete-modal">
-      <ModalHeader tag="h4" toggle={closeDeleteModal} className={`bg-${deleteIconAndKey[deleteMode].color} text-light`}>
+      <ModalHeader toggle={closeDeleteModal}>
         {headerContent()}
       </ModalHeader>
       <ModalBody>

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

@@ -152,12 +152,12 @@ const PageRenameModal = (): JSX.Element => {
   }, [checkExistPaths]);
 
   const checkIsUsersHomepageDebounce = useMemo(() => {
-    const checkIsPagePathRenameable = () => {
+    const checkIsPagePathRenameable = (pageNameInput: string) => {
       setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
     };
 
     return debounce(1000, checkIsPagePathRenameable);
-  }, [isUsersHomepage, pageNameInput]);
+  }, [isUsersHomepage]);
 
   useEffect(() => {
     if (isOpened && page != null && pageNameInput !== page.data.path) {

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

@@ -169,7 +169,7 @@ const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturn
     : undefined;
 
   return (
-    <div className="search-result-base flex-expand-horiz" data-testid="search-result-base">
+    <div className="search-result-base flex-grow-1 d-flex flex-expand-vh-100" data-testid="search-result-base">
 
       <div className="flex-expand-vert border boder-gray search-result-list" id="search-result-list">
 

+ 3 - 1
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -31,6 +31,8 @@ import type { PageContentFooterProps } from '../PageContentFooter';
 
 import styles from './SearchResultContent.module.scss';
 
+const moduleClass = styles['search-result-content'];
+
 
 const SubNavButtons = dynamic(() => import('../PageControls').then(mod => mod.PageControls), { ssr: false });
 const RevisionLoader = dynamic<RevisionLoaderProps>(() => import('../Page/RevisionLoader').then(mod => mod.RevisionLoader), { ssr: false });
@@ -210,7 +212,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     <div
       key={page._id}
       data-testid="search-result-content"
-      className={`dynamic-layout-root ${growiLayoutFluidClass} search-result-content ${styles['search-result-content']}`}
+      className={`dynamic-layout-root ${growiLayoutFluidClass} ${moduleClass}`}
     >
       <RightComponent />
 

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

@@ -11,7 +11,7 @@ import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
 import { PagePathNavSticky } from './Common/PagePathNav';
-import { PageViewLayout } from './Layout/PageViewLayout';
+import { PageViewLayout } from './Common/PageViewLayout';
 import RevisionRenderer from './Page/RevisionRenderer';
 import ShareLinkAlert from './Page/ShareLinkAlert';
 import type { PageSideContentsProps } from './PageSideContents';

+ 0 - 147
apps/app/src/components/Sidebar/PageCreateButton.tsx

@@ -1,147 +0,0 @@
-import React, { useCallback, useState } from 'react';
-
-import { useRouter } from 'next/router';
-
-import { createPage } from '~/client/services/page-operation';
-import { toastError } from '~/client/util/toastr';
-import { useSWRxCurrentPage } from '~/stores/page';
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:cli:PageCreateButton');
-
-export const PageCreateButton = React.memo((): JSX.Element => {
-  const router = useRouter();
-  const { data: currentPage, isLoading } = useSWRxCurrentPage();
-
-  const [isHovered, setIsHovered] = useState(false);
-  const [isCreating, setIsCreating] = useState(false);
-
-  const onMouseEnterHandler = () => {
-    setIsHovered(true);
-  };
-
-  const onMouseLeaveHandler = () => {
-    setIsHovered(false);
-  };
-
-  const onCreateNewPageButtonHandler = useCallback(async() => {
-    if (isLoading) return;
-
-    try {
-      setIsCreating(true);
-
-      const parentPath = currentPage == null
-        ? '/'
-        : currentPage.path;
-
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: currentPage?.grant || 1,
-        pageTags: [],
-        grantUserGroupId: currentPage?.grantedGroup?._id,
-        shouldGeneratePath: true,
-      };
-
-      const response = await createPage(parentPath, '', params);
-
-      router.push(`${response.page.id}#edit`);
-    }
-    catch (err) {
-      logger.warn(err);
-      toastError(err);
-    }
-    finally {
-      setIsCreating(false);
-    }
-  }, [currentPage, isLoading, router]);
-  const onCreateTodaysButtonHandler = useCallback(() => {
-    // router.push(`${router.pathname}#edit`);
-  }, [router]);
-  const onTemplateForChildrenButtonHandler = useCallback(() => {
-    // router.push(`${router.pathname}/_template#edit`);
-  }, [router]);
-  const onTemplateForDescendantsButtonHandler = useCallback(() => {
-    // router.push(`${router.pathname}/__template#edit`);
-  }, [router]);
-
-  // TODO: update button design
-  // https://redmine.weseek.co.jp/issues/132683
-  // TODO: i18n
-  // https://redmine.weseek.co.jp/issues/132681
-  return (
-    <div
-      className="d-flex flex-row"
-      onMouseEnter={onMouseEnterHandler}
-      onMouseLeave={onMouseLeaveHandler}
-    >
-      <div className="btn-group">
-        <button
-          className="d-block btn btn-primary"
-          onClick={onCreateNewPageButtonHandler}
-          type="button"
-          data-testid="grw-sidebar-nav-page-create-button"
-          disabled={isCreating}
-        >
-          <i className="material-symbols-outlined">edit</i>
-        </button>
-      </div>
-      {isHovered && (
-        <div className="btn-group dropend">
-          <button
-            className="btn btn-secondary dropdown-toggle dropdown-toggle-split position-absolute"
-            type="button"
-            data-bs-toggle="dropdown"
-            aria-expanded="false"
-          />
-          <ul className="dropdown-menu">
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onCreateNewPageButtonHandler}
-                type="button"
-                disabled={isCreating}
-              >
-                Create New Page
-              </button>
-            </li>
-            <li><hr className="dropdown-divider" /></li>
-            <li><span className="text-muted px-3">Create today&apos;s ...</span></li>
-            {/* TODO: show correct create today's page path */}
-            {/* https://redmine.weseek.co.jp/issues/132682 */}
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onCreateTodaysButtonHandler}
-                type="button"
-              >
-                Create today&apos;s
-              </button>
-            </li>
-            <li><hr className="dropdown-divider" /></li>
-            <li><span className="text-muted px-3">Child page template</span></li>
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onTemplateForChildrenButtonHandler}
-                type="button"
-              >
-                Template for children
-              </button>
-            </li>
-            <li>
-              <button
-                className="dropdown-item"
-                onClick={onTemplateForDescendantsButtonHandler}
-                type="button"
-              >
-                Template for descendants
-              </button>
-            </li>
-          </ul>
-        </div>
-      )}
-    </div>
-  );
-});
-PageCreateButton.displayName = 'PageCreateButton';

+ 46 - 0
apps/app/src/components/Sidebar/PageCreateButton/CreateButton.module.scss

@@ -0,0 +1,46 @@
+@use '~/styles/variables' as var;
+
+@use '../button-styles';
+
+.btn-create :global {
+  @extend %btn-basis;
+
+  // centering
+  .icon {
+    top: 50%;
+    left: 50%;
+    transform: translateX(-50%) translateY(-50%);
+  }
+}
+
+// pointer-events
+.btn-create :global {
+  pointer-events: none;
+
+  svg .background {
+    pointer-events: fill;
+  }
+}
+
+// == Colors
+.btn-create {
+  background-color: transparent !important;
+}
+
+.btn-create :global {
+  svg {
+    fill: var(--bs-btn-bg);
+  }
+}
+
+.btn-create:hover :global {
+  svg {
+    fill: var(--bs-btn-hover-bg);
+  }
+}
+
+.btn-create:active :global {
+  svg {
+    fill: var(--bs-btn-active-bg);
+  }
+}

+ 24 - 0
apps/app/src/components/Sidebar/PageCreateButton/CreateButton.tsx

@@ -0,0 +1,24 @@
+import type { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
+
+import { Hexagon } from './Hexagon';
+
+import styles from './CreateButton.module.scss';
+
+const moduleClass = styles['btn-create'];
+
+
+type Props = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
+
+export const CreateButton = (props: Props): JSX.Element => {
+  return (
+    <button
+      type="button"
+      {...props}
+      className={`${moduleClass} btn btn-primary ${props.className ?? ''}`}
+      data-testid="grw-sidebar-nav-page-create-button"
+    >
+      <Hexagon />
+      <span className="icon material-symbols-outlined position-absolute">edit</span>
+    </button>
+  );
+};

+ 69 - 0
apps/app/src/components/Sidebar/PageCreateButton/DropendMenu.tsx

@@ -0,0 +1,69 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+type DropendMenuProps = {
+  todaysPath: string,
+  onClickCreateNewPageButtonHandler: () => Promise<void>
+  onClickCreateTodaysButtonHandler: () => Promise<void>
+  onClickTemplateForChildrenButtonHandler: () => Promise<void>
+  onClickTemplateForDescendantsButtonHandler: () => Promise<void>
+}
+
+export const DropendMenu = React.memo((props: DropendMenuProps): JSX.Element => {
+  const {
+    todaysPath,
+    onClickCreateNewPageButtonHandler,
+    onClickCreateTodaysButtonHandler,
+    onClickTemplateForChildrenButtonHandler,
+    onClickTemplateForDescendantsButtonHandler,
+  } = props;
+
+  const { t } = useTranslation('commons');
+
+  return (
+    <ul className="dropdown-menu">
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickCreateNewPageButtonHandler}
+          type="button"
+        >
+          {t('create_page_dropdown.new_page')}
+        </button>
+      </li>
+      <li><hr className="dropdown-divider" /></li>
+      <li><span className="text-muted px-3">{t('create_page_dropdown.todays.desc')}</span></li>
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickCreateTodaysButtonHandler}
+          type="button"
+        >
+          {todaysPath}
+        </button>
+      </li>
+      <li><hr className="dropdown-divider" /></li>
+      <li><span className="text-muted text-nowrap px-3">{t('create_page_dropdown.template.desc')}</span></li>
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickTemplateForChildrenButtonHandler}
+          type="button"
+        >
+          {t('create_page_dropdown.template.children')}
+        </button>
+      </li>
+      <li>
+        <button
+          className="dropdown-item"
+          onClick={onClickTemplateForDescendantsButtonHandler}
+          type="button"
+        >
+          {t('create_page_dropdown.template.decendants')}
+        </button>
+      </li>
+    </ul>
+  );
+});
+DropendMenu.displayName = 'DropendMenu';

+ 60 - 0
apps/app/src/components/Sidebar/PageCreateButton/DropendToggle.module.scss

@@ -0,0 +1,60 @@
+@use '~/styles/variables' as var;
+
+@use '../button-styles';
+
+.btn-toggle :global {
+  @extend %btn-basis;
+
+  left: 12px;
+  padding: 0;
+
+  .icon {
+    top: 50%;
+    right: 0px;
+    font-size: 22px;
+    transform: translateY(-50%);
+  }
+}
+
+// no caret
+.btn-toggle {
+  &:global {
+    // no caret
+    &::after {
+      display: none !important;
+    }
+  }
+}
+
+// hitarea
+.btn-toggle :global {
+  .hitarea {
+    top: 0;
+    right: -10px;
+    bottom: 0;
+    left: 0;
+  }
+}
+
+// == Colors
+.btn-toggle {
+  background-color: transparent !important;
+}
+
+.btn-toggle :global {
+  svg {
+    fill: var(--grw-primary-400);
+  }
+}
+
+.btn-toggle:hover :global {
+  svg {
+    fill: var(--grw-primary-400);
+  }
+}
+
+.btn-toggle:active :global {
+  svg {
+    fill: var(--grw-primary-600);
+  }
+}

+ 24 - 0
apps/app/src/components/Sidebar/PageCreateButton/DropendToggle.tsx

@@ -0,0 +1,24 @@
+import type { ButtonHTMLAttributes, DetailedHTMLProps } from 'react';
+
+import { Hexagon } from './Hexagon';
+
+import styles from './DropendToggle.module.scss';
+
+const moduleClass = styles['btn-toggle'];
+
+
+type Props = DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
+
+export const DropendToggle = (props: Props): JSX.Element => {
+  return (
+    <button
+      type="button"
+      {...props}
+      className={`${moduleClass} btn btn-primary ${props.className ?? ''}`}
+    >
+      <Hexagon />
+      <div className="hitarea position-absolute" />
+      <span className="icon material-symbols-outlined position-absolute">chevron_right</span>
+    </button>
+  );
+};

+ 18 - 0
apps/app/src/components/Sidebar/PageCreateButton/Hexagon.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+
+type Props = {
+  className?: string,
+}
+
+export const Hexagon = React.memo((props: Props): JSX.Element => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 27.691 23.999"
+    height="36px"
+    className={props.className}
+  >
+    <g className="background" transform="translate(0 0)">
+      <path d="M20.768,0l6.923,12L20.768,24H6.923L0,12,6.923,0Z" transform="translate(0)"></path>
+    </g>
+  </svg>
+));

+ 2 - 0
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.module.scss

@@ -0,0 +1,2 @@
+.grw-page-create-button :global {
+}

+ 205 - 0
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -0,0 +1,205 @@
+import React, { useCallback, useState } from 'react';
+
+import { pagePathUtils } from '@growi/core/dist/utils';
+import { format } from 'date-fns';
+import { useRouter } from 'next/router';
+
+import { createPage, exist } from '~/client/services/page-operation';
+import { toastError } from '~/client/util/toastr';
+import { useCurrentUser } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
+
+import { DropendMenu } from './DropendMenu';
+import { CreateButton } from './CreateButton';
+import { DropendToggle } from './DropendToggle';
+
+const logger = loggerFactory('growi:cli:PageCreateButton');
+
+export const PageCreateButton = React.memo((): JSX.Element => {
+  const router = useRouter();
+  const { data: currentPage, isLoading } = useSWRxCurrentPage();
+  const { data: currentUser } = useCurrentUser();
+
+  const [isHovered, setIsHovered] = useState(false);
+  const [isCreating, setIsCreating] = useState(false);
+
+  const now = format(new Date(), 'yyyy/MM/dd');
+  const userHomepagePath = pagePathUtils.userHomepagePath(currentUser);
+  const todaysPath = `${userHomepagePath}/memo/${now}`;
+
+  const onMouseEnterHandler = () => {
+    setIsHovered(true);
+  };
+
+  const onMouseLeaveHandler = () => {
+    setIsHovered(false);
+  };
+
+  const onClickCreateNewPageButtonHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsCreating(true);
+
+      const parentPath = currentPage == null
+        ? '/'
+        : currentPage.path;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: currentPage?.grant || 1,
+        pageTags: [],
+        grantUserGroupId: currentPage?.grantedGroup?._id,
+        shouldGeneratePath: true,
+      };
+
+      const response = await createPage(parentPath, '', params);
+
+      router.push(`${response.page.id}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
+
+  const onClickCreateTodaysButtonHandler = useCallback(async() => {
+    if (currentUser == null) {
+      return;
+    }
+
+    try {
+      setIsCreating(true);
+
+      // TODO: get grant, grantUserGroupId data from parent page
+      // https://redmine.weseek.co.jp/issues/133892
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: 1,
+        pageTags: [],
+      };
+
+      const res = await exist(JSON.stringify([todaysPath]));
+      if (!res.pages[todaysPath]) {
+        await createPage(todaysPath, '', params);
+      }
+
+      router.push(`${todaysPath}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentUser, router, todaysPath]);
+
+  const onClickTemplateForChildrenButtonHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsCreating(true);
+
+      const path = currentPage == null || currentPage.path === '/'
+        ? '/_template'
+        : `${currentPage.path}/_template`;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: currentPage?.grant || 1,
+        pageTags: [],
+        grantUserGroupId: currentPage?.grantedGroup?._id,
+      };
+
+      const res = await exist(JSON.stringify([path]));
+      if (!res.pages[path]) {
+        await createPage(path, '', params);
+      }
+
+      router.push(`${path}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
+
+  const onClickTemplateForDescendantsButtonHandler = useCallback(async() => {
+    if (isLoading) return;
+
+    try {
+      setIsCreating(true);
+
+      const path = currentPage == null || currentPage.path === '/'
+        ? '/__template'
+        : `${currentPage.path}/__template`;
+
+      const params = {
+        isSlackEnabled: false,
+        slackChannels: '',
+        grant: currentPage?.grant || 1,
+        pageTags: [],
+        grantUserGroupId: currentPage?.grantedGroup?._id,
+      };
+
+      const res = await exist(JSON.stringify([path]));
+      if (!res.pages[path]) {
+        await createPage(path, '', params);
+      }
+
+      router.push(`${path}#edit`);
+    }
+    catch (err) {
+      logger.warn(err);
+      toastError(err);
+    }
+    finally {
+      setIsCreating(false);
+    }
+  }, [currentPage, isLoading, router]);
+
+  // TODO: update button design
+  // https://redmine.weseek.co.jp/issues/132683
+  return (
+    <div
+      className="d-flex flex-row"
+      onMouseEnter={onMouseEnterHandler}
+      onMouseLeave={onMouseLeaveHandler}
+    >
+      <div className="btn-group flex-grow-1">
+        <CreateButton
+          className="z-2"
+          onClick={onClickCreateNewPageButtonHandler}
+          disabled={isCreating}
+        />
+      </div>
+      { isHovered && (
+        <div className="btn-group dropend position-absolute">
+          <DropendToggle
+            className="dropdown-toggle dropdown-toggle-split"
+            data-bs-toggle="dropdown"
+            aria-expanded="false"
+          />
+          <DropendMenu
+            todaysPath={todaysPath}
+            onClickCreateNewPageButtonHandler={onClickCreateNewPageButtonHandler}
+            onClickCreateTodaysButtonHandler={onClickCreateTodaysButtonHandler}
+            onClickTemplateForChildrenButtonHandler={onClickTemplateForChildrenButtonHandler}
+            onClickTemplateForDescendantsButtonHandler={onClickTemplateForDescendantsButtonHandler}
+          />
+        </div>
+      )}
+    </div>
+  );
+});

+ 1 - 0
apps/app/src/components/Sidebar/PageCreateButton/index.ts

@@ -0,0 +1 @@
+export * from './PageCreateButton';

+ 2 - 5
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -5,7 +5,6 @@ import React, {
 
 import dynamic from 'next/dynamic';
 
-import { scheduleToPut } from '~/client/services/user-ui-settings';
 import { SidebarMode } from '~/interfaces/ui';
 import {
   useDrawerOpened,
@@ -42,8 +41,8 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
 
   const { isDrawerMode, isCollapsedMode, isDockMode } = useSidebarMode();
   const { mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { data: currentProductNavWidth, mutate: mutateProductNavWidth } = useCurrentProductNavWidth();
-  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { data: currentProductNavWidth, mutateAndSave: mutateProductNavWidth } = useCurrentProductNavWidth();
+  const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
   const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
   const [resizableAreaWidth, setResizableAreaWidth] = useState<number|undefined>(undefined);
@@ -54,13 +53,11 @@ const ResizableContainer = memo((props: ResizableContainerProps): JSX.Element =>
 
   const resizeDoneHandler = useCallback((newWidth: number) => {
     mutateProductNavWidth(newWidth, false);
-    scheduleToPut({ preferCollapsedModeByUser: false, currentProductNavWidth: newWidth });
   }, [mutateProductNavWidth]);
 
   const collapsedByResizableAreaHandler = useCallback(() => {
     mutatePreferCollapsedMode(true);
     mutateCollapsedContentsOpened(false);
-    scheduleToPut({ preferCollapsedModeByUser: true });
   }, [mutateCollapsedContentsOpened, mutatePreferCollapsedMode]);
 
 

+ 1 - 1
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.module.scss

@@ -5,7 +5,7 @@
 @use '../button-styles';
 
 .btn-toggle-collapse :global {
-  @extend %btn-primary-basis;
+  @extend %btn-basis;
 
   $height: var.$grw-sidebar-nav-width; // declare $height with the same value as the sidebar nav width
   height: $height;

+ 1 - 1
apps/app/src/components/Sidebar/SidebarHead/ToggleCollapseButton.tsx

@@ -12,7 +12,7 @@ export const ToggleCollapseButton = memo((): JSX.Element => {
 
   const { isDrawerMode, isCollapsedMode } = useSidebarMode();
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
-  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { mutateAndSave: mutatePreferCollapsedMode } = usePreferCollapsedMode();
   const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
   const toggleDrawer = useCallback(() => {

+ 3 - 6
apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.module.scss

@@ -1,15 +1,12 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
-@use '~/styles/variables' as var;
 @use '../button-styles';
 
-@use './variables' as sidebarNavVar;
+@use '../variables' as sidebarVar;
 
 .grw-primary-items :global {
   .btn {
-    @extend %btn-primary-basis;
-
-    height: sidebarNavVar.$grw-sidebar-primary-button-height;
+    @extend %btn-basis;
 
     i {
       opacity: 0.7;
@@ -24,7 +21,7 @@
 
 // Add indicator
 .grw-primary-items :global {
-  $btn-height: sidebarNavVar.$grw-sidebar-primary-button-height;
+  $btn-height: sidebarVar.$grw-sidebar-button-height;
   $btn-active-indicator-height: 34px;
 
   .btn {

+ 2 - 4
apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx

@@ -2,7 +2,6 @@ import { FC, memo, useCallback } from 'react';
 
 import dynamic from 'next/dynamic';
 
-import { scheduleToPut } from '~/client/services/user-ui-settings';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import { useCollapsedContentsOpened, useCurrentSidebarContents, useSidebarMode } from '~/stores/ui';
 
@@ -41,13 +40,12 @@ const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
     onHover,
   } = props;
 
-  const { data: currentContents, mutate: mutateContents } = useCurrentSidebarContents();
+  const { data: currentContents, mutateAndSave: mutateContents } = useCurrentSidebarContents();
 
   const indicatorClass = useIndicator(sidebarMode, contents === currentContents);
 
   const selectThisItem = useCallback(() => {
     mutateContents(contents, false);
-    scheduleToPut({ currentSidebarContents: contents });
   }, [contents, mutateContents]);
 
   const itemClickedHandler = useCallback(() => {
@@ -76,7 +74,7 @@ const PrimaryItem: FC<PrimaryItemProps> = (props: PrimaryItemProps) => {
     <button
       type="button"
       data-testid={`grw-sidebar-nav-primary-${labelForTestId}`}
-      className={`d-block btn btn-primary ${indicatorClass}`}
+      className={`btn btn-primary ${indicatorClass}`}
       onClick={itemClickedHandler}
       onMouseEnter={mouseEnteredHandler}
     >

+ 2 - 5
apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.module.scss

@@ -2,15 +2,12 @@
 
 @use '../button-styles';
 
-@use './variables' as sidebarNavVar;
 
 .grw-secondary-items :global {
   .btn {
-    @extend %btn-primary-basis;
+    @extend %btn-basis;
 
-    height: sidebarNavVar.$grw-sidebar-primary-button-height;
-
-    i {
+    span {
       opacity: 0.6;
 
       &:hover,

+ 2 - 2
apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.tsx

@@ -29,11 +29,11 @@ const SecondaryItem: FC<SecondaryItemProps> = (props: SecondaryItemProps) => {
   return (
     <Link
       href={href}
-      className="d-block btn btn-primary"
+      className="d-block btn btn-primary d-flex align-items-center justify-content-center"
       target={`${isBlank ? '_blank' : ''}`}
       prefetch={false}
     >
-      <i className="material-symbols-outlined">{iconName}</i>
+      <span className="material-symbols-outlined">{iconName}</span>
     </Link>
   );
 };

+ 2 - 2
apps/app/src/components/Sidebar/SidebarNav/SkeletonItem.module.scss

@@ -1,9 +1,9 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 
-@use './variables' as sidebarNavVar;
+@use '../variables' as sidebarVar;
 
 .grw-skeleton-item :global {
-  height: sidebarNavVar.$grw-sidebar-primary-button-height;
+  height: sidebarVar.$grw-sidebar-button-height;
   padding: .75rem;
 
   .grw-skeleton {

+ 0 - 1
apps/app/src/components/Sidebar/SidebarNav/_variables.scss

@@ -1 +0,0 @@
-$grw-sidebar-primary-button-height: 50px;

+ 10 - 3
apps/app/src/components/Sidebar/_button-styles.scss

@@ -1,8 +1,15 @@
 @use '~/styles/variables' as var;
 
-%btn-primary-basis {
-  padding-top: .75rem;
-  padding-bottom: .75rem;
+@use './variables' as sidebarVar;
+
+
+%btn-basis {
+  --bs-btn-padding-x: 0;
+  --bs-btn-padding-y: 0;
+
+  width: var.$grw-sidebar-nav-width;
+  height: sidebarVar.$grw-sidebar-button-height;
+
   line-height: 1em;
   border: 0;
   border-radius: 0;

+ 1 - 0
apps/app/src/components/Sidebar/_variables.scss

@@ -0,0 +1 @@
+$grw-sidebar-button-height: 50px;

+ 1 - 1
apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js

@@ -4,7 +4,7 @@ import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:remove-basic-auth-related-config');
+const logger = loggerFactory('growi:migrate:remove-basic-auth-related-config');
 
 const mongoose = require('mongoose');
 

+ 1 - 1
apps/app/src/migrations/20230213090921-remove-presentation-configurations.js

@@ -4,7 +4,7 @@ import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:remove-presentation-configurations');
+const logger = loggerFactory('growi:migrate:remove-presentation-configurations');
 
 const mongoose = require('mongoose');
 

+ 1 - 1
apps/app/src/migrations/20230731075753-add_installed_date_to_config.js

@@ -4,7 +4,7 @@ import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoos
 import loggerFactory from '~/utils/logger';
 
 
-const logger = loggerFactory('growi:migration:add-installed-date-to-config');
+const logger = loggerFactory('growi:migrate:add-installed-date-to-config');
 
 const mongoose = require('mongoose');
 

+ 34 - 0
apps/app/src/migrations/20231102012742-clean-user-ui-settings-collection.js

@@ -0,0 +1,34 @@
+// eslint-disable-next-line import/no-named-as-default
+import UserUISettings from '~/server/models/user-ui-settings';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:migrate:clean-user-ui-settings-collection');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    await UserUISettings.updateMany(
+      {},
+      {
+        $unset: {
+          isSidebarCollapsed: '',
+          preferDrawerModeByUser: '',
+          preferDrawerModeOnEditByUser: '',
+        },
+      },
+      { strict: false },
+    );
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    // No rollback
+  },
+};

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

@@ -20,7 +20,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
-import { useLayoutFluidClassNameByPage, useEditorModeClassName } from '~/client/services/layout';
+import { useEditorModeClassName, useLayoutFluidClassNameByPage } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript'; import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
@@ -344,21 +344,21 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
+
+const BasicLayoutWithEditor = ({ children }: { children?: ReactNode }): JSX.Element => {
+  const editorModeClassName = useEditorModeClassName();
+  return <BasicLayout className={editorModeClassName}>{children}</BasicLayout>;
+};
+
 type LayoutProps = Props & {
   children?: ReactNode
 }
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
-  const className = useEditorModeClassName();
-
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
-  return (
-    <BasicLayout className={className}>
-      {children}
-    </BasicLayout>
-  );
+  return <BasicLayoutWithEditor>{children}</BasicLayoutWithEditor>;
 };
 
 Page.getLayout = function getLayout(page: React.ReactElement<Props>) {

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

@@ -1,7 +1,7 @@
-import React, { useMemo } from 'react';
+import React, { type ReactNode, useMemo } from 'react';
 
 import type { IUserHasId } from '@growi/core';
-import {
+import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { useTranslation } from 'next-i18next';
@@ -135,12 +135,26 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
-MePage.getLayout = function getLayout(page) {
+
+type LayoutProps = Props & {
+  children?: ReactNode
+}
+
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
+
   return (
-    <BasicLayout>{page}</BasicLayout>
+    <BasicLayout>
+      {children}
+    </BasicLayout>
   );
 };
 
+MePage.getLayout = function getLayout(page) {
+  return <Layout {...page.props}>{page}</Layout>;
+};
+
 async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;

+ 2 - 4
apps/app/src/server/routes/apiv3/pages.js

@@ -240,11 +240,9 @@ module.exports = (crowi) => {
   }
 
   async function generateUniquePath(basePath, index = 1) {
-    const Page = mongoose.model('Page');
     const path = basePath + index;
-    const response = await Page.findByPath(path);
-    const isPathExists = response != null;
-    if (isPathExists) {
+    const existingPageId = await Page.exists({ path, isEmpty: false });
+    if (existingPageId != null) {
       return generateUniquePath(basePath, index + 1);
     }
     return path;

+ 47 - 14
apps/app/src/stores/ui.tsx

@@ -3,18 +3,19 @@ import {
 } from 'react';
 
 import { PageGrant, type Nullable } from '@growi/core';
-import { type SWRResponseWithUtils, useSWRStatic } from '@growi/core/dist/swr';
+import { type SWRResponseWithUtils, useSWRStatic, withUtils } from '@growi/core/dist/swr';
 import { pagePathUtils, isClient, isServer } from '@growi/core/dist/utils';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
 import { addBreakpointListener, cleanupBreakpointListener } from '@growi/ui/dist/utils';
 import type { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import {
-  useSWRConfig, type SWRResponse, type Key,
+  useSWRConfig, type SWRResponse, type Key, KeyedMutator, MutatorOptions,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import type { IFocusable } from '~/client/interfaces/focusable';
+import { scheduleToPut } from '~/client/services/user-ui-settings';
 import type { IPageGrantData } from '~/interfaces/page';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
@@ -95,8 +96,6 @@ export const EditorModeHash = {
 } as const;
 export type EditorModeHash = typeof EditorModeHash[keyof typeof EditorModeHash];
 
-export const isEditorModeHash = (hash: string): hash is EditorModeHash => Object.values<string>(EditorModeHash).includes(hash);
-
 const updateHashByEditorMode = (newEditorMode: EditorMode) => {
   const { pathname, search } = window.location;
 
@@ -251,26 +250,60 @@ export const useIsDeviceLargerThanXl = (): SWRResponse<boolean, Error> => {
 };
 
 
-export const useCurrentSidebarContents = (initialData?: SidebarContentsType): SWRResponse<SidebarContentsType, Error> => {
-  return useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
-};
+type MutateAndSaveUserUISettings<Data> = (data: Data, opts?: boolean | MutatorOptions<Data>) => Promise<Data | undefined>;
+type MutateAndSaveUserUISettingsUtils<Data> = {
+  mutateAndSave: MutateAndSaveUserUISettings<Data>;
+}
+
+export const useCurrentSidebarContents = (
+    initialData?: SidebarContentsType,
+): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<SidebarContentsType>, SidebarContentsType> => {
+  const swrResponse = useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
+
+  const { mutate } = swrResponse;
 
-export const useCurrentProductNavWidth = (initialData?: number): SWRResponse<number, Error> => {
-  return useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
+  const mutateAndSave: MutateAndSaveUserUISettings<SidebarContentsType> = useCallback((data, opts?) => {
+    scheduleToPut({ currentSidebarContents: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
-  return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
+export const useCurrentProductNavWidth = (initialData?: number): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<number>, number> => {
+  const swrResponse = useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
+
+  const { mutate } = swrResponse;
+
+  const mutateAndSave: MutateAndSaveUserUISettings<number> = useCallback((data, opts?) => {
+    scheduleToPut({ currentProductNavWidth: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const usePreferCollapsedMode = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+export const usePreferCollapsedMode = (initialData?: boolean): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<boolean>, boolean> => {
+  const swrResponse = useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+
+  const { mutate } = swrResponse;
+
+  const mutateAndSave: MutateAndSaveUserUISettings<boolean> = useCallback((data, opts?) => {
+    scheduleToPut({ preferCollapsedModeByUser: data });
+    return mutate(data, opts);
+  }, [mutate]);
+
+  return withUtils(swrResponse, { mutateAndSave });
 };
 
-export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean, Error> => {
+export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean> => {
   return useSWRStatic('isCollapsedContentsOpened', initialData, { fallbackData: false });
 };
 
+export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
+  return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
+};
+
 type DetectSidebarModeUtils = {
   isDrawerMode(): boolean
   isCollapsedMode(): boolean

+ 2 - 1
apps/app/src/styles/_editor.scss

@@ -1,4 +1,5 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
+
 @use './variables' as var;
 
 @import './organisms/wiki-custom-sidebar';
@@ -27,7 +28,7 @@
    *****************/
   .dynamic-layout-root {
     width: calc(100vw - var.$grw-sidebar-nav-width);
-    height: 100vh;
+    @extend .flex-expand-vh-100;
   }
 
 

+ 1 - 11
apps/app/src/styles/_layout.scss

@@ -1,19 +1,9 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
-@use '@growi/core/scss/flex-expand';
 
 @use './variables' as var;
 
-.flex-expand-horiz {
-  @extend %flex-expand-horiz;
-}
-
-.flex-expand-vert {
-  @extend %flex-expand-vert;
-}
-
 .dynamic-layout-root {
-  @extend %flex-expand-vert;
-  overflow-y: unset;
+  @extend .flex-expand-vert;
 }
 
 .dynamic-layout-root.growi-layout-fluid .grw-container-convertible {

+ 60 - 0
apps/app/turbo.json

@@ -0,0 +1,60 @@
+{
+  "$schema": "https://turbo.build/schema.json",
+  "extends": ["//"],
+  "pipeline": {
+
+    "styles-prebuilt": {
+      "outputs": ["src/styles/prebuilt/**"],
+      "inputs": [
+        "src/styles/**/*.scss",
+        "../../packages/core/scss/**"
+      ],
+      "outputMode": "new-only"
+    },
+    "build": {
+      "dependsOn": ["^build", "styles-prebuilt"],
+      "outputs": [".next/**", "!.next/cache/**", "dist/**"],
+      "outputMode": "new-only"
+    },
+
+    "dev:migrate": {
+      "dependsOn": ["@growi/core#dev"],
+      "outputs": ["tmp/cache/migration-status.out"],
+      "inputs": ["src/migrations/*.js"],
+      "outputMode": "new-only"
+    },
+    "dev:styles-prebuilt": {
+      "outputs": ["src/styles/prebuilt/**"],
+      "inputs": [
+        "src/styles/**/*.scss",
+        "!src/styles/prebuilt/**",
+        "../../packages/core/scss/**"
+      ],
+      "outputMode": "new-only"
+    },
+    "dev": {
+      "dependsOn": ["^dev", "dev:migrate", "dev:styles-prebuilt"],
+      "cache": false,
+      "persistent": true
+    },
+    "dev:ci": {
+      "dependsOn": ["^dev", "dev:migrate", "dev:styles-prebuilt"],
+      "cache": false
+    },
+
+    "lint": {
+      "dependsOn": ["^dev", "dev:styles-prebuilt"]
+    },
+
+    "test": {
+      "dependsOn": ["^dev"],
+      "outputMode": "new-only"
+    },
+
+    "version": {
+      "cache": false,
+      "dependsOn": ["^version", "//#version"]
+    }
+
+  }
+}

+ 17 - 4
packages/core/scss/_flex-expand.scss

@@ -1,9 +1,22 @@
-@use './placeholders/flex-expand';
-
 .flex-expand-horiz {
-  @extend %flex-expand-horiz;
+  display: flex;
+  flex-direction: row;
+  flex-grow: 1;
+  height: 100%;
 }
 
 .flex-expand-vert {
-  @extend %flex-expand-vert;
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  height: 100%;
+}
+
+.flex-expand-vh-100 {
+  height: 100vh;
+
+  .flex-expand-horiz,
+  .flex-expand-vert {
+    overflow-y: auto;
+  }
 }

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

@@ -113,6 +113,7 @@ $font-family-base: $font-family-sans-serif;
 // $modal-content-border-radius: $border-radius-lg;
 // $modal-header-padding-y: 0.75rem;
 // $modal-header-padding-x: 1rem;
+$modal-footer-border-width: 0;
 
 //== Alerts
 // $alert-bg-level: -2;

+ 0 - 17
packages/core/scss/placeholders/_flex-expand.scss

@@ -1,17 +0,0 @@
-// ref: https://discuss.codemirror.net/t/how-to-fit-the-codemirror-6-widget-into-a-flex-div/4207/4
-%flex-expand-horiz {
-  display: flex;
-  flex-direction: row;
-  flex-grow: 1;
-  height: 100%;
-  overflow-y: auto;
-}
-
-// ref: https://discuss.codemirror.net/t/how-to-fit-the-codemirror-6-widget-into-a-flex-div/4207/4
-%flex-expand-vert {
-  display: flex;
-  flex: 1;
-  flex-direction: column;
-  height: 100%;
-  overflow-y: auto;
-}

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

@@ -49,7 +49,7 @@ export const Playground = (): JSX.Element => {
   }, [codeMirrorEditor]);
 
   return (
-    <div className="d-flex flex-column vw-100 vh-100">
+    <div className="d-flex flex-column vw-100 flex-expand-vh-100">
       <div className="flex-expand-vert justify-content-center align-items-center bg-dark" style={{ minHeight: '83px' }}>
         <div className="text-white">GrowiSubNavigation</div>
       </div>

+ 1 - 0
packages/editor/turbo.json

@@ -1,4 +1,5 @@
 {
+  "$schema": "https://turbo.build/schema.json",
   "extends": ["//"],
   "pipeline": {
     "build": {

+ 2 - 0
packages/ui/scss/atoms/_btn-muted.scss

@@ -12,6 +12,8 @@
   --bs-btn-active-color: #{$color-active};
   --bs-btn-active-bg: transparent;
 
+  --bs-btn-border-width: 0;
+
   &:hover {
     --bs-btn-active-bg: rgba(#{$color-active-rgb}, 0.2);
   }

+ 1 - 47
turbo.json

@@ -39,19 +39,6 @@
       "outputs": ["dist/**"],
       "outputMode": "new-only"
     },
-    "@growi/app#styles-prebuilt": {
-      "outputs": ["src/styles/prebuilt/**"],
-      "inputs": [
-        "src/styles/**/*.scss",
-        "../../packages/core/scss/**/*.scss"
-      ],
-      "outputMode": "new-only"
-    },
-    "@growi/app#build": {
-      "dependsOn": ["^build", "@growi/app#styles-prebuilt"],
-      "outputs": [".next/**", "!.next/cache/**", "dist/**"],
-      "outputMode": "new-only"
-    },
     "@growi/slackbot-proxy#build": {
       "dependsOn": ["@growi/slack#build"],
       "outputs": ["dist/**"],
@@ -89,29 +76,6 @@
       "outputs": ["dist/**"],
       "outputMode": "new-only"
     },
-    "@growi/app#dev:migrate": {
-      "dependsOn": ["@growi/core#dev"],
-      "outputs": ["tmp/cache/migration-status.out"],
-      "inputs": ["src/migrations/*.js"],
-      "outputMode": "new-only"
-    },
-    "@growi/app#dev:styles-prebuilt": {
-      "outputs": ["src/styles/prebuilt/**"],
-      "inputs": [
-        "src/styles/**/*.scss",
-        "!src/styles/prebuilt/**"
-      ],
-      "outputMode": "new-only"
-    },
-    "@growi/app#dev": {
-      "dependsOn": ["^dev", "@growi/app#dev:migrate", "@growi/app#dev:styles-prebuilt"],
-      "cache": false,
-      "persistent": true
-    },
-    "@growi/app#dev:ci": {
-      "dependsOn": ["^dev", "@growi/app#dev:migrate", "@growi/app#dev:styles-prebuilt"],
-      "cache": false
-    },
     "@growi/slackbot-proxy#dev": {
       "dependsOn": ["@growi/slack#dev"],
       "cache": false,
@@ -157,19 +121,12 @@
     "@growi/ui#lint": {
       "dependsOn": ["@growi/core#dev"]
     },
-    "@growi/app#lint": {
-      "dependsOn": ["^dev", "@growi/app#dev:styles-prebuilt"]
-    },
     "@growi/slackbot-proxy#lint": {
       "dependsOn": ["@growi/slack#dev"]
     },
     "lint": {
     },
 
-    "@growi/app#test": {
-      "dependsOn": ["^dev"],
-      "outputMode": "new-only"
-    },
     "@growi/slackbot-proxy#test": {
       "dependsOn": ["@growi/slack#dev"],
       "outputMode": "new-only"
@@ -189,10 +146,7 @@
     "test": {
       "outputMode": "new-only"
     },
-    "@growi/app#version": {
-      "cache": false,
-      "dependsOn": ["^version", "//#version"]
-    },
+
     "version": {
       "cache": false
     },

+ 5 - 2
yarn.lock

@@ -1916,7 +1916,6 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.22.5"
 
-
 "@babel/plugin-transform-react-jsx-source@^7.22.5":
   version "7.22.5"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.22.5.tgz#49af1615bfdf6ed9d3e9e43e425e0b2b65d15b6c"
@@ -1932,7 +1931,6 @@
     core-js-pure "^3.20.2"
     regenerator-runtime "^0.13.4"
 
-
 "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.6", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
   version "7.22.10"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682"
@@ -4359,6 +4357,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/throttle-debounce@^5.0.1":
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-5.0.1.tgz#8ce917e41580b2cf16f8ee840e227947f4152b04"
+  integrity sha512-/fifasjlhpz/r4YsH0r0ZXJvivXFB3F6bmezMnqgsn/NK/fYJn7vN84k7eYn/oALu/aenXo+t8Pv+QlkS6iYBg==
+
 "@types/unist@*", "@types/unist@^2.0.0":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"