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

Merge remote-tracking branch 'origin/master' into imprv/140677-145630-new-design

Yuki Takei 1 год назад
Родитель
Сommit
a9782d4d96
100 измененных файлов с 578 добавлено и 142 удалено
  1. 1 0
      .devcontainer/devcontainer.json
  2. 0 1
      .eslintrc.js
  3. 1 1
      .github/workflows/reusable-app-prod.yml
  4. 88 1
      CHANGELOG.md
  5. 2 0
      apps/app/config/logger/config.dev.js
  6. 1 1
      apps/app/docker/README.md
  7. 0 1
      apps/app/next-env.d.ts
  8. 4 1
      apps/app/nodemon.json
  9. 6 6
      apps/app/package.json
  10. 129 1
      apps/app/playwright/20-basic-features/access-to-page.spec.ts
  11. 50 0
      apps/app/playwright/23-editor/saving.spec.ts
  12. 27 0
      apps/app/playwright/23-editor/template-modal.spec.ts
  13. BIN
      apps/app/public/favicon.ico
  14. 24 0
      apps/app/public/favicon.svg
  15. BIN
      apps/app/public/images/icons/favicon/android-icon-144x144.png
  16. BIN
      apps/app/public/images/icons/favicon/android-icon-192x192.png
  17. BIN
      apps/app/public/images/icons/favicon/android-icon-36x36.png
  18. BIN
      apps/app/public/images/icons/favicon/android-icon-48x48.png
  19. BIN
      apps/app/public/images/icons/favicon/android-icon-72x72.png
  20. BIN
      apps/app/public/images/icons/favicon/android-icon-96x96.png
  21. BIN
      apps/app/public/images/icons/favicon/apple-icon-114x114.png
  22. BIN
      apps/app/public/images/icons/favicon/apple-icon-120x120.png
  23. BIN
      apps/app/public/images/icons/favicon/apple-icon-144x144.png
  24. BIN
      apps/app/public/images/icons/favicon/apple-icon-152x152.png
  25. BIN
      apps/app/public/images/icons/favicon/apple-icon-180x180.png
  26. BIN
      apps/app/public/images/icons/favicon/apple-icon-57x57.png
  27. BIN
      apps/app/public/images/icons/favicon/apple-icon-60x60.png
  28. BIN
      apps/app/public/images/icons/favicon/apple-icon-72x72.png
  29. BIN
      apps/app/public/images/icons/favicon/apple-icon-76x76.png
  30. BIN
      apps/app/public/images/icons/favicon/apple-icon-precomposed.png
  31. BIN
      apps/app/public/images/icons/favicon/apple-icon.png
  32. 0 2
      apps/app/public/images/icons/favicon/browserconfig.xml
  33. BIN
      apps/app/public/images/icons/favicon/favicon-16x16.png
  34. BIN
      apps/app/public/images/icons/favicon/favicon-32x32.png
  35. BIN
      apps/app/public/images/icons/favicon/favicon-96x96.png
  36. BIN
      apps/app/public/images/icons/favicon/ms-icon-144x144.png
  37. BIN
      apps/app/public/images/icons/favicon/ms-icon-150x150.png
  38. BIN
      apps/app/public/images/icons/favicon/ms-icon-310x310.png
  39. BIN
      apps/app/public/images/icons/favicon/ms-icon-70x70.png
  40. 8 0
      apps/app/public/static/locales/en_US/translation.json
  41. 8 0
      apps/app/public/static/locales/fr_FR/translation.json
  42. 8 0
      apps/app/public/static/locales/ja_JP/translation.json
  43. 8 0
      apps/app/public/static/locales/zh_CN/translation.json
  44. 6 5
      apps/app/src/client/components/Admin/App/V5PageMigration.tsx
  45. 1 3
      apps/app/src/client/components/Admin/Common/LabeledProgressBar.tsx
  46. 2 15
      apps/app/src/client/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  47. 3 3
      apps/app/src/client/components/Bookmarks/BookmarkFolderItem.tsx
  48. 1 0
      apps/app/src/client/components/Bookmarks/BookmarkFolderTree.module.scss
  49. 0 1
      apps/app/src/client/components/Common/Dropdown/PageItemControl.tsx
  50. 5 0
      apps/app/src/client/components/InAppNotification/PageNotification/ModelNotification.module.scss
  51. 4 2
      apps/app/src/client/components/InAppNotification/PageNotification/ModelNotification.tsx
  52. 41 1
      apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx
  53. 2 2
      apps/app/src/client/components/Navbar/PageEditorModeManager.tsx
  54. 2 0
      apps/app/src/client/components/Page/DisplaySwitcher.tsx
  55. 0 1
      apps/app/src/client/components/PageControls/PageControls.tsx
  56. 18 10
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  57. 6 6
      apps/app/src/client/components/PageEditor/ScrollSyncHelper.tsx
  58. 4 4
      apps/app/src/client/components/PageHistory/PageRevisionTable.tsx
  59. 3 3
      apps/app/src/client/components/PageHistory/RevisionDiff.tsx
  60. 1 1
      apps/app/src/client/components/PageList/PageListItemL.tsx
  61. 1 0
      apps/app/src/client/components/PageList/PageListItemS.module.scss
  62. 10 1
      apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  63. 9 2
      apps/app/src/client/components/ReactMarkdownComponents/Header.tsx
  64. 9 1
      apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx
  65. 3 3
      apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx
  66. 4 0
      apps/app/src/client/components/Sidebar/PageTreeItem/PageTreeItem.module.scss
  67. 4 0
      apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  68. 2 2
      apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  69. 9 1
      apps/app/src/client/components/TemplateModal/TemplateModal.tsx
  70. 1 1
      apps/app/src/client/components/TreeItem/TreeItemLayout.tsx
  71. 6 0
      apps/app/src/client/services/page-operation.ts
  72. 7 15
      apps/app/src/client/services/side-effects/yjs.ts
  73. 6 4
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  74. 1 1
      apps/app/src/interfaces/websocket.ts
  75. 6 1
      apps/app/src/interfaces/yjs.ts
  76. 1 1
      apps/app/src/migrations/20180927102719-init-serverurl.js
  77. 1 1
      apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  78. 1 1
      apps/app/src/migrations/20190618104011-add-config-app-installed.js
  79. 1 1
      apps/app/src/migrations/20200420160390-remove-crowi-layout.js
  80. 1 1
      apps/app/src/migrations/20200512005851-remove-behavior-type.js
  81. 1 1
      apps/app/src/migrations/20200514001356-update-theme-color-for-dark.js
  82. 1 1
      apps/app/src/migrations/20200620203632-normalize-locale-id.js
  83. 1 1
      apps/app/src/migrations/20200827045151-remove-layout-setting.js
  84. 1 1
      apps/app/src/migrations/20200828024025-copy-aws-setting.js
  85. 1 1
      apps/app/src/migrations/20200901034313-update-mail-transmission.js
  86. 1 1
      apps/app/src/migrations/20200903080025-remove-timeline-type.js.js
  87. 1 1
      apps/app/src/migrations/20200915035234-rename-s3-config.js
  88. 1 1
      apps/app/src/migrations/20210830074539-update-configs-for-slackbot.js
  89. 1 1
      apps/app/src/migrations/20211005131430-config-without-proxy-command-permission-for-renaming.js
  90. 1 2
      apps/app/src/migrations/20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js
  91. 2 3
      apps/app/src/migrations/20220311011114-convert-page-delete-config.js
  92. 1 1
      apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js
  93. 1 1
      apps/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  94. 1 1
      apps/app/src/migrations/20230213090921-remove-presentation-configurations.js
  95. 1 3
      apps/app/src/migrations/20230731075753-add_installed_date_to_config.js
  96. 2 0
      apps/app/src/pages/_document.page.tsx
  97. 5 8
      apps/app/src/server/crowi/index.js
  98. 3 3
      apps/app/src/server/models/config.ts
  99. 1 1
      apps/app/src/server/models/index.ts
  100. 4 1
      apps/app/src/server/models/page.ts

+ 1 - 0
.devcontainer/devcontainer.json

@@ -19,6 +19,7 @@
     "eamodio.gitlens",
     "github.vscode-pull-request-github",
     "cschleiden.vscode-github-actions",
+    "cweijan.vscode-database-client2",
     "mongodb.mongodb-vscode",
     "msjsdiag.debugger-for-chrome",
     "firefox-devtools.vscode-firefox-debug",

+ 0 - 1
.eslintrc.js

