2
0
Эх сурвалжийг харах

Merge branch 'master' into fix/admin-screen-styles

Yuki Takei 1 жил өмнө
parent
commit
1d5650eb11
100 өөрчлөгдсөн 806 нэмэгдсэн , 246 устгасан
  1. 2 1
      .devcontainer/devcontainer.json
  2. 1 0
      .github/dependabot.yml
  3. 3 3
      .github/workflows/ci-app.yml
  4. 3 3
      .github/workflows/ci-slackbot-proxy.yml
  5. 6 3
      .mergify.yml
  6. 2 13
      .vscode/launch.json
  7. 1 1
      apps/app/package.json
  8. 1 1
      apps/app/public/static/locales/zh_CN/translation.json
  9. 2 2
      apps/app/src/client/services/layout.ts
  10. 3 2
      apps/app/src/components/Admin/AuditLog/ActivityTable.tsx
  11. 1 1
      apps/app/src/components/Admin/AuditLogManagement.tsx
  12. 1 1
      apps/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  13. 4 5
      apps/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  14. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  15. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  16. 1 1
      apps/app/src/components/Admin/Users/ExternalAccountTable.tsx
  17. 1 1
      apps/app/src/components/Admin/Users/UserTable.tsx
  18. 18 7
      apps/app/src/components/AuthorInfo/AuthorInfo.tsx
  19. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  20. 1 1
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  21. 27 6
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx
  22. 5 0
      apps/app/src/components/Common/CopyDropdown/CopyDropdown.module.scss
  23. 5 5
      apps/app/src/components/ContentLinkButtons.tsx
  24. 3 2
      apps/app/src/components/Layout/BasicLayout.tsx
  25. 1 1
      apps/app/src/components/Me/ExternalAccountRow.jsx
  26. 1 1
      apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkList.tsx
  27. 1 1
      apps/app/src/components/PageAlert/TrashPageAlert.tsx
  28. 1 1
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  29. 2 2
      apps/app/src/components/PageAuthorInfo/PageAuthorInfo.tsx
  30. 1 2
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  31. 5 7
      apps/app/src/components/PageContentFooter.tsx
  32. 5 4
      apps/app/src/components/PageControls/PageControls.tsx
  33. 1 1
      apps/app/src/components/PageCreateModal.tsx
  34. 1 1
      apps/app/src/components/PageEditor/ConflictDiffModal.tsx
  35. 2 2
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx
  36. 5 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss
  37. 4 3
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  38. 12 0
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  39. 2 15
      apps/app/src/components/PageEditor/PageEditor.tsx
  40. 1 1
      apps/app/src/components/PageHeader/PageHeader.tsx
  41. 8 6
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  42. 1 1
      apps/app/src/components/PageList/PageListItemL.tsx
  43. 0 1
      apps/app/src/components/PageSideContents/PageAccessoriesControl.module.scss
  44. 3 3
      apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx
  45. 1 1
      apps/app/src/components/PageTags/TagsInput.tsx
  46. 4 6
      apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx
  47. 1 1
      apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx
  48. 1 0
      apps/app/src/components/Sidebar/Sidebar.module.scss
  49. 4 5
      apps/app/src/components/TagList.tsx
  50. 1 1
      apps/app/src/components/TemplateModal/use-formatter.tsx
  51. 1 1
      apps/app/src/components/TreeItem/SimpleItem.tsx
  52. 1 1
      apps/app/src/components/User/UserDate.jsx
  53. 5 4
      apps/app/src/components/User/Username.tsx
  54. 12 4
      apps/app/src/features/growi-plugin/server/services/growi-plugin/growi-plugin.ts
  55. 1 1
      apps/app/src/server/.node-dev.json
  56. 2 3
      apps/app/src/server/crowi/express-init.js
  57. 1 1
      apps/app/src/server/models/attachment.ts
  58. 1 1
      apps/app/src/server/models/obsolete-page.js
  59. 1 1
      apps/app/src/server/models/page-operation.ts
  60. 3 2
      apps/app/src/server/models/password-reset-order.ts
  61. 3 2
      apps/app/src/server/models/user-registration-order.ts
  62. 9 3
      apps/app/src/server/routes/apiv3/page-listing.ts
  63. 4 2
      apps/app/src/server/routes/apiv3/page/check-page-existence.ts
  64. 4 3
      apps/app/src/server/routes/apiv3/page/update-page.ts
  65. 1 1
      apps/app/src/server/service/config-loader.ts
  66. 2 2
      apps/app/src/server/service/config-manager.spec.ts
  67. 3 2
      apps/app/src/server/service/config-manager.ts
  68. 5 3
      apps/app/src/server/service/customize.ts
  69. 7 6
      apps/app/src/server/service/in-app-notification.ts
  70. 1 1
      apps/app/src/server/service/installer.ts
  71. 2 4
      apps/app/src/server/service/mail.ts
  72. 14 4
      apps/app/src/server/service/page/index.ts
  73. 4 4
      apps/app/src/stores/remote-latest-page.ts
  74. 3 1
      apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts
  75. 1 1
      apps/app/test/integration/service/v5.page.test.ts
  76. 4 3
      apps/slackbot-proxy/package.json
  77. 1 1
      apps/slackbot-proxy/src/controllers/growi-to-slack.ts
  78. 1 1
      apps/slackbot-proxy/src/entities/relation.ts
  79. 1 1
      apps/slackbot-proxy/src/services/RelationsService.ts
  80. 5 5
      package.json
  81. 2 2
      packages/core/src/interfaces/common.ts
  82. 4 8
      packages/core/src/interfaces/page.ts
  83. 2 1
      packages/core/src/interfaces/revision.ts
  84. 3 2
      packages/editor/src/components/CodeMirrorEditorMain.tsx
  85. 1 0
      packages/editor/src/components/playground/Playground.tsx
  86. 5 0
      packages/editor/src/services/editor-theme/.eslintrc.cjs
  87. 34 0
      packages/editor/src/services/editor-theme/eclipse.ts
  88. 3 3
      packages/editor/src/services/editor-theme/index.ts
  89. 223 0
      packages/editor/src/services/editor-theme/material.ts
  90. 231 0
      packages/editor/src/services/editor-theme/nord.ts
  91. 1 0
      packages/editor/src/services/editor-theme/original-dark.ts
  92. 3 2
      packages/editor/src/services/list-util/insert-newline-continue-markup.ts
  93. 2 2
      packages/editor/src/services/table-util/insert-new-row-to-table-markdown.ts
  94. 15 15
      packages/editor/src/stores/use-collaborative-editor-mode.ts
  95. 6 4
      packages/pluginkit/vitest.config.ts
  96. 2 1
      packages/preset-themes/src/index.ts
  97. 1 1
      packages/slack/package.json
  98. 1 1
      packages/slack/src/utils/generate-last-update-markdown.ts
  99. 1 0
      packages/ui/package.json
  100. 0 6
      packages/ui/scss/molecules/_page_list.scss

+ 2 - 1
.devcontainer/devcontainer.json

@@ -25,7 +25,8 @@
     "editorconfig.editorconfig",
     "esbenp.prettier-vscode",
     "shinnn.stylelint",
-    "stylelint.vscode-stylelint"
+    "stylelint.vscode-stylelint",
+    "vitest.explorer"
   ],
 
   // Uncomment the next line if you want start specific services in your Docker Compose config.

+ 1 - 0
.github/dependabot.yml

@@ -26,4 +26,5 @@ updates:
       - dependency-name: string-width
       - dependency-name: "@handsontable/react"
       - dependency-name: handsontable
+      - dependency-name: typeorm
 

+ 3 - 3
.github/workflows/ci-app.yml

@@ -22,7 +22,7 @@ concurrency:
 
 
 jobs:
-  lint:
+  ci-app-lint:
     runs-on: ubuntu-latest
 
     strategy:
@@ -87,7 +87,7 @@ jobs:
           key: dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
 
 
-  test:
+  ci-app-test:
     runs-on: ubuntu-latest
 
     strategy:
@@ -169,7 +169,7 @@ jobs:
           key: dist-app-7.x-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
 
 
-  launch-dev:
+  ci-app-launch-dev:
     runs-on: ubuntu-latest
 
     strategy:

+ 3 - 3
.github/workflows/ci-slackbot-proxy.yml