@@ -49,7 +49,6 @@ module.exports = {
       },
     ],
     '@typescript-eslint/consistent-type-imports': 'warn',
-    '@typescript-eslint/no-explicit-any': 'off',
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     indent: [
       'error',

+ 1 - 1
.github/workflows/reusable-app-prod.yml

@@ -213,7 +213,7 @@ jobs:
       fail-fast: false
       matrix:
         # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['20', '21', '23', '30', '50']
+        spec-group: ['21', '23', '30', '50']
 
     services:
       mongodb:

+ 88 - 1
CHANGELOG.md

@@ -1,9 +1,96 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.11...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.15...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.0.15](https://github.com/weseek/growi/compare/v7.0.14...v7.0.15) - 2024-07-23
+
+### 🐛 Bug Fixes
+
+* fix: The $size query when aggregation to rebuild the index (#8987) @yuki-takei
+* fix: Regaining lost backward compatibility for MongoDB 4.4 (#8985) @yuki-takei
+* fix: Activate express-session middlewares for all sockets in SocketIoService (#8981) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Chage text size in sidebar (#8965) @satof3
+
+## [v7.0.14](https://github.com/weseek/growi/compare/v7.0.13...v7.0.14) - 2024-07-19
+
+### 🐛 Bug Fixes
+
+### 💎 Features
+
+* feat: Alerts when trying to sync with latest revision when yjs data is corrupt (#8971) @miya
+
+### 🚀 Improvement
+
+* imprv: Restrict use of the editing UI from View if there is at least one user currently editing (#8966) @miya
+
+### 🐛 Bug Fixes
+
+* fix: Handle error when folding drawio blocks (#8977) @yuki-takei
+* fix: Sync the editor text with the latest revision menu (1) (#8975) @yuki-takei
+* fix: Sync the editor text with the latest revision menu (2) (#8978) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Normalize Revision.pageId (for #8954) (#8973) @miya
+
+## [v7.0.13](https://github.com/weseek/growi/compare/v7.0.12...v7.0.13) - 2024-07-16
+
+### 💎 Features
+
+* feat: Sync latest revision body to Yjs draft (#8939) @miya
+
+### 🚀 Improvement
+
+* imprv: Better synchronizing between YDoc and the latest revision (#8959) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Revision model (#8967) @yuki-takei
+* fix: Healthcheck with checkServices=mongo (#8961) @yuki-takei
+* fix: Enable  # next to headline in view (#8826) @reiji-h
+
+### 🧰 Maintenance
+
+* ci(deps): bump nodemailer from 6.6.2 to 6.9.14 (#8928) @dependabot
+* support: Update favicon (#8957) @satof3
+
+## [v7.0.12](https://github.com/weseek/growi/compare/v7.0.11...v7.0.12) - 2024-07-10
+
+### 🚀 Improvement
+
+* imprv: lang attribute in Html element to correctly reflect locale (#8940) @maeshinshin
+* imprv: Archive importing and exporting (#8943) @yuki-takei
+* imprv: Restrict indexing for full text search when the body length exceeds the threshold (#8937) @yuki-takei
+* imprv: Dark theme support for emoji mart (#8936) @reiji-h
+* imprv: Add env var for set Elasticsearch reindex bulk size (#8933) @yuki-takei
+* imprv: Size for skeleton for tags (#8923) @yuki-takei
+* imprv: Button opacity of TableWithEditButton and DrawioViewerWithEditButton (#8924) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Initialize sanitize option (#8946) @yuki-takei
+* fix: PageTitleHeader rename input status (#8944) @yuki-takei
+* fix: Presentation section tag (#8941) @yuki-takei
+* fix: Page history colorscheme is broken (#8938) @reiji-h
+* imprv: Rename label for bookmark item (#8925) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Refactor Yjs service (#8949) @yuki-takei
+* support: Upgrade y-mongodb-provider (#8953) @yuki-takei
+* support: Typescriptize Revision model (#8954) @yuki-takei
+* support: Typescriptize SocketIoService (#8948) @yuki-takei
+* support: Update GROWI logo type in NoLogin (#8942) @satof3
+* support: Update logo design (#8934) @satof3
+* ci(deps): bump @azure/identity from 4.0.1 to 4.3.0 (#8927) @dependabot
+* support: Upgrade vitest (#8920) @yuki-takei
+* support: Upgrade playwright (#8921) @yuki-takei
+
 ## [v7.0.11](https://github.com/weseek/growi/compare/v7.0.10...v7.0.11) - 2024-06-25
 
 ### 💎 Features

+ 2 - 0
apps/app/config/logger/config.dev.js

@@ -17,6 +17,8 @@ module.exports = {
   'growi:middleware:safe-redirect': 'debug',
   'growi:service:PassportService': 'debug',
   'growi:service:s2s-messaging:*': 'debug',
+  'growi:service:yjs': 'debug',
+  'growi:service:yjs:*': 'debug',
   // 'growi:service:socket-io': 'debug',
   // 'growi:service:ConfigManager': 'debug',
   // 'growi:service:mail': 'debug',

+ 1 - 1
apps/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`7.0.11`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.11/apps/app/docker/Dockerfile)
+* [`7.0.15`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.15/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)

+ 0 - 1
apps/app/next-env.d.ts

@@ -1,6 +1,5 @@
 /// <reference types="next" />
 /// <reference types="next/image-types/global" />
-/// <reference types="next/navigation-types/compat/navigation" />
 
 // NOTE: This file should not be edited
 // see https://nextjs.org/docs/basic-features/typescript for more information.

+ 4 - 1
apps/app/nodemon.json

@@ -5,8 +5,11 @@
     "public/static",
     "package.json",
     "playwright",
+    "src/client",
+    "src/**/client",
     "test",
     "test-with-vite",
-    "tmp"
+    "tmp",
+    "*.mongodb.js"
   ]
 }

+ 6 - 6
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.0.12-RC.0",
+  "version": "7.0.16-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -148,9 +148,9 @@
     "next-i18next": "^15.2.0",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.1",
-    "nocache": "^3.0.1",
+    "nocache": "^4.0.0",
     "node-cron": "^3.0.2",
-    "nodemailer": "^6.6.2",
+    "nodemailer": "^6.9.14",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "^5.4.0",
     "p-retry": "^4.0.0",
@@ -209,9 +209,9 @@
     "validator": "^13.7.0",
     "ws": "^8.17.1",
     "xss": "^1.0.14",
-    "y-mongodb-provider": "^0.1.10",
+    "y-mongodb-provider": "^0.2.0",
     "y-socket.io": "^1.1.3",
-    "yjs": "^13.6.15"
+    "yjs": "^13.6.18"
   },
   "// comments for defDependencies": {
     "bootstrap": "v5.3.3 has a bug. refs: https://github.com/twbs/bootstrap/issues/39798",
@@ -230,7 +230,7 @@
     "@swc/jest": "^0.2.36",
     "@testing-library/react": "^14.1.2",
     "@testing-library/user-event": "^14.5.2",
-    "@types/express": "^4.17.11",
+    "@types/express": "^4.17.21",
     "@types/jest": "^29.5.2",
     "@types/react-input-autosize": "^2.2.4",
     "@types/react-scroll": "^1.8.4",

+ 129 - 1
apps/app/playwright/20-basic-features/access-to-page.spec.ts

@@ -1,4 +1,9 @@
-import { test, expect } from '@playwright/test';
+import { test, expect, type Page } from '@playwright/test';
+
+const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+  await page.locator('.cm-content').fill(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+};
 
 test('has title', async({ page }) => {
   await page.goto('/Sandbox');
@@ -14,9 +19,132 @@ test('get h1', async({ page }) => {
   await expect(page.getByRole('heading').filter({ hasText: /\/Sandbox/ })).toBeVisible();
 });
 
+test('/Sandbox/Math is successfully loaded', async({ page }) => {
+  await page.goto('/Sandbox/Math');
+
+  // Expect the Math-specific elements to be present
+  await expect(page.locator('.math').first()).toBeVisible();
+});
+
+test('Sandbox with edit is successfully loaded', async({ page }) => {
+  await page.goto('/Sandbox#edit');
+
+  // Expect the Editor-specific elements to be present
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+  await expect(page.getByTestId('save-page-btn')).toBeVisible();
+  await expect(page.getByTestId('grw-grant-selector')).toBeVisible();
+});
+
+test.describe.serial('PageEditor', () => {
+  const body1 = 'hello';
+  const body2 = ' world!';
+  const targetPath = '/Sandbox/testForUseEditingMarkdown';
+
+  test('Edit and save with save-page-btn', async({ page }) => {
+    await page.goto(targetPath);
+
+    await page.getByTestId('editor-button').click();
+    await appendTextToEditorUntilContains(page, body1);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(body1);
+  });
+
+  test('Edit and save with shortcut key', async({ page }) => {
+    const savePageShortcutKey = 'Control+s';
+
+    await page.goto(targetPath);
+
+    await page.getByTestId('editor-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(body1);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(body1);
+
+    await appendTextToEditorUntilContains(page, body1 + body2);
+    await page.keyboard.press(savePageShortcutKey);
+    await page.getByTestId('view-button').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(body1 + body2);
+  });
+});
+
 test('Access to /me page', async({ page }) => {
   await page.goto('/me');
 
   // Expect the UserSettgins-specific elements to be present when accessing /me (UserSettgins)
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
 });
+
+test('All In-App Notification list is successfully loaded', async({ page }) => {
+  await page.goto('/me/all-in-app-notifications');
+
+  // Expect the In-App Notification-specific elements to be present when accessing /me/all-in-app-notifications
+  await expect(page.getByTestId('grw-in-app-notification-page')).toBeVisible();
+});
+
+test('/trash is successfully loaded', async({ page }) => {
+  await page.goto('/trash');
+
+  await expect(page.getByTestId('trash-page-list')).toContainText('There are no pages under this page.');
+});
+
+test('/tags is successfully loaded', async({ page }) => {
+  await page.goto('/tags');
+
+  await expect(page.getByTestId('grw-tags-list')).toContainText('You have no tag, You can set tags on pages');
+});
+
+test.describe.serial('Access to Template Editing Mode', () => {
+  const templateBody1 = 'Template for children';
+  const templateBody2 = 'Template for descendants';
+
+  test('Successfully created template for children', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+    await page.getByTestId('open-page-template-modal-btn').click();
+    expect(page.getByTestId('page-template-modal')).toBeVisible();
+
+    await page.getByTestId('template-button-children').click();
+
+    await appendTextToEditorUntilContains(page, templateBody1);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(templateBody1);
+  });
+
+  test('Template is applied to pages created (template for children)', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await page.getByTestId('grw-page-create-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(templateBody1);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody1);
+  });
+
+  test('Successfully created template for descendants', async({ page }) => {
+    await page.goto('/Sandbox');
+
+    await expect(page.getByTestId('grw-contextual-sub-nav')).toBeVisible();
+    await page.getByTestId('grw-contextual-sub-nav').getByTestId('open-page-item-control-btn').click();
+    await page.getByTestId('open-page-template-modal-btn').click();
+    expect(page.getByTestId('page-template-modal')).toBeVisible();
+
+    await page.getByTestId('template-button-descendants').click();
+
+    await appendTextToEditorUntilContains(page, templateBody2);
+    await page.getByTestId('save-page-btn').click();
+
+    await expect(page.locator('.wiki').first()).toContainText(templateBody2);
+  });
+
+  test('Template is applied to pages created (template for descendants)', async({ page }) => {
+    await page.goto('/Sandbox/Bootstrap5');
+
+    await page.getByTestId('grw-page-create-button').click();
+
+    await expect(page.locator('.cm-content')).toContainText(templateBody2);
+    await expect(page.getByTestId('page-editor-preview-body')).toContainText(templateBody2);
+  });
+});

+ 50 - 0
apps/app/playwright/23-editor/saving.spec.ts

@@ -0,0 +1,50 @@
+import path from 'path';
+
+import { test, expect, type Page } from '@playwright/test';
+
+const appendTextToEditorUntilContains = async(page: Page, text: string) => {
+  await page.locator('.cm-content').fill(text);
+  await expect(page.getByTestId('page-editor-preview-body')).toContainText(text);
+};
+
+
+test('Successfully create page under specific path', async({ page }) => {
+  const newPagePath = '/child';
+  const openPageCreateModalShortcutKey = 'c';
+
+  await page.goto('/Sandbox');
+
+  await page.keyboard.press(openPageCreateModalShortcutKey);
+  await expect(page.getByTestId('page-create-modal')).toBeVisible();
+  page.getByTestId('page-create-modal').locator('.rbt-input-main').fill(newPagePath);
+  page.getByTestId('btn-create-page-under-below').click();
+  await page.getByTestId('view-button').click();
+
+  const createdPageId = path.basename(page.url());
+  expect(createdPageId.length).toBe(24);
+});
+
+
+test('Successfully updating a page using a shortcut on a previously created page', async({ page }) => {
+  const body1 = 'hello';
+  const body2 = ' world!';
+  const savePageShortcutKey = 'Control+s';
+
+  await page.goto('/Sandbox/child');
+
+  // 1st
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+  await appendTextToEditorUntilContains(page, body1);
+  await page.keyboard.press(savePageShortcutKey);
+  await page.getByTestId('view-button').click();
+  await expect(page.locator('.main')).toContainText(body1);
+
+  // 2nd
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+  await appendTextToEditorUntilContains(page, body1 + body2);
+  await page.keyboard.press(savePageShortcutKey);
+  await page.getByTestId('view-button').click();
+  await expect(page.locator('.main')).toContainText(body1 + body2);
+});

+ 27 - 0
apps/app/playwright/23-editor/template-modal.spec.ts

@@ -0,0 +1,27 @@
+import { test, expect } from '@playwright/test';
+
+test('Successfully select template and template locale', async({ page }) => {
+  const jaText = '今日の目標';
+  const enText = "TODAY'S GOALS";
+  await page.goto('/Sandbox/TemplateModal');
+
+  // move to edit mode
+  await page.getByTestId('editor-button').click();
+  await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
+
+  // open TemplateModal
+  const templateModal = page.getByTestId('template-modal');
+  await page.getByTestId('open-template-button').click();
+  await expect(templateModal).toBeVisible();
+
+  // select template and template locale
+  await templateModal.locator('.list-group-item').nth(0).click();
+  await expect(templateModal.locator('.card-body').locator('.has-data-line').nth(1)).toHaveText(enText);
+  await templateModal.getByTestId('select-locale-dropdown-toggle').click();
+  await templateModal.getByTestId('select-locale-dropdown-item').nth(1).click();
+  await expect(templateModal.locator('.card-body').locator('.has-data-line').nth(1)).toHaveText(jaText);
+
+  // insert
+  await templateModal.locator('.btn-primary').click();
+  await expect(page.locator('.has-data-line').nth(1)).toHaveText(jaText);
+});

BIN
apps/app/public/favicon.ico


+ 24 - 0
apps/app/public/favicon.svg

@@ -0,0 +1,24 @@
+
+<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <style>
+    .logo {
+      fill: #777570;
+    }
+    .bg {
+      fill: white;
+    }
+    @media (prefers-color-scheme: dark) {
+      .logo {
+        fill: #E6E5E3;
+      }
+      .bg {
+        fill: black;
+      }
+    }
+  </style>
+<path class="bg" d="M47.134 22.5C47.6699 23.4282 47.6699 24.5718 47.134 25.5L36.866 43.2846C36.3301 44.2128 35.3397 44.7846 34.2679 44.7846L13.7321 44.7846C12.6603 44.7846 11.6699 44.2128 11.134 43.2846L0.866028 25.5C0.33013 24.5718 0.33013 23.4282 0.866028 22.5L11.134 4.71539C11.6699 3.78719 12.6603 3.21539 13.7321 3.21539L34.268 3.21539C35.3397 3.21539 36.3301 3.78719 36.866 4.71539L47.134 22.5Z"/>
+<path class="logo" d="M16.0962 27.332L12.5626 33.4861C12.4547 33.6759 12.4547 33.9097 12.5626 34.0961L15.2836 38.8337C15.3847 39.0065 15.5904 39.1251 15.7995 39.1251H16.0962L19.4848 33.2286L16.0962 27.332Z"/>
+<path class="logo" d="M33.9938 24.8076L29.3307 32.9272C29.243 33.0763 29.0845 33.2322 28.8148 33.2322H19.4819L16.0933 39.1254H32.2135C32.4226 39.1254 32.5979 39.0203 32.7058 38.8339L40.7642 24.8042H33.9938V24.8076Z"/>
+<path class="logo" d="M40.9127 24.5569C41.024 24.3671 41.0307 24.1536 40.9228 23.9639L38.1985 19.2229C38.0906 19.0331 37.9051 18.9111 37.686 18.9111H21.2892C21.0701 18.9111 20.8712 19.0297 20.7599 19.2127L18.1873 23.686L21.5758 29.5893L24.0676 25.2516C24.226 24.9805 24.516 24.8111 24.8262 24.8111H40.7677L40.9127 24.5603V24.5569Z"/>
+<path class="logo" d="M19.1953 15.2715H35.9292L32.7193 9.68338C32.6114 9.49361 32.426 9.375 32.2068 9.375H15.8101C15.5909 9.375 15.392 9.48344 15.2807 9.67322L7.08068 23.9435C6.97278 24.1333 6.97278 24.3638 7.08068 24.5535L10.2973 30.1519L18.6659 15.5698C18.7738 15.38 18.9761 15.2682 19.1953 15.2682V15.2715Z"/>
+</svg>

BIN
apps/app/public/images/icons/favicon/android-icon-144x144.png


BIN
apps/app/public/images/icons/favicon/android-icon-192x192.png


BIN
apps/app/public/images/icons/favicon/android-icon-36x36.png


BIN
apps/app/public/images/icons/favicon/android-icon-48x48.png


BIN
apps/app/public/images/icons/favicon/android-icon-72x72.png


BIN
apps/app/public/images/icons/favicon/android-icon-96x96.png


BIN
apps/app/public/images/icons/favicon/apple-icon-114x114.png


BIN
apps/app/public/images/icons/favicon/apple-icon-120x120.png


BIN
apps/app/public/images/icons/favicon/apple-icon-144x144.png


BIN
apps/app/public/images/icons/favicon/apple-icon-152x152.png


BIN
apps/app/public/images/icons/favicon/apple-icon-180x180.png


BIN
apps/app/public/images/icons/favicon/apple-icon-57x57.png


BIN
apps/app/public/images/icons/favicon/apple-icon-60x60.png


BIN
apps/app/public/images/icons/favicon/apple-icon-72x72.png


BIN
apps/app/public/images/icons/favicon/apple-icon-76x76.png


BIN
apps/app/public/images/icons/favicon/apple-icon-precomposed.png


BIN
apps/app/public/images/icons/favicon/apple-icon.png


+ 0 - 2
apps/app/public/images/icons/favicon/browserconfig.xml

@@ -1,2 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>

BIN
apps/app/public/images/icons/favicon/favicon-16x16.png


BIN
apps/app/public/images/icons/favicon/favicon-32x32.png


BIN
apps/app/public/images/icons/favicon/favicon-96x96.png


BIN
apps/app/public/images/icons/favicon/ms-icon-144x144.png


BIN
apps/app/public/images/icons/favicon/ms-icon-150x150.png


BIN
apps/app/public/images/icons/favicon/ms-icon-310x310.png


BIN
apps/app/public/images/icons/favicon/ms-icon-70x70.png


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

@@ -849,5 +849,13 @@
   },
   "create_page": {
     "untitled": "Untitled"
+  },
+  "sync-latest-revision-body": {
+    "menuitem": "Sync the editor text with the latest revision body",
+    "confirm": "Delete the draft data being entered into the editor and synchronize the latest text. Are you sure you want to run it?",
+    "alert": "The latest text may not have been synchronized. Please reload and check again.",
+    "success-toaster": "Latest text synchronized",
+    "skipped-toaster": "Skipped synchronizing since the editor is not activated. Please open the editor and try again.",
+    "error-toaster": "Synchronization of the latest text failed"
   }
 }

+ 8 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -840,5 +840,13 @@
     "show_wip_page": "Voir brouillon",
     "size_s": "Taille: P",
     "size_l": "Taille: G"
+  },
+  "sync-latest-revision-body": {
+    "menuitem": "Synchroniser le texte de l'éditeur avec le corps de la dernière révision",
+    "confirm": "Delete the draft data being entered into the editor and synchronize the latest text. Are you sure you want to run it?",
+    "alert": "Il se peut que le texte le plus récent n'ait pas été synchronisé. Veuillez recharger et vérifier à nouveau.",
+    "success-toaster": "Dernier texte synchronisé",
+    "skipped-toaster": "Synchronisation ignorée car l'éditeur n'est pas activé. Ouvrir l'éditeur et réessayer.",
+    "error-toaster": "La synchronisation du dernier texte a échoué"
   }
 }

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

@@ -882,5 +882,13 @@
   },
   "create_page": {
     "untitled": "無題のページ"
+  },
+  "sync-latest-revision-body": {
+    "menuitem": "最新のリビジョンの本文とエディタのテキストを同期",
+    "confirm": "エディターに入力中のドラフトデータを削除して最新の本文を同期します。実行しますか?",
+    "alert": "最新の本文が同期されていない可能性があります。リロードして再度ご確認ください。",
+    "success-toaster": "最新の本文を同期しました",
+    "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
+    "error-toaster": "最新の本文の同期に失敗しました"
   }
 }

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

@@ -852,5 +852,13 @@
   },
   "create_page": {
     "untitled": "Untitled"
+  },
+  "sync-latest-revision-body": {
+    "menuitem": "同步编辑器文本与最新修订正文",
+    "confirm": "删除输入编辑器的草稿数据,同步最新文本。 您真的想运行它吗?",
+    "alert": "最新文本可能尚未同步。 请重新加载并再次检查。",
+    "success-toaster": "同步最新文本",
+    "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
+    "error-toaster": "同步最新文本失败"
   }
 }

+ 6 - 5
apps/app/src/client/components/Admin/App/V5PageMigration.tsx

@@ -1,12 +1,14 @@
-import React, {
-  FC, useCallback, useEffect, useState,
-} from 'react';
+import type { FC } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
+import type {
+  PMStartedData, PMMigratingData, PMErrorCountData, PMEndedData,
+} from '~/interfaces/websocket';
 import {
-  SocketEventName, PMStartedData, PMMigratingData, PMErrorCountData, PMEndedData,
+  SocketEventName,
 } from '~/interfaces/websocket';
 import { useGlobalAdminSocket } from '~/stores/websocket';
 
@@ -64,7 +66,6 @@ const V5PageMigration: FC<Props> = (props: Props) => {
         <LabeledProgressBar
           header={t('admin:v5_page_migration.header_upgrading_progress')}
           currentCount={current}
-          errorsCount={skip}
           totalCount={total}
           isInProgress={isInProgress}
         />

+ 1 - 3
apps/app/src/client/components/Admin/Common/LabeledProgressBar.tsx

@@ -6,13 +6,12 @@ type Props = {
   header: string,
   currentCount: number,
   totalCount: number,
-  errorsCount?: number,
   isInProgress?: boolean,
 }
 
 const LabeledProgressBar = (props: Props): JSX.Element => {
   const {
-    header, currentCount, totalCount, errorsCount, isInProgress,
+    header, currentCount, totalCount, isInProgress,
   } = props;
 
   const progressingColor = isInProgress ? 'info' : 'success';
@@ -25,7 +24,6 @@ const LabeledProgressBar = (props: Props): JSX.Element => {
       </h6>
       <Progress multi>
         <Progress bar max={totalCount} color={progressingColor} striped={isInProgress} animated={isInProgress} value={currentCount} />
-        <Progress bar max={totalCount} color="danger" striped={isInProgress} animated={isInProgress} value={errorsCount} />
       </Progress>
     </>
   );

+ 2 - 15
apps/app/src/client/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -16,7 +16,6 @@ class RebuildIndexControls extends React.Component {
     this.state = {
       total: 0,
       current: 0,
-      skip: 0,
     };
   }
 
@@ -32,7 +31,6 @@ class RebuildIndexControls extends React.Component {
         this.setState({
           total: data.totalCount,
           current: data.count,
-          skip: data.skipped,
         });
       });
 
@@ -40,7 +38,6 @@ class RebuildIndexControls extends React.Component {
         this.setState({
           total: data.totalCount,
           current: data.count,
-          skip: data.skipped,
         });
       });
     }
@@ -51,7 +48,7 @@ class RebuildIndexControls extends React.Component {
       isRebuildingProcessing, isRebuildingCompleted,
     } = this.props;
     const {
-      total, current, skip,
+      total, current,
     } = this.state;
     const showProgressBar = isRebuildingProcessing || isRebuildingCompleted;
 
@@ -59,23 +56,13 @@ class RebuildIndexControls extends React.Component {
       return null;
     }
 
-    function getCompletedLabel() {
-      const completedLabel = skip === 0 ? 'Completed' : `Done (${skip} skips)`;
-      return completedLabel;
-    }
-
-    function getSkipLabel() {
-      return `Processing.. (${skip} skips)`;
-    }
-
-    const header = isRebuildingCompleted ? getCompletedLabel() : getSkipLabel();
+    const header = isRebuildingCompleted ? 'Completed' : 'Processing..';
 
     return (
       <div className="mb-3">
         <LabeledProgressBar
           header={header}
           currentCount={current}
-          errorsCount={skip}
           totalCount={total}
         />
       </div>

+ 3 - 3
apps/app/src/client/components/Bookmarks/BookmarkFolderItem.tsx

@@ -4,11 +4,11 @@ import { useCallback, useState } from 'react';
 import type { IPageToDeleteWithMeta } from '@growi/core';
 import { DropdownToggle } from 'reactstrap';
 
+import { FolderIcon } from '~/client/components/Icons/FolderIcon';
 import {
   addBookmarkToFolder, addNewFolder, hasChildren, updateBookmarkFolder,
 } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
-import { FolderIcon } from '~/client/components/Icons/FolderIcon';
 import type { BookmarkFolderItems, DragItemDataType, DragItemType } from '~/interfaces/bookmark-info';
 import { DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
 import type { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
@@ -250,7 +250,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
               onClick={loadChildFolder}
             >
               <div className="d-flex justify-content-center">
-                <span className="material-symbols-outlined">arrow_right</span>
+                <span className="material-symbols-outlined fs-5">arrow_right</span>
               </div>
             </button>
           </div>
@@ -268,7 +268,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
           ) : (
             <>
               <div className="grw-foldertree-title-anchor ps-1">
-                <p className="text-truncate m-auto ">{name}</p>
+                <p className="text-truncate m-auto">{name}</p>
               </div>
             </>
           )}

+ 1 - 0
apps/app/src/client/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -51,6 +51,7 @@ $grw-bookmark-item-padding-left: 35px;
     .grw-foldertree-title-anchor {
       width: 100%;
       overflow: hidden;
+      font-size: 14px;
       text-decoration: none;
     }
   }

+ 0 - 1
apps/app/src/client/components/Common/Dropdown/PageItemControl.tsx

@@ -108,7 +108,6 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
     await onClickRevertMenuItem(pageId);
   }, [onClickRevertMenuItem, pageId]);
 
-
   // eslint-disable-next-line react-hooks/rules-of-hooks
   const deleteItemClickedHandler = useCallback(async() => {
     if (pageInfo == null || onClickDeleteMenuItem == null) {

+ 5 - 0
apps/app/src/client/components/InAppNotification/PageNotification/ModelNotification.module.scss

@@ -0,0 +1,5 @@
+.modal-notification :global {
+  .page-title {
+    font-size: 14px;
+  }
+}

+ 4 - 2
apps/app/src/client/components/InAppNotification/PageNotification/ModelNotification.tsx

@@ -8,6 +8,8 @@ import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
 import FormattedDistanceDate from '../../FormattedDistanceDate';
 
+import styles from './ModelNotification.module.scss';
+
 type Props = {
   notification: IInAppNotification & HasObjectId
   actionMsg: string
@@ -21,8 +23,8 @@ export const ModelNotification: FC<Props> = (props) => {
   } = props;
 
   return (
-    <div className="p-2 overflow-hidden">
-      <div className="text-truncate">
+    <div className={`${styles['modal-notification']} p-2 overflow-hidden`}>
+      <div className="text-truncate page-title">
         <b>{actionUsers}</b>
         {actionMsg}
         <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />

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

@@ -7,6 +7,8 @@ import type {
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import { GlobalCodeMirrorEditorKey } from '@growi/editor';
+import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
@@ -14,7 +16,8 @@ import { useRouter } from 'next/router';
 import Sticky from 'react-stickynode';
 import { DropdownItem } from 'reactstrap';
 
-import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
+import { exportAsMarkdown, updateContentWidth, syncLatestRevisionBody } from '~/client/services/page-operation';
+import { toastSuccess, toastError, toastWarning } from '~/client/util/toastr';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
@@ -76,8 +79,45 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openAccessoriesModal } = usePageAccessoriesModal();
 
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
+
+  const syncLatestRevisionBodyHandler = useCallback(async() => {
+    // eslint-disable-next-line no-alert
+    const answer = window.confirm(t('sync-latest-revision-body.confirm'));
+    if (answer) {
+      try {
+        const editingMarkdownLength = codeMirrorEditor?.getDoc().length;
+        const res = await syncLatestRevisionBody(pageId, editingMarkdownLength);
+
+        if (!res.synced) {
+          toastWarning(t('sync-latest-revision-body.skipped-toaster'));
+          return;
+        }
+
+        if (res?.isYjsDataBroken) {
+          // eslint-disable-next-line no-alert
+          window.alert(t('sync-latest-revision-body.alert'));
+          return;
+        }
+
+        toastSuccess(t('sync-latest-revision-body.success-toaster'));
+      }
+      catch {
+        toastError(t('sync-latest-revision-body.error-toaster'));
+      }
+    }
+  }, [codeMirrorEditor, pageId, t]);
+
   return (
     <>
+      <DropdownItem
+        onClick={() => syncLatestRevisionBodyHandler()}
+        className="grw-page-control-dropdown-item"
+      >
+        <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">sync</span>
+        {t('sync-latest-revision-body.menuitem')}
+      </DropdownItem>
+
       {/* Presentation */}
       <DropdownItem
         onClick={() => openPresentationModal()}

+ 2 - 2
apps/app/src/client/components/Navbar/PageEditorModeManager.tsx

@@ -93,11 +93,11 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
   const _isBtnDisabled = isCreating || isBtnDisabled;
 
   const circleColor = useMemo(() => {
-    if (currentPageYjsData?.awarenessStateSize != null && currentPageYjsData.awarenessStateSize > 0) {
+    if ((currentPageYjsData?.awarenessStateSize ?? 0) > 0) {
       return 'bg-primary';
     }
 
-    if (currentPageYjsData?.hasRevisionBodyDiff != null && currentPageYjsData.hasRevisionBodyDiff) {
+    if (currentPageYjsData?.hasYdocsNewerThanLatestRevision ?? false) {
       return 'bg-secondary';
     }
   }, [currentPageYjsData]);

+ 2 - 0
apps/app/src/client/components/Page/DisplaySwitcher.tsx

@@ -3,6 +3,7 @@ import dynamic from 'next/dynamic';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { useIsEditable } from '~/stores-universal/context';
 import { EditorMode, useEditorMode } from '~/stores-universal/ui';
+import { useReservedNextCaretLine } from '~/stores/editor';
 import { useIsLatestRevision } from '~/stores/page';
 
 import { LazyRenderer } from '../Common/LazyRenderer';
@@ -18,6 +19,7 @@ export const DisplaySwitcher = (): JSX.Element => {
   const { data: isLatestRevision } = useIsLatestRevision();
 
   useHashChangedEffect();
+  useReservedNextCaretLine();
 
   return (
     <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>

+ 0 - 1
apps/app/src/client/components/PageControls/PageControls.tsx

@@ -1,4 +1,3 @@
-import type { MouseEventHandler } from 'react';
 import React, {
   memo, useCallback, useEffect, useMemo, useRef,
 } from 'react';

+ 18 - 10
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -32,6 +32,7 @@ import {
 import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { useNextThemes } from '~/stores-universal/use-next-themes';
 import {
+  useReservedNextCaretLine,
   useEditorSettings,
   useCurrentIndentSize,
   useEditingMarkdown,
@@ -109,6 +110,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: user } = useCurrentUser();
   const { onEditorsUpdated } = useEditingUsers();
   const onConflict = useConflictResolver();
+  const { data: reservedNextCaretLine, mutate: mutateReservedNextCaretLine } = useReservedNextCaretLine();
 
   const { data: rendererOptions } = usePreviewOptions();
 
@@ -298,19 +300,25 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     }
   }, [initialValue, isIndentSizeForced, mutateCurrentIndentSize]);
 
-  // set handler to set caret line
+
+  // set caret line if the edit button next to Header is clicked.
   useEffect(() => {
-    const handler = (lineNumber?: number) => {
-      codeMirrorEditor?.setCaretLine(lineNumber);
+    if (codeMirrorEditor?.setCaretLine == null) {
+      return;
+    }
+    if (editorMode === EditorMode.Editor) {
+      codeMirrorEditor.setCaretLine(reservedNextCaretLine ?? 0, true);
+    }
 
-      // TODO: scroll to the caret line
-    };
-    globalEmitter.on('setCaretLine', handler);
+  }, [codeMirrorEditor, editorMode, reservedNextCaretLine]);
+
+  // reset caret line if returning to the View.
+  useEffect(() => {
+    if (editorMode === EditorMode.View) {
+      mutateReservedNextCaretLine(0);
+    }
+  }, [editorMode, mutateReservedNextCaretLine]);
 
-    return function cleanup() {
-      globalEmitter.removeListener('setCaretLine', handler);
-    };
-  }, [codeMirrorEditor]);
 
   // TODO: Check the reproduction conditions that made this code necessary and confirm reproduction
   // // when transitioning to a different page, if the initialValue is the same,

+ 6 - 6
apps/app/src/client/components/PageEditor/ScrollSyncHelper.tsx

@@ -121,12 +121,12 @@ const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLEl
   newScrollTop += calcScrollElementToTop(previewElements[topPreviewElementIndex]);
   newScrollTop += calcScorllElementByRatio(
     {
-      start: editorElements[startEditorElementIndex].getBoundingClientRect(),
-      top: editorElements[topEditorElementIndex].getBoundingClientRect(),
+      start: editorElements[startEditorElementIndex]?.getBoundingClientRect(),
+      top: editorElements[topEditorElementIndex]?.getBoundingClientRect(),
       next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
     },
     {
-      start: previewElements[topPreviewElementIndex].getBoundingClientRect(),
+      start: previewElements[topPreviewElementIndex]?.getBoundingClientRect(),
       next: previewElements[topPreviewElementIndex + 1]?.getBoundingClientRect(),
     },
   );
@@ -156,12 +156,12 @@ const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLE
   newScrollTop += calcScrollElementToTop(editorElements[startEditorElementIndex]);
   newScrollTop += calcScorllElementByRatio(
     {
-      start: previewElements[topPreviewElementIndex].getBoundingClientRect(),
-      top: previewElements[topPreviewElementIndex].getBoundingClientRect(),
+      start: previewElements[topPreviewElementIndex]?.getBoundingClientRect(),
+      top: previewElements[topPreviewElementIndex]?.getBoundingClientRect(),
       next: previewElements[topPreviewElementIndex + 1]?.getBoundingClientRect(),
     },
     {
-      start: editorElements[startEditorElementIndex].getBoundingClientRect(),
+      start: editorElements[startEditorElementIndex]?.getBoundingClientRect(),
       next: editorElements[nextEditorElementIndex]?.getBoundingClientRect(),
     },
   );

+ 4 - 4
apps/app/src/client/components/PageHistory/PageRevisionTable.tsx

@@ -2,7 +2,7 @@ import React, {
   useEffect, useRef, useState,
 } from 'react';
 
-import type { IRevisionHasPageId } from '@growi/core';
+import type { IRevisionHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { useSWRxInfinitePageRevisions } from '~/stores/page';
@@ -48,8 +48,8 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
     || (isValidating && data != null && typeof data[size - 1] === 'undefined');
   const isReachingEnd = (revisionPerPage === 0) || !!(data != null && data[data.length - 1]?.revisions.length < REVISIONS_PER_PAGE);
 
-  const [sourceRevision, setSourceRevision] = useState<IRevisionHasPageId>();
-  const [targetRevision, setTargetRevision] = useState<IRevisionHasPageId>();
+  const [sourceRevision, setSourceRevision] = useState<IRevisionHasId>();
+  const [targetRevision, setTargetRevision] = useState<IRevisionHasId>();
 
   const tbodyRef = useRef<HTMLTableSectionElement>(null);
 
@@ -96,7 +96,7 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
   }, [isLoadingMore, isReachingEnd, setSize, size]);
 
 
-  const renderRow = (revision: IRevisionHasPageId, previousRevision: IRevisionHasPageId, latestRevision: IRevisionHasPageId,
+  const renderRow = (revision: IRevisionHasId, previousRevision: IRevisionHasId, latestRevision: IRevisionHasId,
       isOldestRevision: boolean, hasDiff: boolean) => {
 
     const revisionId = revision._id;

+ 3 - 3
apps/app/src/client/components/PageHistory/RevisionDiff.tsx

@@ -1,6 +1,6 @@
 import { useMemo } from 'react';
 
-import type { IRevisionHasPageId } from '@growi/core';
+import type { IRevisionHasId } from '@growi/core';
 import { GrowiThemeSchemeType } from '@growi/core';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { PresetThemesMetadatas } from '@growi/preset-themes';
@@ -26,8 +26,8 @@ import 'diff2html/bundles/css/diff2html.min.css';
 const moduleClass = styles['revision-diff-container'];
 
 type RevisioinDiffProps = {
-  currentRevision: IRevisionHasPageId,
-  previousRevision: IRevisionHasPageId,
+  currentRevision: IRevisionHasId,
+  previousRevision: IRevisionHasId,
   revisionDiffOpened: boolean,
   currentPageId: string,
   currentPagePath: string,

+ 1 - 1
apps/app/src/client/components/PageList/PageListItemL.tsx

@@ -31,10 +31,10 @@ import {
 } from '~/stores/modal';
 import { useIsDeviceLargerThanLg } from '~/stores/ui';
 
+import { PagePathHierarchicalLink } from '../../../components/Common/PagePathHierarchicalLink';
 import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../../stores/page';
 import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageItemControl } from '../Common/Dropdown/PageItemControl';
-import { PagePathHierarchicalLink } from '../../../components/Common/PagePathHierarchicalLink';
 
 type Props = {
   page: IPageWithSearchMeta | IPageWithMeta<IPageInfoForListing & IPageSearchMeta>,

+ 1 - 0
apps/app/src/client/components/PageList/PageListItemS.module.scss

@@ -1,4 +1,5 @@
 .page-title {
   flex: 1;
+  font-size: 14px;
   line-height: 1.2;
 }

+ 10 - 1
apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -13,6 +13,7 @@ import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
 } from '~/stores-universal/context';
 import { useIsRevisionOutdated } from '~/stores/page';
+import { useCurrentPageYjsData } from '~/stores/yjs';
 
 import '@growi/remark-drawio/dist/style.css';
 import styles from './DrawioViewerWithEditButton.module.scss';
@@ -34,6 +35,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
   const { data: isRevisionOutdated } = useIsRevisionOutdated();
+  const { data: currentPageYjsData } = useCurrentPageYjsData();
 
   const [isRendered, setRendered] = useState(false);
   const [mxfile, setMxfile] = useState('');
@@ -57,7 +59,14 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
     }
   }, []);
 
-  const showEditButton = !isRevisionOutdated && isRendered && !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
+  const isNoEditingUsers = currentPageYjsData?.awarenessStateSize == null || currentPageYjsData?.awarenessStateSize === 0;
+  const showEditButton = isNoEditingUsers
+     && !isRevisionOutdated
+     && isRendered
+     && !isGuestUser
+     && !isReadOnlyUser
+     && !isSharedUser
+     && shareLinkId == null;
 
   return (
     <div className={`drawio-viewer-with-edit-button ${styles['drawio-viewer-with-edit-button']}`}>

+ 9 - 2
apps/app/src/client/components/ReactMarkdownComponents/Header.tsx

@@ -9,6 +9,7 @@ import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
 } from '~/stores-universal/context';
+import { useCurrentPageYjsData } from '~/stores/yjs';
 import loggerFactory from '~/utils/logger';
 
 
@@ -26,7 +27,7 @@ declare global {
 
 function setCaretLine(line?: number): void {
   if (line != null) {
-    globalEmitter.emit('setCaretLine', line);
+    globalEmitter.emit('reservedNextCaretLine', line);
   }
 }
 
@@ -66,6 +67,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
+  const { data: currentPageYjsData } = useCurrentPageYjsData();
 
   const router = useRouter();
 
@@ -111,7 +113,12 @@ export const Header = (props: HeaderProps): JSX.Element => {
     };
   }, [activateByHash, router.events]);
 
-  const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
+  // TODO: currentPageYjsData?.hasYdocsNewerThanLatestRevision === false make to hide the edit button when a Yjs draft exists
+  // This is because the current conditional logic cannot handle cases where the draft is an empty string.
+  // It will be possible to address this TODO ySyncAnnotation become available for import.
+  // Ref: https://github.com/yjs/y-codemirror.next/pull/30
+  const showEditButton = !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null
+                            && currentPageYjsData?.hasYdocsNewerThanLatestRevision === false;
 
   return (
     <>

+ 9 - 1
apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -8,6 +8,7 @@ import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
 } from '~/stores-universal/context';
 import { useIsRevisionOutdated } from '~/stores/page';
+import { useCurrentPageYjsData } from '~/stores/yjs';
 
 import styles from './TableWithEditButton.module.scss';
 
@@ -31,6 +32,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
   const { data: isSharedUser } = useIsSharedUser();
   const { data: shareLinkId } = useShareLinkId();
   const { data: isRevisionOutdated } = useIsRevisionOutdated();
+  const { data: currentPageYjsData } = useCurrentPageYjsData();
 
   const bol = node.position?.start.line;
   const eol = node.position?.end.line;
@@ -39,7 +41,13 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
     globalEmitter.emit('launchHandsonTableModal', bol, eol);
   }, [bol, eol]);
 
-  const showEditButton = !isRevisionOutdated && !isGuestUser && !isReadOnlyUser && !isSharedUser && shareLinkId == null;
+  const isNoEditingUsers = currentPageYjsData?.awarenessStateSize == null || currentPageYjsData?.awarenessStateSize === 0;
+  const showEditButton = isNoEditingUsers
+    && !isRevisionOutdated
+    && !isGuestUser
+    && !isReadOnlyUser
+    && !isSharedUser
+    && shareLinkId == null;
 
   return (
     <div className={`editable-with-handsontable ${styles['editable-with-handsontable']}`}>

+ 3 - 3
apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx

@@ -1,6 +1,6 @@
 import React, { useState } from 'react';
 
-import type { IRevisionHasPageId } from '@growi/core';
+import type { IRevisionHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
@@ -22,8 +22,8 @@ const DropdownItemContents = ({ title, contents }) => (
 );
 
 type RevisionComparerProps = {
-  sourceRevision: IRevisionHasPageId
-  targetRevision: IRevisionHasPageId
+  sourceRevision: IRevisionHasId
+  targetRevision: IRevisionHasId
   currentPageId?: string
   currentPagePath: string
   onClose: () => void

+ 4 - 0
apps/app/src/client/components/Sidebar/PageTreeItem/PageTreeItem.module.scss

@@ -8,6 +8,10 @@
   }
 }
 
+// font size
+.page-tree-item :global {
+    font-size: 14px;
+}
 
 // == Colors
 

+ 4 - 0
apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss

@@ -8,6 +8,10 @@
 .list-group-item :global {
   font-size: 12px;
 
+  h6 {
+    font-size: 14px;
+  }
+
   .grw-recent-changes-skeleton-small {
     @include grw-skeleton-text($font-size: 14px, $line-height: 16px);
 

+ 2 - 2
apps/app/src/client/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -9,10 +9,10 @@ import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'react-i18next';
 
-import { useKeywordManager } from '~/client/services/search-operation';
-import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
 import FormattedDistanceDate from '~/client/components/FormattedDistanceDate';
 import InfiniteScroll from '~/client/components/InfiniteScroll';
+import { useKeywordManager } from '~/client/services/search-operation';
+import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
 import LinkedPagePath from '~/models/linked-page-path';
 import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import loggerFactory from '~/utils/logger';

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

@@ -247,13 +247,21 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
               </div>
               <div className="col-6 d-flex justify-content-end">
                 <UncontrolledDropdown>
-                  <DropdownToggle caret type="button" outline className="float-end" disabled={selectedTemplateSummary == null}>
+                  <DropdownToggle
+                    caret
+                    type="button"
+                    outline
+                    className="float-end"
+                    disabled={selectedTemplateSummary == null}
+                    data-testid="select-locale-dropdown-toggle"
+                  >
                     <span className="float-start">{selectedTemplateLocale != null ? selectedTemplateLocale : t('Language')}</span>
                   </DropdownToggle>
                   <DropdownMenu className="dropdown-menu" role="menu">
                     { selectedTemplateLocales != null && Array.from(selectedTemplateLocales).map((locale) => {
                       return (
                         <DropdownItem
+                          data-testid="select-locale-dropdown-item"
                           key={locale}
                           onClick={() => setSelectedTemplateLocale(locale)}
                         >

+ 1 - 1
apps/app/src/client/components/TreeItem/TreeItemLayout.tsx

@@ -152,7 +152,7 @@ export const TreeItemLayout: FC<TreeItemLayoutProps> = (props) => {
               onClick={onClickLoadChildren}
             >
               <div className="d-flex justify-content-center">
-                <span className="material-symbols-outlined">arrow_right</span>
+                <span className="material-symbols-outlined fs-5">arrow_right</span>
               </div>
             </button>
           )}

+ 6 - 0
apps/app/src/client/services/page-operation.ts

@@ -4,6 +4,7 @@ import type { IPageHasId } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 
+import type { SyncLatestRevisionBody } from '~/interfaces/yjs';
 import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import {
   useCurrentPageId, useSWRMUTxCurrentPage, useSWRxApplicableGrant, useSWRxTagsInfo,
@@ -174,3 +175,8 @@ export const unpublish = async(pageId: string): Promise<IPageHasId> => {
   const res = await apiv3Put(`/page/${pageId}/unpublish`);
   return res.data;
 };
+
+export const syncLatestRevisionBody = async(pageId: string, editingMarkdownLength?: number): Promise<SyncLatestRevisionBody> => {
+  const res = await apiv3Put(`/page/${pageId}/sync-latest-revision-body-to-yjs-draft`, { editingMarkdownLength });
+  return res.data;
+};

+ 7 - 15
apps/app/src/client/services/side-effects/yjs.ts

@@ -1,4 +1,4 @@
-import { useCallback, useEffect } from 'react';
+import { useEffect } from 'react';
 
 import { useGlobalSocket } from '@growi/core/dist/swr';
 
@@ -7,27 +7,19 @@ import { useCurrentPageYjsData } from '~/stores/yjs';
 
 export const useCurrentPageYjsDataEffect = (): void => {
   const { data: socket } = useGlobalSocket();
-  const { updateHasRevisionBodyDiff, updateAwarenessStateSize } = useCurrentPageYjsData();
-
-  const hasRevisionBodyDiffUpdateHandler = useCallback((hasRevisionBodyDiff: boolean) => {
-    updateHasRevisionBodyDiff(hasRevisionBodyDiff);
-  }, [updateHasRevisionBodyDiff]);
-
-  const awarenessStateSizeUpdateHandler = useCallback(((awarenessStateSize: number) => {
-    updateAwarenessStateSize(awarenessStateSize);
-  }), [updateAwarenessStateSize]);
+  const { updateHasYdocsNewerThanLatestRevision, updateAwarenessStateSize } = useCurrentPageYjsData();
 
   useEffect(() => {
 
     if (socket == null) { return }
 
-    socket.on(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiffUpdateHandler);
-    socket.on(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSizeUpdateHandler);
+    socket.on(SocketEventName.YjsHasYdocsNewerThanLatestRevisionUpdated, updateHasYdocsNewerThanLatestRevision);
+    socket.on(SocketEventName.YjsAwarenessStateSizeUpdated, updateAwarenessStateSize);
 
     return () => {
-      socket.off(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiffUpdateHandler);
-      socket.off(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSizeUpdateHandler);
+      socket.off(SocketEventName.YjsHasYdocsNewerThanLatestRevisionUpdated, updateHasYdocsNewerThanLatestRevision);
+      socket.off(SocketEventName.YjsAwarenessStateSizeUpdated, updateAwarenessStateSize);
     };
 
-  }, [socket, awarenessStateSizeUpdateHandler, hasRevisionBodyDiffUpdateHandler]);
+  }, [socket, updateAwarenessStateSize, updateHasYdocsNewerThanLatestRevision]);
 };

+ 6 - 4
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -3,19 +3,21 @@ import * as os from 'node:os';
 
 import type { IUserHasId } from '@growi/core';
 
-import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
+import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { aclService } from '~/server/service/acl';
 import loggerFactory from '~/utils/logger';
 
+import type { IGrowiInfo } from '../../interfaces/growi-info';
 import {
-  GrowiWikiType, GrowiExternalAuthProviderType, IGrowiInfo, GrowiServiceType, GrowiAttachmentType, GrowiDeploymentType,
+  GrowiWikiType, GrowiExternalAuthProviderType, GrowiServiceType, GrowiAttachmentType, GrowiDeploymentType,
 } from '../../interfaces/growi-info';
 import { StatusType } from '../../interfaces/questionnaire-answer-status';
 import { type IUserInfo, UserType } from '../../interfaces/user-info';
 import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder, { QuestionnaireOrderDocument } from '../models/questionnaire-order';
+import type { QuestionnaireOrderDocument } from '../models/questionnaire-order';
+import QuestionnaireOrder from '../models/questionnaire-order';
 import { isShowableCondition } from '../util/condition';
 
 

+ 1 - 1
apps/app/src/interfaces/websocket.ts

@@ -51,7 +51,7 @@ export const SocketEventName = {
 
   // Yjs
   YjsAwarenessStateSizeUpdated: 'yjs:awareness-state-size-update',
-  YjsHasRevisionBodyDiffUpdated: 'yjs:has-revision-body-diff-update',
+  YjsHasYdocsNewerThanLatestRevisionUpdated: 'yjs:has-ydocs-newer-than-latest-revision-update',
 } as const;
 export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
 

+ 6 - 1
apps/app/src/interfaces/yjs.ts

@@ -1,4 +1,9 @@
 export type CurrentPageYjsData = {
-  hasRevisionBodyDiff?: boolean,
+  hasYdocsNewerThanLatestRevision?: boolean,
   awarenessStateSize?: number,
 }
+
+export type SyncLatestRevisionBody = {
+  synced: boolean,
+  isYjsDataBroken?: boolean,
+}

+ 1 - 1
apps/app/src/migrations/20180927102719-init-serverurl.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200420160390-remove-crowi-layout.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200512005851-remove-behavior-type.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200514001356-update-theme-color-for-dark.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200620203632-normalize-locale-id.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200828024025-copy-aws-setting.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200903080025-remove-timeline-type.js.js

@@ -1,5 +1,5 @@
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20200915035234-rename-s3-config.js

@@ -1,5 +1,5 @@
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20210830074539-update-configs-for-slackbot.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 1
apps/app/src/migrations/20211005131430-config-without-proxy-command-permission-for-renaming.js

@@ -1,7 +1,7 @@
 import mongoose from 'mongoose';
 
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

+ 1 - 2
apps/app/src/migrations/20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js

@@ -4,6 +4,7 @@ import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 
 import getPageModel from '~/server/models/page';
+import { Revision } from '~/server/models/revision';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
@@ -18,7 +19,6 @@ module.exports = {
   async up(db, client) {
     mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
-    const Revision = getModelSafely('Revision') || require('~/server/models/revision')();
 
     const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });
     const batchStrem = createBatchStream(LIMIT);
@@ -69,7 +69,6 @@ module.exports = {
   async down(db, client) {
     mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
-    const Revision = getModelSafely('Revision') || require('~/server/models/revision')();
 
     const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });
     const batchStrem = createBatchStream(LIMIT);

+ 2 - 3
apps/app/src/migrations/20220311011114-convert-page-delete-config.js

@@ -3,8 +3,8 @@ import mongoose from 'mongoose';
 import {
   PageRecursiveDeleteConfigValue, PageRecursiveDeleteCompConfigValue,
 } from '~/interfaces/page-delete-config';
-import ConfigModel from '~/server/models/config';
-import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import { Config } from '~/server/models/config';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:migrate:convert-page-delete-config');
@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:migrate:convert-page-delete-config');
 module.exports = {
   async up(db, client) {
     mongoose.connect(getMongoUri(), mongoOptions);
-    const Config = getModelSafely('Config') || ConfigModel;
 
     const isNewConfigExists = await Config.count({
       ns: 'crowi',

+ 1 - 1
apps/app/src/migrations/20221014130200-remove-customize-is-saved-states-of-tab-changes.js

@@ -1,5 +1,5 @@
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

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

@@ -1,5 +1,5 @@
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

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

@@ -1,5 +1,5 @@
 // eslint-disable-next-line import/no-named-as-default
-import Config from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 

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

@@ -1,5 +1,5 @@
 // eslint-disable-next-line import/no-named-as-default
-import ConfigModel from '~/server/models/config';
+import { Config } from '~/server/models/config';
 import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
 
@@ -12,7 +12,6 @@ module.exports = {
   async up() {
     logger.info('Apply migration');
     mongoose.connect(getMongoUri(), mongoOptions);
-    const Config = getModelSafely('Config') || ConfigModel;
     const User = getModelSafely('User') || require('~/server/models/user')();
 
     const appInstalled = await Config.findOne({ key: 'app:installed' });
@@ -39,7 +38,6 @@ module.exports = {
   async down() {
     logger.info('Rollback migration');
     mongoose.connect(getMongoUri(), mongoOptions);
-    const Config = getModelSafely('Config') || ConfigModel;
 
     const appInstalled = await Config.findOne({ key: 'app:installed' });
     if (appInstalled != null) {

+ 2 - 0
apps/app/src/pages/_document.page.tsx

@@ -121,6 +121,8 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
         <Head>
           {this.renderCustomScript(customScript)}
           <link rel="stylesheet" key="link-theme" href={themeHref} />
+          <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+          <link rel="alternate icon" href="/favicon.ico" />
           <HeadersForGrowiPlugin pluginResourceEntries={pluginResourceEntries} />
           {this.renderCustomCss(customCss)}
         </Head>

+ 5 - 8
apps/app/src/server/crowi/index.js

@@ -34,9 +34,10 @@ import PageOperationService from '../service/page-operation';
 import PassportService from '../service/passport';
 import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
+import { SocketIoService } from '../service/socket-io';
 import UserGroupService from '../service/user-group';
 import { UserNotificationService } from '../service/user-notification';
-import { instantiateYjsConnectionManager } from '../service/yjs-connection-manager';
+import { initializeYjsService } from '../service/yjs';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 
 
@@ -301,10 +302,7 @@ Crowi.prototype.setupS2sMessagingService = async function() {
 };
 
 Crowi.prototype.setupSocketIoService = async function() {
-  const SocketIoService = require('../service/socket-io');
-  if (this.socketIoService == null) {
-    this.socketIoService = new SocketIoService(this);
-  }
+  this.socketIoService = new SocketIoService(this);
 };
 
 Crowi.prototype.setupModels = async function() {
@@ -475,9 +473,8 @@ Crowi.prototype.start = async function() {
   // attach to socket.io
   this.socketIoService.attachServer(httpServer);
 
-  // Initialization YjsConnectionManager
-  instantiateYjsConnectionManager(this.socketIoService.io);
-  this.socketIoService.setupYjsConnection();
+  // Initialization YjsService
+  initializeYjsService(this.socketIoService.io);
 
   await this.autoInstall();
 

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

@@ -7,7 +7,7 @@ import { RehypeSanitizeType } from '~/interfaces/services/rehype-sanitize';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
-export interface Config {
+export interface IConfig {
   _id: Types.ObjectId;
   ns: string;
   key: string;
@@ -21,7 +21,7 @@ export interface Config {
 interface ModelMethods { any }
 
 
-const schema = new Schema<Config>({
+const schema = new Schema<IConfig>({
   ns: { type: String, required: true },
   key: { type: String, required: true },
   value: { type: String, required: true },
@@ -176,4 +176,4 @@ export const defaultNotificationConfigs: { [key: string]: any } = {
   'slack:token': undefined,
 };
 
-export default getOrCreateModel<Config, ModelMethods>('Config', schema);
+export const Config = getOrCreateModel<IConfig, ModelMethods>('Config', schema);

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

@@ -5,7 +5,6 @@ export const modelsDependsOnCrowi = {
   Page,
   PageTagRelation: require('./page-tag-relation'),
   User: require('./user'),
-  Revision: require('./revision'),
   Bookmark: require('./bookmark'),
   GlobalNotificationSetting: GlobalNotificationSettingFactory,
   GlobalNotificationMailSetting: require('./GlobalNotificationSetting/GlobalNotificationMailSetting'),
@@ -17,6 +16,7 @@ export const modelsDependsOnCrowi = {
 export * from './attachment';
 export * as Activity from './activity';
 export * as PageRedirect from './page-redirect';
+export * from './revision';
 export * as ShareLink from './share-link';
 export * as Tag from './tag';
 export * as UserGroup from './user-group';

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

@@ -67,7 +67,10 @@ export type CreateMethod = (path: string, body: string, user, options: IOptionsF
 
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete static methods
-  findByIdsAndViewer(pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean, includeAnyoneWithTheLink?: boolean): Promise<PageDocument[]>
+  findByIdAndViewer(pageId: ObjectIdLike, user, userGroups?, includeEmpty?: boolean): Promise<PageDocument & HasObjectId>
+  findByIdsAndViewer(
+    pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean, includeAnyoneWithTheLink?: boolean,
+  ): Promise<(PageDocument & HasObjectId)[]>
   findByPath(path: string, includeEmpty?: boolean): Promise<PageDocument | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: true, includeEmpty?: boolean): Promise<PageDocument & HasObjectId | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: false, includeEmpty?: boolean): Promise<(PageDocument & HasObjectId)[]>

Некоторые файлы не были показаны из-за большого количества измененных файлов