@@ -24,7 +24,7 @@ concurrency:
 
 jobs:
 
-  lint:
+  ci-slackbot-proxy-lint:
     runs-on: ubuntu-latest
 
     strategy:
@@ -89,7 +89,7 @@ jobs:
         key: dist-slackbot-proxy-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
 
 
-  launch-dev:
+  ci-slackbot-proxy-launch-dev:
     runs-on: ubuntu-latest
 
     strategy:
@@ -174,7 +174,7 @@ jobs:
         key: dist-slackbot-proxy-ci-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
 
 
-  launch-prod:
+  ci-slackbot-proxy-launch-prod:
     runs-on: ubuntu-latest
 
     strategy:

+ 6 - 3
.mergify.yml

@@ -3,9 +3,12 @@ pull_request_rules:
     conditions:
       - author = dependabot[bot]
       - '#approved-reviews-by >= 1'
-      - check-success = "lint (20.x)"
-      - check-success = "test (20.x)"
-      - check-success = "launch-dev (20.x)"
+      - check-success = "ci-slackbot-proxy-lint (20.x)"
+      - check-success = "ci-slackbot-proxy-launch-dev (20.x)"
+      - check-success = "ci-slackbot-proxy-launch-prod (20.x)"
+      - check-success = "ci-app-lint (20.x)"
+      - check-success = "ci-app-test (20.x)"
+      - check-success = "ci-app-launch-dev (20.x)"
       - check-success = "test-prod-node18 / launch-prod"
       - check-success = "test-prod-node20 / launch-prod"
     actions:

+ 2 - 13
.vscode/launch.json

@@ -18,17 +18,6 @@
       },
       {
         "type": "node",
-        "request": "launch",
-        "name": "Debug: Current File with Vitest",
-        "autoAttachChildProcesses": true,
-        "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
-        "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
-        "args": ["run", "${relativeFile}"],
-        "smartStep": true,
-        "console": "integratedTerminal"
-      },
-      {
-        "type": "pwa-node",
         "request": "attach",
         "name": "Debug: Attach Debugger to Server",
         "port": 9229,
@@ -38,7 +27,7 @@
         }
       },
       {
-        "type": "pwa-node",
+        "type": "node",
         "request": "launch",
         "name": "Debug: Server",
         "cwd": "${workspaceFolder}/apps/app",
@@ -57,7 +46,7 @@
         }
       },
       {
-        "type": "pwa-chrome",
+        "type": "chrome",
         "request": "launch",
         "name": "Debug: Chrome",
         "sourceMaps": true,

+ 1 - 1
apps/app/package.json

@@ -103,7 +103,7 @@
     "cookie-parser": "^1.4.5",
     "csurf": "^1.11.0",
     "csv-to-markdown-table": "^1.4.1",
-    "date-fns": "^2.23.0",
+    "date-fns": "^3.6.0",
     "dayjs": "^1.11.7",
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",

+ 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) {

+ 3 - 2
apps/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,9 +1,10 @@
 import type { FC } from 'react';
 import React, { useState, useCallback } from 'react';
 
+import { isPopulated } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { useTranslation } from 'react-i18next';
 import { Tooltip } from 'reactstrap';
@@ -51,7 +52,7 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                       <UserPicture user={activity.user} />
                       <a
                         className="ms-2"
-                        href={pagePathUtils.userHomepagePath(activity.user)}
+                        href={isPopulated(activity.user) ? pagePathUtils.userHomepagePath(activity.user) : undefined}
                       >
                         {activity.snapshot?.username}
                       </a>

+ 1 - 1
apps/app/src/components/Admin/AuditLogManagement.tsx

@@ -2,7 +2,7 @@ import type { FC } from 'react';
 import React, { useState, useCallback, useRef } from 'react';
 
 import { LoadingSpinner } from '@growi/ui/dist/components';
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'react-i18next';
 
 import type { IClearable } from '~/client/interfaces/clearable';

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

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
 import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';

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

@@ -1,9 +1,8 @@
-import React, {
-  FC, useCallback, useEffect, useState,
-} from 'react';
+import type { FC } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 import type { IUserGroupHasId } from '@growi/core';
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
 type Props = {
@@ -117,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/UserGroupTable.tsx

@@ -2,7 +2,7 @@ import type { FC } from 'react';
 import React, { useState, useEffect } from 'react';
 
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 

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

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import { UserPicture } from '@growi/ui/dist/components';
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
 import type { IUserGroupRelationHasIdPopulatedUser } from '~/interfaces/user-group-response';

+ 1 - 1
apps/app/src/components/Admin/Users/ExternalAccountTable.tsx

@@ -1,7 +1,7 @@
 import React, { useCallback } from 'react';
 
 import type { IAdminExternalAccount } from '@growi/core';
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';

+ 1 - 1
apps/app/src/components/Admin/Users/UserTable.tsx

@@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
 
 import type { IUserHasId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';

+ 18 - 7
apps/app/src/components/AuthorInfo/AuthorInfo.tsx

@@ -1,19 +1,32 @@
 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';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 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>;
 

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -225,7 +225,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
         isDropable={isDropable}
       >
         <li
-          className="list-group-item list-group-item-action border-0 py-2 d-flex align-items-center rounded"
+          className="list-group-item list-group-item-action border-0 py-2 d-flex align-items-center rounded-1"
           onClick={loadChildFolder}
           style={{ paddingLeft }}
         >

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -148,7 +148,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
       useDragMode={isOperable}
     >
       <li
-        className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 pe-1 me-auto d-flex align-items-center rounded"
+        className="grw-bookmark-item-list list-group-item list-group-item-action border-0 py-0 pe-1 me-auto d-flex align-items-center rounded-1"
         key={bookmarkedPage._id}
         id={bookmarkItemId}
         style={{ paddingLeft }}

+ 27 - 6
apps/app/src/components/Common/CopyDropdown/CopyDropdown.jsx

@@ -16,10 +16,12 @@ import styles from './CopyDropdown.module.scss';
 const { encodeSpaces } = pagePathUtils;
 
 /* eslint-disable react/prop-types */
-const DropdownItemContents = ({ title, contents }) => (
+const DropdownItemContents = ({
+  title, contents, className, style,
+}) => (
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
-    <div className="card custom-card mb-1 p-2">{contents}</div>
+    <div className={`card custom-card mb-1 p-2 ${className}`} style={style}>{contents}</div>
   </>
 );
 /* eslint-enable react/prop-types */
@@ -110,7 +112,12 @@ export const CopyDropdown = (props) => {
 
   return (
     <>
-      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown d-print-none`} isOpen={dropdownOpen} size="sm" toggle={toggleDropdown}>
+      <Dropdown
+        className={`${styles['grw-copy-dropdown']} grw-copy-dropdown d-print-none`}
+        isOpen={dropdownOpen}
+        size="sm"
+        toggle={toggleDropdown}
+      >
         <DropdownToggle
           caret={isShareLinkMode}
           className={`btn-copy ${dropdownToggleClassName}`}
@@ -144,7 +151,11 @@ export const CopyDropdown = (props) => {
           {/* Page path */}
           <CopyToClipboard text={pagePathWithParams} onCopy={showToolTip}>
             <DropdownItem className="px-3">
-              <DropdownItemContents title={t('copy_to_clipboard.Page path')} contents={pagePathWithParams} />
+              <DropdownItemContents
+                title={t('copy_to_clipboard.Page path')}
+                contents={pagePathWithParams}
+                className="text-truncate d-block"
+              />
             </DropdownItem>
           </CopyToClipboard>
 
@@ -153,7 +164,11 @@ export const CopyDropdown = (props) => {
           {/* Page path URL */}
           <CopyToClipboard text={pagePathUrl} onCopy={showToolTip}>
             <DropdownItem className="px-3">
-              <DropdownItemContents title={t('copy_to_clipboard.Page URL')} contents={pagePathUrl} />
+              <DropdownItemContents
+                title={t('copy_to_clipboard.Page URL')}
+                contents={pagePathUrl}
+                className="text-truncate d-block"
+              />
             </DropdownItem>
           </CopyToClipboard>
           <DropdownItem divider className="my-0"></DropdownItem>
@@ -162,7 +177,11 @@ export const CopyDropdown = (props) => {
           { pageId && (
             <CopyToClipboard text={permalink} onCopy={showToolTip}>
               <DropdownItem className="px-3">
-                <DropdownItemContents title={t('copy_to_clipboard.Permanent link')} contents={permalink} />
+                <DropdownItemContents
+                  title={t('copy_to_clipboard.Permanent link')}
+                  contents={permalink}
+                  className="text-truncate d-block"
+                />
               </DropdownItem>
             </CopyToClipboard>
           )}
@@ -176,6 +195,8 @@ export const CopyDropdown = (props) => {
                 <DropdownItemContents
                   title={t('copy_to_clipboard.Page path and permanent link')}
                   contents={<>{pagePathWithParams}<br />{permalink}</>}
+                  className="text-truncate"
+                  style={{ direction: 'rtl' }}
                 />
               </DropdownItem>
             </CopyToClipboard>

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

@@ -9,6 +9,11 @@
 
   .dropdown-menu {
     min-width: 310px;
+    max-width: 375px;
+
+    @include bs.media-breakpoint-up(md) {
+      max-width: 600px;
+    }
 
     .dropdown-header {
       margin-bottom: 0.5em;

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

@@ -10,9 +10,9 @@ const BookMarkLinkButton = React.memo(() => {
     <ScrollLink to="bookmarks-list" offset={-120}>
       <button
         type="button"
-        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
+        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
       >
-        <span className="material-symbols-outlined p-0">bookmark</span>
+        <span className="material-symbols-outlined p-0 me-2">bookmark</span>
         <span>{t('user_home_page.bookmarks')}</span>
       </button>
     </ScrollLink>
@@ -27,9 +27,9 @@ const RecentlyCreatedLinkButton = React.memo(() => {
     <ScrollLink to="recently-created-list" offset={-120}>
       <button
         type="button"
-        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
+        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
       >
-        <span className="growi-custom-icons mx-1">recently_created</span>
+        <span className="growi-custom-icons mx-2 ">recently_created</span>
         <span>{t('user_home_page.recently_created')}</span>
       </button>
     </ScrollLink>
@@ -40,7 +40,7 @@ RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
 
 
 export type ContentLinkButtonsProps = {
-  author: IUserHasId | null,
+  author?: IUserHasId,
 }
 
 export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element => {

+ 3 - 2
apps/app/src/components/Layout/BasicLayout.tsx

@@ -1,4 +1,5 @@
-import React, { ReactNode } from 'react';
+import type { ReactNode } from 'react';
+import React from 'react';
 
 import dynamic from 'next/dynamic';
 import { DndProvider } from 'react-dnd';
@@ -42,7 +43,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
             <Sidebar />
           </div>
 
-          <div className="d-flex flex-grow-1 flex-column z-1">{/* neccessary for nested {children} make expanded */}
+          <div className="d-flex flex-grow-1 flex-column mw-0 z-1">{/* neccessary for nested {children} make expanded */}
             <AlertSiteUrlUndefined />
             {children}
           </div>

+ 1 - 1
apps/app/src/components/Me/ExternalAccountRow.jsx

@@ -1,7 +1,7 @@
 
 import React from 'react';
 
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 

+ 1 - 1
apps/app/src/components/PageAccessoriesModal/ShareLink/ShareLinkList.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 
 import { CopyDropdown } from '../../Common/CopyDropdown';

+ 1 - 1
apps/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -1,7 +1,7 @@
 import React, { useCallback } from 'react';
 
 import { UserPicture } from '@growi/ui/dist/components';
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 

+ 1 - 1
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>

+ 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 - 2
apps/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -2,8 +2,7 @@ import React from 'react';
 
 import { isPopulated } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
-import { format } from 'date-fns';
-import { t } from 'i18next';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,

+ 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>

+ 5 - 4
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>
   );
@@ -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

@@ -4,7 +4,7 @@ import React, {
 
 import { Origin } from '@growi/core';
 import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem,

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

@@ -7,7 +7,7 @@ import {
   MergeViewer, CodeMirrorEditorDiff, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated,
 } from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,

+ 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>

+ 2 - 15
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -273,26 +273,12 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     };
   }, [saveAndReturnToViewHandler]);
 
-
-  // TODO: https://redmine.weseek.co.jp/issues/142729
-  // https://regex101.com/r/Wg2Hh6/1
-  // initial caret line
-  useEffect(() => {
-    const untitledPageRegex = /^Untitled-\d+$/;
-    const isNewlyCreatedPage = (
-      currentPage?.wip && currentPage?.latestRevision == null && untitledPageRegex.test(nodePath.basename(currentPage?.path ?? ''))
-    ) ?? false;
-    if (!isNewlyCreatedPage) {
-      codeMirrorEditor?.setCaretLine();
-    }
-  }, [codeMirrorEditor, currentPage]);
-
   // set handler to focus
   useLayoutEffect(() => {
     if (editorMode === EditorMode.Editor) {
       codeMirrorEditor?.focus();
     }
-  }, [codeMirrorEditor, editorMode]);
+  }, [codeMirrorEditor, currentPage, editorMode]);
 
   // Detect indent size from contents (only when users are allowed to change it)
   useEffect(() => {
@@ -367,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}

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

@@ -21,7 +21,7 @@ export const PageHeader: FC = () => {
       <PagePathHeader
         currentPage={currentPage}
       />
-      <div className="mt-1">
+      <div className="mt-0 mt-md-1">
         <PageTitleHeader
           currentPage={currentPage}
         />

+ 8 - 6
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -84,11 +84,13 @@ export const PageTitleHeader: FC<Props> = (props) => {
     setRenameInputShown(true);
   }, [currentPagePath, isMovable]);
 
-  useEffect(() => {
-    if (isNewlyCreatedPage) {
-      setRenameInputShown(true);
-    }
-  }, [currentPage._id, isNewlyCreatedPage]);
+  // TODO: auto focus when create new page
+  // https://redmine.weseek.co.jp/issues/136128
+  // useEffect(() => {
+  //   if (isNewlyCreatedPage) {
+  //     setRenameInputShown(true);
+  //   }
+  // }, [currentPage._id, isNewlyCreatedPage]);
 
   return (
     <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`}>
@@ -108,7 +110,7 @@ export const PageTitleHeader: FC<Props> = (props) => {
           </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/PageList/PageListItemL.tsx

@@ -10,7 +10,7 @@ import { isIPageInfoForListing, isIPageInfoForEntity } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pathUtils } from '@growi/core/dist/utils';
 import { UserPicture, PageListMeta } from '@growi/ui/dist/components';
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Clamp from 'react-multiline-clamp';

+ 0 - 1
apps/app/src/components/PageSideContents/PageAccessoriesControl.module.scss

@@ -31,6 +31,5 @@
 @include bs.media-breakpoint-up(lg) {
   .btn-page-accessories :global {
     flex-grow: 1;
-    padding: 1px 5px 1px 10px;
   }
 }

+ 3 - 3
apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx

@@ -27,11 +27,11 @@ export const PageAccessoriesControl = memo((props: Props): JSX.Element => {
   return (
     <button
       type="button"
-      className={`btn btn-outline-neutral-secondary ${moduleClass} ${className} rounded-pill`}
+      className={`btn btn-outline-neutral-secondary ${moduleClass} ${className} rounded-pill py-1 px-lg-3`}
       onClick={onClick}
     >
-      <span className="grw-icon d-flex">{icon}</span>
-      <span className="grw-labels ms-1 d-none d-lg-flex">
+      <span className="grw-icon d-flex me-lg-2">{icon}</span>
+      <span className="grw-labels d-none d-lg-flex">
         {label}
         {/* Do not display CountBadge if '/trash/*': https://github.com/weseek/growi/pull/7600 */}
         { count != null

+ 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

+ 4 - 6
apps/app/src/components/ReactMarkdownComponents/CodeBlock.tsx

@@ -1,12 +1,11 @@
-import { ReactNode } from 'react';
+import type { ReactNode } from 'react';
 
-import type { CodeComponent } from 'react-markdown/lib/ast-to-react';
+import type { CodeComponent, CodeProps } from 'react-markdown/lib/ast-to-react';
 import { PrismAsyncLight } from 'react-syntax-highlighter';
 import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 
 import styles from './CodeBlock.module.scss';
 
-
 // remove font-family
 Object.entries<object>(oneDark).forEach(([key, value]) => {
   if ('fontFamily' in value) {
@@ -49,7 +48,7 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   const isSimpleString = Array.isArray(children) && children.length === 1 && typeof children[0] === 'string';
   if (!isSimpleString) {
     return (
-      <div className="code-highlighted" style={oneDark['pre[class*="language-"]']}>
+      <div style={oneDark['pre[class*="language-"]']}>
         <code className={`language-${lang}`} style={oneDark['code[class*="language-"]']}>
           {children}
         </code>
@@ -59,7 +58,6 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
 
   return (
     <PrismAsyncLight
-      className="code-highlighted"
       PreTag="div"
       style={oneDark}
       language={lang}
@@ -69,7 +67,7 @@ function CodeBlockSubstance({ lang, children }: { lang: string, children: ReactN
   );
 }
 
-export const CodeBlock: CodeComponent = ({ inline, className, children }) => {
+export const CodeBlock: CodeComponent = ({ inline, className, children }: CodeProps) => {
 
   if (inline) {
     return <code className={`code-inline ${className ?? ''}`}>{children}</code>;

+ 1 - 1
apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
 
 import { Origin } from '@growi/core';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import { useTranslation } from 'react-i18next';
 
 import { useCreatePageAndTransit } from '~/client/services/create-page';

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

@@ -44,6 +44,7 @@
         left: var.$grw-sidebar-nav-width;
         min-height: 50vh;
         max-height: calc(100vh - var.$grw-sidebar-nav-width * 2);
+        border-radius: 0 4px 4px 0 ;
       }
     }
   }

+ 4 - 5
apps/app/src/components/TagList.tsx

@@ -1,11 +1,10 @@
-import React, {
-  FC, useCallback,
-} from 'react';
+import type { FC } from 'react';
+import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 import { useKeywordManager } from '~/client/services/search-operation';
-import { IDataTagCount } from '~/interfaces/tag';
+import type { IDataTagCount } from '~/interfaces/tag';
 
 import PaginationWrapper from './PaginationWrapper';
 
@@ -42,7 +41,7 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
         <button
           key={tag._id}
           type="button"
-          className="list-group-item list-group-item-action d-flex justify-content-between"
+          className="list-group-item list-group-item-action d-flex justify-content-between rounded-1"
           onClick={() => pushState(`tag:${tag.name}`)}
         >
           <div className="text-truncate grw-tag badge">{tag.name}</div>

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

@@ -1,6 +1,6 @@
 import path from 'path';
 
-import dateFnsFormat from 'date-fns/format';
+import { format as dateFnsFormat } from 'date-fns/format';
 import mustache from 'mustache';
 
 import { useCurrentPagePath } from '~/stores/page';

+ 1 - 1
apps/app/src/components/TreeItem/SimpleItem.tsx

@@ -201,7 +201,7 @@ export const SimpleItem: FC<SimpleItemProps> = (props) => {
       <li
         ref={itemRef}
         role="button"
-        className={`list-group-item border-0 py-0 pr-3 d-flex align-items-center text-muted ${page.isTarget ? 'active' : 'list-group-item-action'}`}
+        className={`list-group-item border-0 py-0 pr-3 d-flex align-items-center text-muted rounded-1 ${page.isTarget ? 'active' : 'list-group-item-action'}`}
         id={page.isTarget ? 'grw-pagetree-current-page-item' : `grw-pagetree-list-${page._id}`}
         onClick={itemClickHandler}
       >

+ 1 - 1
apps/app/src/components/User/UserDate.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import { format } from 'date-fns';
+import { format } from 'date-fns/format';
 import PropTypes from 'prop-types';
 
 

+ 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)';

+ 12 - 4
apps/app/src/features/growi-plugin/server/services/growi-plugin/growi-plugin.ts

@@ -7,7 +7,7 @@ import type { GrowiPluginPackageData } from '@growi/pluginkit';
 import { importPackageJson, validateGrowiDirective } from '@growi/pluginkit/dist/v4/server/index.cjs';
 // eslint-disable-next-line no-restricted-imports
 import axios from 'axios';
-import mongoose from 'mongoose';
+import type mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 import unzipStream from 'unzip-stream';
 
@@ -28,13 +28,21 @@ const logger = loggerFactory('growi:plugins:plugin-utils');
 export type GrowiPluginResourceEntries = [installedPath: string, href: string][];
 
 function retrievePluginManifest(growiPlugin: IGrowiPlugin): ViteManifest | undefined {
-  const manifestPath = path.join(PLUGIN_STORING_PATH, growiPlugin.installedPath, 'dist/manifest.json');
+  // ref: https://vitejs.dev/guide/migration.html#manifest-files-are-now-generated-in-vite-directory-by-default
+  const manifestPathByVite4 = path.join(PLUGIN_STORING_PATH, growiPlugin.installedPath, 'dist/manifest.json');
+  const manifestPath = path.join(PLUGIN_STORING_PATH, growiPlugin.installedPath, 'dist/.vite/manifest.json');
 
-  if (!fs.existsSync(manifestPath)) {
+  const isManifestByVite4Exists = fs.existsSync(manifestPathByVite4);
+  const isManifestExists = fs.existsSync(manifestPath);
+
+  if (!isManifestByVite4Exists && !isManifestExists) {
     return;
   }
 
-  const manifestStr: string = readFileSync(manifestPath, 'utf-8');
+  const manifestStr: string = readFileSync(
+    isManifestExists ? manifestPath : manifestPathByVite4,
+    'utf-8',
+  );
   return JSON.parse(manifestStr);
 }
 

+ 1 - 1
apps/app/src/server/.node-dev.json

@@ -5,6 +5,6 @@
     "public/static",
 
     "// ignore watching preset theme updates",
-    "packages/preset-themes/dist/themes/manifest.json"
+    "packages/preset-themes/dist/themes/.vite/manifest.json"
   ]
 }

+ 2 - 3
apps/app/src/server/crowi/express-init.js

@@ -1,4 +1,4 @@
-import { manifestPath as presetThemesManifestPath } from '@growi/preset-themes';
+import { themesRootPath as presetThemesRootPath } from '@growi/preset-themes';
 import csrf from 'csurf';
 import qs from 'qs';
 
@@ -12,7 +12,6 @@ const logger = loggerFactory('growi:crowi:express-init');
 
 module.exports = function(crowi, app) {
   const debug = require('debug')('growi:crowi:express-init');
-  const path = require('path');
   const express = require('express');
   const compression = require('compression');
   const helmet = require('helmet');
@@ -86,7 +85,7 @@ module.exports = function(crowi, app) {
   const staticOption = (crowi.node_env === 'production') ? { maxAge: '30d' } : {};
   app.use(express.static(crowi.publicDir, staticOption));
   app.use('/static/preset-themes', express.static(
-    resolveFromRoot(`../../node_modules/@growi/preset-themes/${path.dirname(presetThemesManifestPath)}`),
+    resolveFromRoot(`../../node_modules/@growi/preset-themes/${presetThemesRootPath}`),
   ));
   app.use(PLUGIN_EXPRESS_STATIC_DIR, express.static(PLUGIN_STORING_PATH));
 

+ 1 - 1
apps/app/src/server/models/attachment.ts

@@ -1,7 +1,7 @@
 import path from 'path';
 
 import type { IAttachment } from '@growi/core';
-import { addSeconds } from 'date-fns';
+import { addSeconds } from 'date-fns/addSeconds';
 import {
   Schema, type Model, type Document, Types,
 } from 'mongoose';

+ 1 - 1
apps/app/src/server/models/obsolete-page.js

@@ -1,5 +1,6 @@
 import { GroupType, Origin } from '@growi/core';
 import { templateChecker, pagePathUtils, pathUtils } from '@growi/core/dist/utils';
+import { differenceInYears } from 'date-fns/differenceInYears';
 import escapeStringRegexp from 'escape-string-regexp';
 
 import { Comment } from '~/features/comment/server/models/comment';
@@ -18,7 +19,6 @@ import UserGroupRelation from './user-group-relation';
 
 const nodePath = require('path');
 
-const differenceInYears = require('date-fns/differenceInYears');
 const debug = require('debug')('growi:models:page');
 const mongoose = require('mongoose');
 const urljoin = require('url-join');

+ 1 - 1
apps/app/src/server/models/page-operation.ts

@@ -1,6 +1,6 @@
 import type { IGrantedGroup } from '@growi/core';
 import { GroupType } from '@growi/core';
-import { addSeconds } from 'date-fns';
+import { addSeconds } from 'date-fns/addSeconds';
 import type {
   Model, Document, QueryOptions, FilterQuery,
 } from 'mongoose';

+ 3 - 2
apps/app/src/server/models/password-reset-order.ts

@@ -1,8 +1,9 @@
 import crypto from 'crypto';
 
-import { addMinutes } from 'date-fns';
+import { addMinutes } from 'date-fns/addMinutes';
+import type { Model, Document } from 'mongoose';
 import mongoose, {
-  Schema, Model, Document,
+  Schema,
 } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 

+ 3 - 2
apps/app/src/server/models/user-registration-order.ts

@@ -1,8 +1,9 @@
 import crypto from 'crypto';
 
-import { addHours } from 'date-fns';
+import { addHours } from 'date-fns/addHours';
+import type { Model, Document } from 'mongoose';
 import {
-  Schema, Model, Document,
+  Schema,
 } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 

+ 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 - 2
apps/app/src/server/routes/apiv3/page/check-page-existence.ts

@@ -1,5 +1,6 @@
 import type { IPage, IUserHasId } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
+import { normalizePath } from '@growi/core/dist/utils/path-utils';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { query } from 'express-validator';
@@ -44,10 +45,11 @@ export const checkPageExistenceHandlersFactory: CreatePageHandlersFactory = (cro
       const { path } = req.query;
 
       if (path == null || Array.isArray(path)) {
-        return res.apiv3Err(new ErrorV3('The param "path" must be an page id'));
+        return res.apiv3Err(new ErrorV3('The param "path" must be a string'));
       }
 
-      const count = await Page.countByPathAndViewer(path.toString(), req.user);
+      const normalizedPath = normalizePath(path.toString());
+      const count = await Page.countByPathAndViewer(normalizedPath, req.user);
       res.apiv3({ isExist: count > 0 });
     },
   ];

+ 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,

+ 1 - 1
apps/app/src/server/service/config-loader.ts

@@ -1,5 +1,5 @@
 import { envUtils } from '@growi/core/dist/utils';
-import { parseISO } from 'date-fns';
+import { parseISO } from 'date-fns/parseISO';
 
 import { GrowiServiceType } from '~/features/questionnaire/interfaces/growi-info';
 import loggerFactory from '~/utils/logger';

+ 2 - 2
apps/app/src/server/service/config-manager.spec.ts

@@ -17,7 +17,7 @@ describe('ConfigManager test', () => {
 
   describe('updateConfigsInTheSameNamespace()', () => {
 
-    test.concurrent('invoke publishUpdateMessage()', async() => {
+    test('invoke publishUpdateMessage()', async() => {
       // setup
       ConfigModel.bulkWrite = vi.fn();
       configManager.loadConfigs = vi.fn();
@@ -33,7 +33,7 @@ describe('ConfigManager test', () => {
       expect(configManager.publishUpdateMessage).toHaveBeenCalledTimes(1);
     });
 
-    test.concurrent('does not invoke publishUpdateMessage()', async() => {
+    test('does not invoke publishUpdateMessage()', async() => {
       // setup
       ConfigModel.bulkWrite = vi.fn();
       configManager.loadConfigs = vi.fn();

+ 3 - 2
apps/app/src/server/service/config-manager.ts

@@ -1,11 +1,12 @@
-import parseISO from 'date-fns/parseISO';
+import { parseISO } from 'date-fns/parseISO';
 
 import loggerFactory from '~/utils/logger';
 
 import ConfigModel from '../models/config';
 import S2sMessage from '../models/vo/s2s-message';
 
-import ConfigLoader, { ConfigObject } from './config-loader';
+import type { ConfigObject } from './config-loader';
+import ConfigLoader from './config-loader';
 import type { S2sMessagingService } from './s2s-messaging/base';
 import type { S2sMessageHandlable } from './s2s-messaging/handlable';
 

+ 5 - 3
apps/app/src/server/service/customize.ts

@@ -1,7 +1,9 @@
-import { ColorScheme } from '@growi/core';
+import path from 'path';
+
+import type { ColorScheme } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { getForcedColorScheme } from '@growi/core/dist/utils';
-import { DefaultThemeMetadata, PresetThemesMetadatas } from '@growi/preset-themes';
+import { DefaultThemeMetadata, PresetThemesMetadatas, manifestPath } from '@growi/preset-themes';
 import uglifycss from 'uglifycss';
 
 import { growiPluginService } from '~/features/growi-plugin/server/services';
@@ -162,7 +164,7 @@ class CustomizeService implements S2sMessageHandlable {
     // retrieve preset theme
     else {
       // import preset-themes manifest
-      const presetThemesManifest = await import('@growi/preset-themes/dist/themes/manifest.json').then(imported => imported.default);
+      const presetThemesManifest = await import(path.join('@growi/preset-themes', manifestPath)).then(imported => imported.default);
 
       const themeMetadata = PresetThemesMetadatas.find(p => p.name === theme);
       this.forcedColorScheme = getForcedColorScheme(themeMetadata?.schemeType);

+ 7 - 6
apps/app/src/server/service/in-app-notification.ts

@@ -2,21 +2,22 @@ import type {
   HasObjectId, IUser, IPage,
 } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
-import { subDays } from 'date-fns';
-import { Types, FilterQuery, UpdateQuery } from 'mongoose';
+import { subDays } from 'date-fns/subDays';
+import type { Types, FilterQuery, UpdateQuery } from 'mongoose';
 
 import { AllEssentialActions } from '~/interfaces/activity';
-import { InAppNotificationStatuses, PaginateResult } from '~/interfaces/in-app-notification';
-import { ActivityDocument } from '~/server/models/activity';
+import type { PaginateResult } from '~/interfaces/in-app-notification';
+import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
+import type { ActivityDocument } from '~/server/models/activity';
+import type { InAppNotificationDocument } from '~/server/models/in-app-notification';
 import {
   InAppNotification,
-  InAppNotificationDocument,
 } from '~/server/models/in-app-notification';
 import InAppNotificationSettings from '~/server/models/in-app-notification-settings';
 import Subscription from '~/server/models/subscription';
 import loggerFactory from '~/utils/logger';
 
-import Crowi from '../crowi';
+import type Crowi from '../crowi';
 import { RoomPrefix, getRoomNameWithId } from '../util/socket-io-helpers';
 
 import { generateSnapshot } from './in-app-notification/in-app-notification-utils';

+ 1 - 1
apps/app/src/server/service/installer.ts

@@ -3,7 +3,7 @@ import path from 'path';
 import type {
   Lang, IPage, IUser,
 } from '@growi/core';
-import { addSeconds } from 'date-fns';
+import { addSeconds } from 'date-fns/addSeconds';
 import ExtensibleCustomError from 'extensible-custom-error';
 import fs from 'graceful-fs';
 import mongoose from 'mongoose';

+ 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;
     });
 

+ 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,
 }
 

+ 3 - 1
apps/app/test/cypress/e2e/50-sidebar/50-sidebar--access-to-side-bar.cy.ts

@@ -270,7 +270,9 @@ describe('Access to sidebar', () => {
 
         it('Succesfully click all tags button', () => {
           cy.getByTestid('grw-sidebar-content-tags').within(() => {
-            cy.get('.btn-primary').click();
+            cy.get('.btn-primary').as('check-all-tags-button');
+            cy.get('@check-all-tags-button').should('be.visible');
+            cy.get('@check-all-tags-button').click({force: true});
           });
           cy.collapseSidebar(true);
           cy.getByTestid('grw-tags-list').should('be.visible');

+ 1 - 1
apps/app/test/integration/service/v5.page.test.ts

@@ -1,4 +1,4 @@
-import { addSeconds } from 'date-fns';
+import { addSeconds } from 'date-fns/addSeconds';
 import mongoose from 'mongoose';
 
 import { PageActionStage, PageActionType } from '../../../src/interfaces/page-operation';

+ 4 - 3
apps/slackbot-proxy/package.json

@@ -24,7 +24,8 @@
     "version": "yarn version --no-git-tag-version --preid=slackbot-proxy"
   },
   "// comments for dependencies": {
-    "read-pkg-up": "v8 doesn't support CommonJS anymore. https://github.com/sindresorhus/read-pkg-up/issues/17"
+    "read-pkg-up": "v8 doesn't support CommonJS anymore. https://github.com/sindresorhus/read-pkg-up/issues/17",
+    "typeorm": "Upgrading to v0.3.x requires significant changes. https://github.com/tsedio/tsed/blob/production/docs/tutorials/typeorm.md"
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
@@ -41,7 +42,7 @@
     "bunyan": "^1.8.15",
     "compression": "^1.7.4",
     "cookie-parser": "^1.4.5",
-    "date-fns": "^2.23.0",
+    "date-fns": "^3.6.0",
     "express-bunyan-logger": "^1.3.3",
     "extensible-custom-error": "^0.0.7",
     "helmet": "^4.6.0",
@@ -49,7 +50,7 @@
     "method-override": "^3.0.0",
     "mysql2": "^2.2.5",
     "read-pkg-up": "^7.0.1",
-    "typeorm": "^0.2.31",
+    "typeorm": "=0.2.45",
     "universal-bunyan": "^0.9.2"
   },
   "devDependencies": {

+ 1 - 1
apps/slackbot-proxy/src/controllers/growi-to-slack.ts

@@ -7,7 +7,7 @@ import {
   Controller, Get, Post, Inject, Req, Res, UseBefore, PathParams, Put, QueryParams,
 } from '@tsed/common';
 import axios from 'axios';
-import { addHours } from 'date-fns';
+import { addHours } from 'date-fns/addHours';
 import createError from 'http-errors';
 
 

+ 1 - 1
apps/slackbot-proxy/src/entities/relation.ts

@@ -1,4 +1,4 @@
-import { differenceInMilliseconds } from 'date-fns';
+import { differenceInMilliseconds } from 'date-fns/differenceInMilliseconds';
 import {
   Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, ManyToOne, Index,
 } from 'typeorm';

+ 1 - 1
apps/slackbot-proxy/src/services/RelationsService.ts

@@ -3,7 +3,7 @@ import { getSupportedGrowiActionsRegExp } from '@growi/slack/dist/utils/get-supp
 import { permissionParser } from '@growi/slack/dist/utils/permission-parser';
 import { Inject, Service } from '@tsed/di';
 import axios from 'axios';
-import { addHours } from 'date-fns';
+import { addHours } from 'date-fns/addHours';
 
 import { Relation, PermissionSettingsInterface } from '~/entities/relation';
 import { RelationRepository } from '~/repositories/relation';

+ 5 - 5
package.json

@@ -89,11 +89,11 @@
     "ts-node-dev": "^2.0.0",
     "tsconfig-paths": "^3.9.0",
     "typescript": "~5.0.0",
-    "vite": "^4.5.3",
-    "vite-plugin-dts": "^2.3.0",
-    "vite-tsconfig-paths": "^4.2.0",
-    "vitest": "^0.34.6",
-    "vitest-mock-extended": "^1.1.3"
+    "vite": "^5.2.9",
+    "vite-plugin-dts": "^3.8.3",
+    "vite-tsconfig-paths": "^4.3.2",
+    "vitest": "^1.5.0",
+    "vitest-mock-extended": "^1.3.1"
   },
   "engines": {
     "node": "^18 || ^20",

+ 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}

+ 5 - 0
packages/editor/src/services/editor-theme/.eslintrc.cjs

@@ -0,0 +1,5 @@
+module.exports = {
+  rules: {
+    camelcase: 'off',
+  },
+};

+ 34 - 0
packages/editor/src/services/editor-theme/eclipse.ts

@@ -0,0 +1,34 @@
+// Ref: https://github.com/uiwjs/react-codemirror/blob/399d127f59a97a64974a65923d477d585d2abee5/themes/eclipse/src/index.ts
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+
+export const eclipse = createTheme({
+  theme: 'light',
+  settings: {
+    background: '#fff',
+    foreground: '#000',
+    // Change color
+    caret: '#000',
+    selection: '#d7d4f0',
+    selectionMatch: '#d7d4f0',
+    gutterBackground: '#f7f7f7',
+    gutterForeground: '#999',
+    lineHighlight: '#006fff1c',
+    gutterBorder: 'transparent',
+  },
+  styles: [
+    { tag: [t.comment], color: '#3F7F5F' },
+    { tag: [t.documentMeta], color: '#FF1717' },
+    { tag: t.keyword, color: '#7F0055', fontWeight: 'bold' },
+    { tag: t.atom, color: '#00f' },
+    { tag: t.number, color: '#164' },
+    { tag: t.propertyName, color: '#164' },
+    { tag: [t.variableName, t.definition(t.variableName)], color: '#0000C0' },
+    { tag: t.function(t.variableName), color: '#0000C0' },
+    { tag: t.string, color: '#2A00FF' },
+    { tag: t.operator, color: 'black' },
+    { tag: t.tagName, color: '#170' },
+    { tag: t.attributeName, color: '#00c' },
+    { tag: t.link, color: '#219' },
+  ],
+});

+ 3 - 3
packages/editor/src/services/editor-theme/index.ts

@@ -3,7 +3,7 @@ import { Extension } from '@codemirror/state';
 export const getEditorTheme = async(themeName?: EditorTheme): Promise<Extension> => {
   switch (themeName) {
     case 'eclipse':
-      return (await import('@uiw/codemirror-theme-eclipse')).eclipse;
+      return (await import('./eclipse')).eclipse;
     case 'basic':
       return (await import('cm6-theme-basic-light')).basicLight;
     case 'ayu':
@@ -13,9 +13,9 @@ export const getEditorTheme = async(themeName?: EditorTheme): Promise<Extension>
     case 'defaultdark':
       return (await import('./original-dark')).originalDark;
     case 'material':
-      return (await import('cm6-theme-material-dark')).materialDark;
+      return (await import('./material')).materialDark;
     case 'nord':
-      return (await import('cm6-theme-nord')).nord;
+      return (await import('./nord')).nord;
     case 'cobalt':
       return (await import('./cobalt')).cobalt;
     case 'kimbie':

+ 223 - 0
packages/editor/src/services/editor-theme/material.ts

@@ -0,0 +1,223 @@
+// Ref: https://github.com/craftzdog/cm6-themes/blob/289d9e0ca6b500f4cdf68464f4f21dd8e2dd8963/packages/material-dark/src/index.ts
+import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
+import { Extension } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
+import { tags as t } from '@lezer/highlight';
+
+// Auther: stephen-liu-fipo
+const base00 = '#2e3235';
+const base01 = '#505d64';
+const base02 = '#606f7a';
+const base03 = '#707d8b';
+// base04 = '#a0a4ae',
+const base05 = '#bdbdbd';
+const base06 = '#e0e0e0';
+const base07 = '#fdf6e3';
+const base_red = '#ff5f52';
+const base_deeporange = '#ff6e40';
+const base_pink = '#fa5788';
+const base_yellow = '#facf4e';
+const base_orange = '#ffad42';
+const base_cyan = '#56c8d8';
+const base_indigo = '#7186f0';
+const base_purple = '#cf6edf';
+const base_green = '#6abf69';
+const base_lightgreen = '#99d066';
+const base_teal = '#4ebaaa';
+
+const invalid = base_red;
+// Adjust color
+const darkBackground = '#36383a';
+// Adjust color
+const highlightBackground = '#44494d';
+const background = base00;
+const tooltipBackground = base01;
+const selection = base01;
+// Change color
+const cursor = base05;
+
+// / The editor theme styles for Material Dark.
+export const materialDarkTheme = EditorView.theme(
+  {
+    '&': {
+      color: base05,
+      backgroundColor: background,
+    },
+
+    '.cm-content': {
+      caretColor: cursor,
+    },
+
+    '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
+    '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
+      { backgroundColor: selection },
+
+    '.cm-panels': { backgroundColor: darkBackground, color: base03 },
+    '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
+    '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
+
+    '.cm-searchMatch': {
+      outline: `1px solid ${base_yellow}`,
+      backgroundColor: 'transparent',
+    },
+    '.cm-searchMatch.cm-searchMatch-selected': {
+      backgroundColor: highlightBackground,
+    },
+
+    '.cm-activeLine': { backgroundColor: highlightBackground },
+    '.cm-selectionMatch': {
+      backgroundColor: darkBackground,
+      outline: `1px solid ${base_teal}`,
+    },
+
+    '&.cm-focused .cm-matchingBracket': {
+      color: base06,
+      outline: `1px solid ${base_teal}`,
+    },
+
+    '&.cm-focused .cm-nonmatchingBracket': {
+      color: base_red,
+    },
+
+    '.cm-gutters': {
+      backgroundColor: base00,
+      borderRight: '1px solid #4f5b66',
+      color: base02,
+    },
+
+    '.cm-activeLineGutter': {
+      backgroundColor: highlightBackground,
+      color: base07,
+    },
+
+    '.cm-foldPlaceholder': {
+      backgroundColor: 'transparent',
+      border: 'none',
+      color: '#ddd',
+    },
+
+    '.cm-tooltip': {
+      border: 'none',
+      backgroundColor: tooltipBackground,
+    },
+    '.cm-tooltip .cm-tooltip-arrow:before': {
+      borderTopColor: 'transparent',
+      borderBottomColor: 'transparent',
+    },
+    '.cm-tooltip .cm-tooltip-arrow:after': {
+      borderTopColor: tooltipBackground,
+      borderBottomColor: tooltipBackground,
+    },
+    '.cm-tooltip-autocomplete': {
+      '& > ul > li[aria-selected]': {
+        backgroundColor: highlightBackground,
+        color: base03,
+      },
+    },
+  },
+  { dark: true },
+);
+
+// / The highlighting style for code in the Material Dark theme.
+export const materialDarkHighlightStyle = HighlightStyle.define([
+  { tag: t.keyword, color: base_purple },
+  {
+    tag: [t.name, t.deleted, t.character, t.macroName],
+    color: base_cyan,
+  },
+  { tag: [t.propertyName], color: base_yellow },
+  { tag: [t.variableName], color: base05 },
+  { tag: [t.function(t.variableName)], color: base_cyan },
+  { tag: [t.labelName], color: base_purple },
+  {
+    tag: [t.color, t.constant(t.name), t.standard(t.name)],
+    color: base_yellow,
+  },
+  { tag: [t.definition(t.name), t.separator], color: base_pink },
+  { tag: [t.brace], color: base_purple },
+  {
+    tag: [t.annotation],
+    color: invalid,
+  },
+  {
+    tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
+    color: base_orange,
+  },
+  {
+    tag: [t.typeName, t.className],
+    color: base_orange,
+  },
+  {
+    tag: [t.operator, t.operatorKeyword],
+    color: base_indigo,
+  },
+  {
+    tag: [t.tagName],
+    color: base_deeporange,
+  },
+  {
+    tag: [t.squareBracket],
+    color: base_red,
+  },
+  {
+    tag: [t.angleBracket],
+    color: base02,
+  },
+  {
+    tag: [t.attributeName],
+    color: base05,
+  },
+  {
+    tag: [t.regexp],
+    color: invalid,
+  },
+  {
+    tag: [t.quote],
+    color: base_green,
+  },
+  { tag: [t.string], color: base_lightgreen },
+  {
+    tag: t.link,
+    color: base_cyan,
+    textDecoration: 'underline',
+    textUnderlinePosition: 'under',
+  },
+  {
+    tag: [t.url, t.escape, t.special(t.string)],
+    color: base_yellow,
+  },
+  { tag: [t.meta], color: base03 },
+  { tag: [t.comment], color: base03, fontStyle: 'italic' },
+  { tag: t.monospace, color: base05 },
+  { tag: t.strong, fontWeight: 'bold', color: base_red },
+  { tag: t.emphasis, fontStyle: 'italic', color: base_lightgreen },
+  { tag: t.strikethrough, textDecoration: 'line-through' },
+  { tag: t.heading, fontWeight: 'bold', color: base_yellow },
+  { tag: t.heading1, fontWeight: 'bold', color: base_yellow },
+  {
+    tag: [t.heading2, t.heading3, t.heading4],
+    fontWeight: 'bold',
+    color: base_yellow,
+  },
+  {
+    tag: [t.heading5, t.heading6],
+    color: base_yellow,
+  },
+  { tag: [t.atom, t.bool, t.special(t.variableName)], color: base_cyan },
+  {
+    tag: [t.processingInstruction, t.inserted],
+    color: base_red,
+  },
+  {
+    tag: [t.contentSeparator],
+    color: base_cyan,
+  },
+  { tag: t.invalid, color: base02, borderBottom: `1px dotted ${base_red}` },
+]);
+
+// / Extension to enable the Material Dark theme (both the editor theme and
+// / the highlight style).
+export const materialDark: Extension = [
+  materialDarkTheme,
+  syntaxHighlighting(materialDarkHighlightStyle),
+];

+ 231 - 0
packages/editor/src/services/editor-theme/nord.ts

@@ -0,0 +1,231 @@
+// Ref: https://github.com/craftzdog/cm6-themes/blob/221936c525dcfc05b298cc4d4a0ba284cb7c7138/packages/nord/src/index.ts
+
+import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
+import { Extension } from '@codemirror/state';
+import { EditorView } from '@codemirror/view';
+import { tags as t } from '@lezer/highlight';
+
+// Author: stephen-liu-fipo
+// Colors from https://www.nordtheme.com/docs/colors-and-palettes
+// Polar Night
+const base00 = '#2e3440'; // black
+const base01 = '#3b4252'; // dark grey
+const base02 = '#434c5e';
+const base03 = '#4c566a'; // grey
+
+// Snow Storm
+const base04 = '#d8dee9'; // grey
+const base05 = '#e5e9f0'; // off white
+const base06 = '#eceff4'; // white
+
+// Frost
+const base07 = '#8fbcbb'; // moss green
+const base08 = '#88c0d0'; // ice blue
+const base09 = '#81a1c1'; // water blue
+const base0A = '#5e81ac'; // deep blue
+
+// Aurora
+const base0b = '#bf616a'; // red
+const base0C = '#d08770'; // orange
+const base0D = '#ebcb8b'; // yellow
+const base0E = '#a3be8c'; // green
+const base0F = '#b48ead'; // purple
+
+const invalid = '#d30102';
+const darkBackground = '#252a33';
+// Cutomize
+const highlightBackground = base02;
+const background = base00;
+const tooltipBackground = base01;
+const selection = base03;
+// Cutomize
+const cursor = base06;
+
+// / The editor theme styles for Nord.
+export const nordTheme = EditorView.theme(
+  {
+    '&': {
+      color: base04,
+      backgroundColor: background,
+    },
+
+    '.cm-content': {
+      caretColor: cursor,
+    },
+
+    '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
+    '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
+      { backgroundColor: selection },
+
+    '.cm-panels': { backgroundColor: darkBackground, color: base03 },
+    '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
+    '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
+
+    '.cm-searchMatch': {
+      backgroundColor: 'transparent',
+      outline: `1px solid ${base07}`,
+    },
+    '.cm-searchMatch.cm-searchMatch-selected': {
+      backgroundColor: base03,
+      color: base00,
+    },
+
+    '.cm-activeLine': { backgroundColor: highlightBackground },
+    '.cm-selectionMatch': {
+      backgroundColor: base05,
+      color: base01,
+    },
+
+    '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
+      // Customize
+      outline: `1px solid ${base03}`,
+    },
+
+    '&.cm-focused .cm-matchingBracket': {
+      // Cutomize
+      backgroundColor: base02,
+      color: base02,
+    },
+
+    '.cm-gutters': {
+      backgroundColor: base00,
+      color: base03,
+      border: 'none',
+    },
+
+    '.cm-activeLineGutter': {
+      backgroundColor: highlightBackground,
+      color: base04,
+    },
+
+    '.cm-foldPlaceholder': {
+      backgroundColor: 'transparent',
+      border: 'none',
+      color: '#ddd',
+    },
+
+    '.cm-tooltip': {
+      border: 'none',
+      backgroundColor: tooltipBackground,
+    },
+    '.cm-tooltip .cm-tooltip-arrow:before': {
+      borderTopColor: 'transparent',
+      borderBottomColor: 'transparent',
+    },
+    '.cm-tooltip .cm-tooltip-arrow:after': {
+      borderTopColor: tooltipBackground,
+      borderBottomColor: tooltipBackground,
+    },
+    '.cm-tooltip-autocomplete': {
+      '& > ul > li[aria-selected]': {
+        backgroundColor: highlightBackground,
+        color: base03,
+      },
+    },
+  },
+  { dark: true },
+);
+
+// / The highlighting style for code in the Nord theme.
+export const nordHighlightStyle = HighlightStyle.define([
+  { tag: t.keyword, color: base0A },
+  {
+    tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
+    color: base08,
+  },
+  { tag: [t.variableName], color: base07 },
+  { tag: [t.function(t.variableName)], color: base07 },
+  { tag: [t.labelName], color: base09 },
+  {
+    tag: [t.color, t.constant(t.name), t.standard(t.name)],
+    color: base0A,
+  },
+  { tag: [t.definition(t.name), t.separator], color: base0E },
+  { tag: [t.brace], color: base07 },
+  {
+    tag: [t.annotation],
+    color: invalid,
+  },
+  {
+    tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
+    color: base0F,
+  },
+  {
+    tag: [t.typeName, t.className],
+    color: base0D,
+  },
+  {
+    tag: [t.operator, t.operatorKeyword],
+    color: base0E,
+  },
+  {
+    tag: [t.tagName],
+    color: base0F,
+  },
+  {
+    tag: [t.squareBracket],
+    color: base0b,
+  },
+  {
+    tag: [t.angleBracket],
+    color: base0C,
+  },
+  {
+    tag: [t.attributeName],
+    color: base0D,
+  },
+  {
+    tag: [t.regexp],
+    color: base0A,
+  },
+  {
+    tag: [t.quote],
+    color: base0F,
+  },
+  { tag: [t.string], color: base0E },
+  {
+    tag: t.link,
+    color: base0E,
+    textDecoration: 'underline',
+    textUnderlinePosition: 'under',
+  },
+  {
+    tag: [t.url, t.escape, t.special(t.string)],
+    color: base07,
+  },
+  { tag: [t.meta], color: base08 },
+  { tag: [t.monospace], color: base04, fontStyle: 'italic' },
+  { tag: [t.comment], color: base03, fontStyle: 'italic' },
+  { tag: t.strong, fontWeight: 'bold', color: base0A },
+  { tag: t.emphasis, fontStyle: 'italic', color: base0A },
+  { tag: t.strikethrough, textDecoration: 'line-through' },
+  { tag: t.heading, fontWeight: 'bold', color: base0A },
+  { tag: t.special(t.heading1), fontWeight: 'bold', color: base0A },
+  { tag: t.heading1, fontWeight: 'bold', color: base0A },
+  {
+    tag: [t.heading2, t.heading3, t.heading4],
+    fontWeight: 'bold',
+    color: base0A,
+  },
+  {
+    tag: [t.heading5, t.heading6],
+    color: base0A,
+  },
+  { tag: [t.atom, t.bool, t.special(t.variableName)], color: base0C },
+  {
+    tag: [t.processingInstruction, t.inserted],
+    color: base07,
+  },
+  {
+    tag: [t.contentSeparator],
+    color: base0D,
+  },
+  { tag: t.invalid, color: base02, borderBottom: `1px dotted ${invalid}` },
+]);
+
+// / Extension to enable the Nord theme (both the editor theme and
+// / the highlight style).
+export const nord: Extension = [
+  nordTheme,
+  syntaxHighlighting(nordHighlightStyle),
+];

+ 1 - 0
packages/editor/src/services/editor-theme/original-dark.ts

@@ -8,6 +8,7 @@ export const originalDark = createTheme({
   settings: {
     background: '#323132',
     foreground: '#EFEEED',
+    caret: '#A2A9B5',
     selection: '#4C5964',
     selectionMatch: '#3A546E',
     gutterBackground: '#393939',

+ 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 },
   });

+ 15 - 15
packages/editor/src/stores/use-collaborative-editor-mode.ts

@@ -18,6 +18,7 @@ type UserLocalState = {
 }
 
 export const useCollaborativeEditorMode = (
+    isEnabled: boolean,
     user?: IUserHasId,
     pageId?: string,
     initialValue?: string,
@@ -30,8 +31,9 @@ export const useCollaborativeEditorMode = (
 
   const { data: socket } = useGlobalSocket();
 
-  const cleanupYDoc = () => {
-    if (cPageId === pageId) {
+  // Cleanup Ydoc
+  useEffect(() => {
+    if (cPageId === pageId && isEnabled) {
       return;
     }
 
@@ -49,10 +51,11 @@ export const useCollaborativeEditorMode = (
 
     // reset editors
     onEditorsUpdated?.([]);
-  };
+  }, [cPageId, isEnabled, onEditorsUpdated, pageId, provider?.awareness, socket, ydoc]);
 
-  const setupYDoc = () => {
-    if (ydoc != null) {
+  // Setup Ydoc
+  useEffect(() => {
+    if (ydoc != null || !isEnabled) {
       return;
     }
 
@@ -63,9 +66,10 @@ export const useCollaborativeEditorMode = (
 
     const _ydoc = new Y.Doc();
     setYdoc(_ydoc);
-  };
+  }, [isEnabled, provider, ydoc]);
 
-  const setupProvider = () => {
+  // Setup provider
+  useEffect(() => {
     if (provider != null || ydoc == null || socket == null || onEditorsUpdated == null) {
       return;
     }
@@ -104,9 +108,10 @@ export const useCollaborativeEditorMode = (
     });
 
     setProvider(socketIOProvider);
-  };
+  }, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
 
-  const setupYDocExtensions = () => {
+  // Setup Ydoc Extensions
+  useEffect(() => {
     if (ydoc == null || provider == null || codeMirrorEditor == null) {
       return;
     }
@@ -127,10 +132,5 @@ export const useCollaborativeEditorMode = (
       cleanupYUndoManagerKeymap?.();
       cleanupYCollab?.();
     };
-  };
-
-  useEffect(cleanupYDoc, [cPageId, onEditorsUpdated, pageId, provider, socket, ydoc]);
-  useEffect(setupYDoc, [provider, ydoc]);
-  useEffect(setupProvider, [initialValue, onEditorsUpdated, pageId, provider, socket, user, ydoc]);
-  useEffect(setupYDocExtensions, [codeMirrorEditor, provider, ydoc]);
+  }, [codeMirrorEditor, provider, ydoc]);
 };

+ 6 - 4
packages/pluginkit/vitest.config.ts

@@ -10,10 +10,12 @@ export default defineConfig({
     clearMocks: true,
     globals: true,
     coverage: {
-      lines: 100,
-      functions: 100,
-      branches: 100,
-      statements: 100,
+      thresholds: {
+        lines: 100,
+        functions: 100,
+        branches: 100,
+        statements: 100,
+      },
     },
   },
 });

+ 2 - 1
packages/preset-themes/src/index.ts

@@ -1,3 +1,4 @@
 export * from './consts/preset-themes';
 
-export const manifestPath = 'dist/themes/manifest.json';
+export const themesRootPath = 'dist/themes';
+export const manifestPath = `${themesRootPath}/.vite/manifest.json`;

+ 1 - 1
packages/slack/package.json

@@ -57,7 +57,7 @@
     "browser-bunyan": "^1.6.3",
     "bunyan": "^1.8.15",
     "crypto": "^1.0.1",
-    "date-fns": "^2.23.0",
+    "date-fns": "^3.6.0",
     "extensible-custom-error": "^0.0.7",
     "http-errors": "^2.0.0",
     "qs": "^6.10.2",

+ 1 - 1
packages/slack/src/utils/generate-last-update-markdown.ts

@@ -1,4 +1,4 @@
-import { formatDistanceStrict } from 'date-fns';
+import { formatDistanceStrict } from 'date-fns/formatDistanceStrict';
 
 export function generateLastUpdateMrkdwn(updatedAt: string | Date | number, baseDate: Date): string {
   if (updatedAt != null) {

+ 1 - 0
packages/ui/package.json

@@ -40,6 +40,7 @@
     "@growi/core": "link:../core"
   },
   "devDependencies": {
+    "date-fns": "^3.6.0",
     "reactstrap": "^9.2.0"
   },
   "peerDependencies": {

+ 0 - 6
packages/ui/scss/molecules/_page_list.scss

@@ -44,12 +44,6 @@
         margin-right: 2px;
       }
 
-      .footstamp-icon {
-        svg {
-          width: 14px;
-          height: 14px;
-        }
-      }
       .seen-users-count {
         &.strength-0,
         &.strength-1,

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно