Przeglądaj źródła

Merge branch 'master' into support/156162-172282-app-services-dir-biome

Yuki Takei 6 miesięcy temu
rodzic
commit
ec28b87ec3
34 zmienionych plików z 144 dodań i 101 usunięć
  1. 4 0
      apps/app/.eslintrc.js
  2. 40 40
      apps/app/public/images/icons/favicon/manifest.json
  3. 1 1
      apps/app/public/static/locales/en_US/admin.json
  4. 1 1
      apps/app/public/static/locales/en_US/translation.json
  5. 1 1
      apps/app/public/static/locales/fr_FR/admin.json
  6. 1 1
      apps/app/public/static/locales/fr_FR/translation.json
  7. 1 1
      apps/app/public/static/locales/ja_JP/admin.json
  8. 1 1
      apps/app/public/static/locales/ja_JP/translation.json
  9. 1 1
      apps/app/public/static/locales/ko_KR/admin.json
  10. 1 1
      apps/app/public/static/locales/ko_KR/translation.json
  11. 1 1
      apps/app/public/static/locales/zh_CN/admin.json
  12. 1 1
      apps/app/public/static/locales/zh_CN/translation.json
  13. 3 0
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  14. 2 0
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  15. 2 0
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  16. 5 3
      apps/app/src/models/admin/growi-archive-import-option.ts
  17. 1 1
      apps/app/src/models/admin/import-mode.ts
  18. 8 4
      apps/app/src/models/admin/import-option-for-pages.ts
  19. 5 3
      apps/app/src/models/admin/import-option-for-revisions.ts
  20. 0 2
      apps/app/src/models/cdn-resource.js
  21. 0 4
      apps/app/src/models/linked-page-path.js
  22. 8 4
      apps/app/src/models/serializers/in-app-notification-snapshot/page-bulk-export-job.ts
  23. 2 2
      apps/app/src/models/serializers/in-app-notification-snapshot/page.ts
  24. 1 1
      apps/app/src/models/serializers/in-app-notification-snapshot/user.ts
  25. 0 2
      apps/app/src/models/vo/external-account-login-error.ts
  26. 7 5
      apps/app/src/pages/[[...path]].page.tsx
  27. 3 0
      apps/app/src/server/routes/apiv3/page/update-page.ts
  28. 7 2
      apps/app/src/server/service/page/index.ts
  29. 26 8
      apps/app/src/stores/websocket.tsx
  30. 3 1
      apps/app/src/stores/yjs.ts
  31. 1 3
      apps/app/test-with-vite/.eslintrc.cjs
  32. 2 3
      apps/app/test-with-vite/setup/mongoms.ts
  33. 1 1
      apps/app/test-with-vite/tsconfig.json
  34. 3 2
      biome.json

+ 4 - 0
apps/app/.eslintrc.js

@@ -27,10 +27,14 @@ module.exports = {
     'test/integration/models/**',
     'test/integration/models/**',
     'test/integration/service/**',
     'test/integration/service/**',
     'test/integration/setup.js',
     'test/integration/setup.js',
+    'test-with-vite/**',
+    'public/**',
     'bin/**',
     'bin/**',
     'config/**',
     'config/**',
+    'src/styles/**',
     'src/linter-checker/**',
     'src/linter-checker/**',
     'src/migrations/**',
     'src/migrations/**',
+    'src/models/**',
     'src/features/callout/**',
     'src/features/callout/**',
     'src/features/comment/**',
     'src/features/comment/**',
     'src/features/templates/**',
     'src/features/templates/**',

+ 40 - 40
apps/app/public/images/icons/favicon/manifest.json

@@ -1,41 +1,41 @@
 {
 {
- "name": "App",
- "icons": [
-  {
-   "src": "\/android-icon-36x36.png",
-   "sizes": "36x36",
-   "type": "image\/png",
-   "density": "0.75"
-  },
-  {
-   "src": "\/android-icon-48x48.png",
-   "sizes": "48x48",
-   "type": "image\/png",
-   "density": "1.0"
-  },
-  {
-   "src": "\/android-icon-72x72.png",
-   "sizes": "72x72",
-   "type": "image\/png",
-   "density": "1.5"
-  },
-  {
-   "src": "\/android-icon-96x96.png",
-   "sizes": "96x96",
-   "type": "image\/png",
-   "density": "2.0"
-  },
-  {
-   "src": "\/android-icon-144x144.png",
-   "sizes": "144x144",
-   "type": "image\/png",
-   "density": "3.0"
-  },
-  {
-   "src": "\/android-icon-192x192.png",
-   "sizes": "192x192",
-   "type": "image\/png",
-   "density": "4.0"
-  }
- ]
-}
+  "name": "App",
+  "icons": [
+    {
+      "src": "\/android-icon-36x36.png",
+      "sizes": "36x36",
+      "type": "image\/png",
+      "density": "0.75"
+    },
+    {
+      "src": "\/android-icon-48x48.png",
+      "sizes": "48x48",
+      "type": "image\/png",
+      "density": "1.0"
+    },
+    {
+      "src": "\/android-icon-72x72.png",
+      "sizes": "72x72",
+      "type": "image\/png",
+      "density": "1.5"
+    },
+    {
+      "src": "\/android-icon-96x96.png",
+      "sizes": "96x96",
+      "type": "image\/png",
+      "density": "2.0"
+    },
+    {
+      "src": "\/android-icon-144x144.png",
+      "sizes": "144x144",
+      "type": "image\/png",
+      "density": "3.0"
+    },
+    {
+      "src": "\/android-icon-192x192.png",
+      "sizes": "192x192",
+      "type": "image\/png",
+      "density": "4.0"
+    }
+  ]
+}

+ 1 - 1
apps/app/public/static/locales/en_US/admin.json

@@ -1139,4 +1139,4 @@
     "disable_mode_explanation": "Currently, AI integration is disabled. To enable it, configure the <code>AI_ENABLED</code> environment variable along with the required additional variables.<br><br>For details, please refer to the <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "disable_mode_explanation": "Currently, AI integration is disabled. To enable it, configure the <code>AI_ENABLED</code> environment variable along with the required additional variables.<br><br>For details, please refer to the <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "ai_search_management": "AI search management"
     "ai_search_management": "AI search management"
   }
   }
-}
+}

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

@@ -1062,4 +1062,4 @@
     "skipped-toaster": "Skipped synchronizing since the editor is not activated. Please open the editor and try again.",
     "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"
     "error-toaster": "Synchronization of the latest text failed"
   }
   }
-}
+}

+ 1 - 1
apps/app/public/static/locales/fr_FR/admin.json

@@ -1138,4 +1138,4 @@
     "disable_mode_explanation": "Actuellement, l'intégration AI est désactivée. Pour l'activer, configurez la variable d'environnement <code>AI_ENABLED</code> ainsi que les autres variables nécessaires.<br><br>Pour plus de détails, veuillez consulter la <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "disable_mode_explanation": "Actuellement, l'intégration AI est désactivée. Pour l'activer, configurez la variable d'environnement <code>AI_ENABLED</code> ainsi que les autres variables nécessaires.<br><br>Pour plus de détails, veuillez consulter la <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>documentation</a>.",
     "ai_search_management": "Gestion de la recherche par l'IA"
     "ai_search_management": "Gestion de la recherche par l'IA"
   }
   }
-}
+}

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

@@ -1053,4 +1053,4 @@
     "skipped-toaster": "L'éditeur n'est pas actif. Synchronisation annulée.",
     "skipped-toaster": "L'éditeur n'est pas actif. Synchronisation annulée.",
     "error-toaster": "Synchronisation échouée"
     "error-toaster": "Synchronisation échouée"
   }
   }
-}
+}

+ 1 - 1
apps/app/public/static/locales/ja_JP/admin.json

@@ -1148,4 +1148,4 @@
     "disable_mode_explanation": "現在、AI 連携は無効になっています。有効にする場合は環境変数 <code>AI_ENABLED</code> の他、必要な環境変数を設定してください。<br><br>詳細は<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}ja/guide/features/ai-knowledge-assistant.html>ドキュメント</a>を参照してください。",
     "disable_mode_explanation": "現在、AI 連携は無効になっています。有効にする場合は環境変数 <code>AI_ENABLED</code> の他、必要な環境変数を設定してください。<br><br>詳細は<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}ja/guide/features/ai-knowledge-assistant.html>ドキュメント</a>を参照してください。",
     "ai_search_management": "AI 検索管理"
     "ai_search_management": "AI 検索管理"
   }
   }
-}
+}

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

@@ -1095,4 +1095,4 @@
     "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
     "skipped-toaster": "エディターがアクティブではないため、同期をスキップしました。エディターを開いて再度お試しください。",
     "error-toaster": "最新の本文の同期に失敗しました"
     "error-toaster": "最新の本文の同期に失敗しました"
   }
   }
-}
+}

+ 1 - 1
apps/app/public/static/locales/ko_KR/admin.json

@@ -1139,4 +1139,4 @@
     "disable_mode_explanation": "현재 AI 통합이 비활성화되어 있습니다. 활성화하려면 <code>AI_ENABLED</code> 환경 변수와 필요한 추가 변수를 구성하십시오.<br><br>자세한 내용은 <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>문서</a>를 참조하십시오.",
     "disable_mode_explanation": "현재 AI 통합이 비활성화되어 있습니다. 활성화하려면 <code>AI_ENABLED</code> 환경 변수와 필요한 추가 변수를 구성하십시오.<br><br>자세한 내용은 <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>문서</a>를 참조하십시오.",
     "ai_search_management": "AI 검색 관리"
     "ai_search_management": "AI 검색 관리"
   }
   }
-}
+}

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

@@ -1022,4 +1022,4 @@
     "skipped-toaster": "편집기가 활성화되지 않아 동기화 건너뜀. 편집기를 열고 다시 시도하십시오.",
     "skipped-toaster": "편집기가 활성화되지 않아 동기화 건너뜀. 편집기를 열고 다시 시도하십시오.",
     "error-toaster": "최신 텍스트 동기화 실패"
     "error-toaster": "최신 텍스트 동기화 실패"
   }
   }
-}
+}

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

@@ -1148,4 +1148,4 @@
     "disable_mode_explanation": "目前,AI 集成已被禁用。若要启用,请配置 <code>AI_ENABLED</code> 环境变量以及其他必要的变量。<br><br>详细信息请参考<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>文档</a>。",
     "disable_mode_explanation": "目前,AI 集成已被禁用。若要启用,请配置 <code>AI_ENABLED</code> 环境变量以及其他必要的变量。<br><br>详细信息请参考<a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>文档</a>。",
     "ai_search_management": "AI 搜索管理"
     "ai_search_management": "AI 搜索管理"
   }
   }
-}
+}

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

@@ -1067,4 +1067,4 @@
     "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
     "skipped-toaster": "由于编辑器未激活,因此跳过同步。 请打开编辑器并重试。",
     "error-toaster": "同步最新文本失败"
     "error-toaster": "同步最新文本失败"
   }
   }
-}
+}

+ 3 - 0
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -128,6 +128,9 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
   mutateResolvedTheme({ themeData: resolvedTheme });
   mutateResolvedTheme({ themeData: resolvedTheme });
 
 
   const currentRevisionId = currentPage?.revision?._id;
   const currentRevisionId = currentPage?.revision?._id;
+
+  // There are cases where "revisionId" is not required for revision updates
+  // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
   const isRevisionIdRequiredForPageUpdate = currentPage?.revision?.origin === undefined;
   const isRevisionIdRequiredForPageUpdate = currentPage?.revision?.origin === undefined;
 
 
   const initialValueRef = useRef('');
   const initialValueRef = useRef('');

+ 2 - 0
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -46,6 +46,8 @@ export const useDrawioModalLauncherForView = (opts?: {
       return;
       return;
     }
     }
 
 
+    // There are cases where "revisionId" is not required for revision updates
+    // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
     try {
     try {
       await _updatePage({
       await _updatePage({
         pageId: currentPage._id,
         pageId: currentPage._id,

+ 2 - 0
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -47,6 +47,8 @@ export const useHandsontableModalLauncherForView = (opts?: {
     }
     }
 
 
     try {
     try {
+      // There are cases where "revisionId" is not required for revision updates
+      // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
       await _updatePage({
       await _updatePage({
         pageId: currentPage._id,
         pageId: currentPage._id,
         revisionId,
         revisionId,

+ 5 - 3
apps/app/src/models/admin/growi-archive-import-option.ts

@@ -1,12 +1,15 @@
 import { ImportMode } from './import-mode';
 import { ImportMode } from './import-mode';
 
 
 export class GrowiArchiveImportOption {
 export class GrowiArchiveImportOption {
-
   collectionName: string;
   collectionName: string;
 
 
   mode: ImportMode;
   mode: ImportMode;
 
 
-  constructor(collectionName: string, mode: ImportMode = ImportMode.insert, initProps = {}) {
+  constructor(
+    collectionName: string,
+    mode: ImportMode = ImportMode.insert,
+    initProps = {},
+  ) {
     this.collectionName = collectionName;
     this.collectionName = collectionName;
     this.mode = mode;
     this.mode = mode;
 
 
@@ -14,5 +17,4 @@ export class GrowiArchiveImportOption {
       this[key] = value;
       this[key] = value;
     });
     });
   }
   }
-
 }
 }

+ 1 - 1
apps/app/src/models/admin/import-mode.ts

@@ -3,4 +3,4 @@ export const ImportMode = {
   upsert: 'upsert',
   upsert: 'upsert',
   flushAndInsert: 'flushAndInsert',
   flushAndInsert: 'flushAndInsert',
 } as const;
 } as const;
-export type ImportMode = typeof ImportMode[keyof typeof ImportMode];
+export type ImportMode = (typeof ImportMode)[keyof typeof ImportMode];

+ 8 - 4
apps/app/src/models/admin/import-option-for-pages.ts

@@ -11,7 +11,6 @@ const DEFAULT_PROPS = {
 };
 };
 
 
 export class ImportOptionForPages extends GrowiArchiveImportOption {
 export class ImportOptionForPages extends GrowiArchiveImportOption {
-
   isOverwriteAuthorWithCurrentUser;
   isOverwriteAuthorWithCurrentUser;
 
 
   makePublicForGrant2;
   makePublicForGrant2;
@@ -22,12 +21,17 @@ export class ImportOptionForPages extends GrowiArchiveImportOption {
 
 
   initPageMetadatas;
   initPageMetadatas;
 
 
-  constructor(collectionName: string, mode: ImportMode = ImportMode.insert, initProps = DEFAULT_PROPS) {
+  constructor(
+    collectionName: string,
+    mode: ImportMode = ImportMode.insert,
+    initProps = DEFAULT_PROPS,
+  ) {
     super(collectionName, mode, initProps);
     super(collectionName, mode, initProps);
   }
   }
-
 }
 }
 
 
-export const isImportOptionForPages = (opt: GrowiArchiveImportOption): opt is ImportOptionForPages => {
+export const isImportOptionForPages = (
+  opt: GrowiArchiveImportOption,
+): opt is ImportOptionForPages => {
   return 'isOverwriteAuthorWithCurrentUser' in opt;
   return 'isOverwriteAuthorWithCurrentUser' in opt;
 };
 };

+ 5 - 3
apps/app/src/models/admin/import-option-for-revisions.ts

@@ -7,9 +7,11 @@ const DEFAULT_PROPS = {
 };
 };
 
 
 export class ImportOptionForRevisions extends GrowiArchiveImportOption {
 export class ImportOptionForRevisions extends GrowiArchiveImportOption {
-
-  constructor(collectionName: string, mode: ImportMode = ImportMode.insert, initProps = DEFAULT_PROPS) {
+  constructor(
+    collectionName: string,
+    mode: ImportMode = ImportMode.insert,
+    initProps = DEFAULT_PROPS,
+  ) {
     super(collectionName, mode, initProps);
     super(collectionName, mode, initProps);
   }
   }
-
 }
 }

+ 0 - 2
apps/app/src/models/cdn-resource.js

@@ -2,13 +2,11 @@
  * Value Object
  * Value Object
  */
  */
 class CdnResource {
 class CdnResource {
-
   constructor(name, url, outDir) {
   constructor(name, url, outDir) {
     this.name = name;
     this.name = name;
     this.url = url;
     this.url = url;
     this.outDir = outDir;
     this.outDir = outDir;
   }
   }
-
 }
 }
 
 
 module.exports = CdnResource;
 module.exports = CdnResource;

+ 0 - 4
apps/app/src/models/linked-page-path.js

@@ -7,9 +7,7 @@ const { isTrashPage } = pagePathUtils;
  * Linked Array Structured PagePath Model
  * Linked Array Structured PagePath Model
  */
  */
 export default class LinkedPagePath {
 export default class LinkedPagePath {
-
   constructor(path) {
   constructor(path) {
-
     const pagePath = new DevidedPagePath(path);
     const pagePath = new DevidedPagePath(path);
 
 
     this.path = path;
     this.path = path;
@@ -18,7 +16,6 @@ export default class LinkedPagePath {
     this.parent = pagePath.isRoot
     this.parent = pagePath.isRoot
       ? null
       ? null
       : new LinkedPagePath(pagePath.former, true);
       : new LinkedPagePath(pagePath.former, true);
-
   }
   }
 
 
   get href() {
   get href() {
@@ -32,5 +29,4 @@ export default class LinkedPagePath {
   get isInTrash() {
   get isInTrash() {
     return isTrashPage(this.path);
     return isTrashPage(this.path);
   }
   }
-
 }
 }

+ 8 - 4
apps/app/src/models/serializers/in-app-notification-snapshot/page-bulk-export-job.ts

@@ -1,17 +1,21 @@
-import { isPopulated } from '@growi/core';
 import type { IPage } from '@growi/core';
 import type { IPage } from '@growi/core';
+import { isPopulated } from '@growi/core';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import type { PageModel } from '~/server/models/page';
 import type { PageModel } from '~/server/models/page';
 
 
 export interface IPageBulkExportJobSnapshot {
 export interface IPageBulkExportJobSnapshot {
-  path: string
+  path: string;
 }
 }
 
 
-export const stringifySnapshot = async(exportJob: IPageBulkExportJob): Promise<string | undefined> => {
+export const stringifySnapshot = async (
+  exportJob: IPageBulkExportJob,
+): Promise<string | undefined> => {
   const Page = mongoose.model<IPage, PageModel>('Page');
   const Page = mongoose.model<IPage, PageModel>('Page');
-  const page = isPopulated(exportJob.page) ? exportJob.page : (await Page.findById(exportJob.page));
+  const page = isPopulated(exportJob.page)
+    ? exportJob.page
+    : await Page.findById(exportJob.page);
 
 
   if (page != null) {
   if (page != null) {
     return JSON.stringify({
     return JSON.stringify({

+ 2 - 2
apps/app/src/models/serializers/in-app-notification-snapshot/page.ts

@@ -1,8 +1,8 @@
 import type { IPage, IUser } from '@growi/core';
 import type { IPage, IUser } from '@growi/core';
 
 
 export interface IPageSnapshot {
 export interface IPageSnapshot {
-  path: string
-  creator: IUser
+  path: string;
+  creator: IUser;
 }
 }
 
 
 export const stringifySnapshot = (page: IPage): string => {
 export const stringifySnapshot = (page: IPage): string => {

+ 1 - 1
apps/app/src/models/serializers/in-app-notification-snapshot/user.ts

@@ -1,7 +1,7 @@
 import type { IUser } from '@growi/core';
 import type { IUser } from '@growi/core';
 
 
 export interface IUserSnapshot {
 export interface IUserSnapshot {
-  username: string
+  username: string;
 }
 }
 
 
 export const stringifySnapshot = (user: IUser): string => {
 export const stringifySnapshot = (user: IUser): string => {

+ 0 - 2
apps/app/src/models/vo/external-account-login-error.ts

@@ -1,5 +1,4 @@
 export class ExternalAccountLoginError extends Error {
 export class ExternalAccountLoginError extends Error {
-
   args?: any;
   args?: any;
 
 
   constructor(message = '', args = undefined) {
   constructor(message = '', args = undefined) {
@@ -7,5 +6,4 @@ export class ExternalAccountLoginError extends Error {
     this.message = message;
     this.message = message;
     this.args = args;
     this.args = args;
   }
   }
-
 }
 }

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

@@ -47,7 +47,7 @@ import {
   useIsLocalAccountRegistrationEnabled,
   useIsLocalAccountRegistrationEnabled,
   useIsRomUserAllowedToComment,
   useIsRomUserAllowedToComment,
   useIsPdfBulkExportEnabled,
   useIsPdfBulkExportEnabled,
-  useIsAiEnabled, useLimitLearnablePageCountPerAssistant, useIsUsersHomepageDeletionEnabled,
+  useIsAiEnabled, useLimitLearnablePageCountPerAssistant, useIsUsersHomepageDeletionEnabled, useIsGuestUser,
 } from '~/stores-universal/context';
 } from '~/stores-universal/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import {
 import {
@@ -276,6 +276,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
 
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
   const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
+  const { data: isGuestUser } = useIsGuestUser();
 
 
   const { mutate: mutateIsNotFound } = useIsNotFound();
   const { mutate: mutateIsNotFound } = useIsNotFound();
 
 
@@ -308,16 +309,17 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
       mutatePageData();
       mutatePageData();
     }
     }
   }, [
   }, [
-    revisionId, currentPageId, mutateCurrentPage,
-    mutateCurrentPageYjsDataFromApi, mutateEditingMarkdown, props.isNotFound, props.skipSSR,
+    revisionId, currentPageId,
+    mutateCurrentPage, mutateEditingMarkdown,
+    props.isNotFound, props.skipSSR,
   ]);
   ]);
 
 
   // Load current yjs data
   // Load current yjs data
   useEffect(() => {
   useEffect(() => {
-    if (currentPageId != null && revisionId != null && !props.isNotFound) {
+    if (!isGuestUser && currentPageId != null && revisionId != null && mutateCurrentPageYjsDataFromApi != null && !props.isNotFound) {
       mutateCurrentPageYjsDataFromApi();
       mutateCurrentPageYjsDataFromApi();
     }
     }
-  }, [currentPageId, mutateCurrentPageYjsDataFromApi, props.isNotFound, revisionId]);
+  }, [isGuestUser, currentPageId, mutateCurrentPageYjsDataFromApi, props.isNotFound, revisionId]);
 
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
   useEffect(() => {

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

@@ -196,6 +196,9 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
           options.userRelatedGrantUserGroupIds = userRelatedGrantUserGroupIds;
           options.userRelatedGrantUserGroupIds = userRelatedGrantUserGroupIds;
         }
         }
         previousRevision = await Revision.findById(sanitizeRevisionId);
         previousRevision = await Revision.findById(sanitizeRevisionId);
+
+        // There are cases where "revisionId" is not required for revision updates
+        // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
         updatedPage = await crowi.pageService.updatePage(currentPage, body, previousRevision?.body ?? null, req.user, options);
         updatedPage = await crowi.pageService.updatePage(currentPage, body, previousRevision?.body ?? null, req.user, options);
       }
       }
       catch (err) {
       catch (err) {

+ 7 - 2
apps/app/src/server/service/page/index.ts

@@ -4122,6 +4122,9 @@ class PageService implements IPageService {
     return [...userUnrelatedPreviousGrantedGroups, ...userRelatedGrantedGroups];
     return [...userUnrelatedPreviousGrantedGroups, ...userRelatedGrantedGroups];
   }
   }
 
 
+
+  // There are cases where "revisionId" is not required for revision updates
+  // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
   async updatePage(
   async updatePage(
       pageData: HydratedDocument<PageDocument>,
       pageData: HydratedDocument<PageDocument>,
       body: string | null,
       body: string | null,
@@ -4271,6 +4274,8 @@ class PageService implements IPageService {
   }
   }
 
 
 
 
+  // There are cases where "revisionId" is not required for revision updates
+  // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
   async updatePageV4(
   async updatePageV4(
       pageData: HydratedDocument<PageDocument>, body, previousBody, user, options: IOptionsForUpdate = {},
       pageData: HydratedDocument<PageDocument>, body, previousBody, user, options: IOptionsForUpdate = {},
   ): Promise<HydratedDocument<PageDocument>> {
   ): Promise<HydratedDocument<PageDocument>> {
@@ -4291,10 +4296,10 @@ class PageService implements IPageService {
     let savedPage = await pageData.save();
     let savedPage = await pageData.save();
 
 
     // Update revision
     // Update revision
-    const isBodyPresent = body != null && previousBody != null;
+    const isBodyPresent = body != null;
     const shouldUpdateBody = isBodyPresent;
     const shouldUpdateBody = isBodyPresent;
     if (shouldUpdateBody) {
     if (shouldUpdateBody) {
-      const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
+      const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user, options.origin);
       savedPage = await pushRevision(savedPage, newRevision, user);
       savedPage = await pushRevision(savedPage, newRevision, user);
       await savedPage.populateDataToShowRevision();
       await savedPage.populateDataToShowRevision();
     }
     }

+ 26 - 8
apps/app/src/stores/websocket.tsx

@@ -1,12 +1,13 @@
 import { useEffect } from 'react';
 import { useEffect } from 'react';
 
 
 import {
 import {
-  useGlobalSocket, GLOBAL_SOCKET_KEY, GLOBAL_SOCKET_NS, useSWRStatic,
+  useGlobalSocket, GLOBAL_SOCKET_NS, useSWRStatic,
 } from '@growi/core/dist/swr';
 } from '@growi/core/dist/swr';
 import type { Socket } from 'socket.io-client';
 import type { Socket } from 'socket.io-client';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 
 
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
+import { useIsGuestUser } from '~/stores-universal/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:stores:ui');
 const logger = loggerFactory('growi:stores:ui');
@@ -19,25 +20,42 @@ export const GLOBAL_ADMIN_SOCKET_KEY = 'globalAdminSocket';
  */
  */
 export const useSetupGlobalSocket = (): void => {
 export const useSetupGlobalSocket = (): void => {
 
 
-  const { data, mutate } = useSWRStatic(GLOBAL_SOCKET_KEY);
+  const { data: socket, mutate } = useGlobalSocket();
+  const { data: isGuestUser } = useIsGuestUser();
 
 
   useEffect(() => {
   useEffect(() => {
-    if (data != null) {
+    // Skip Socket.IO connection for guest users (not logged in)
+    // Guest users don't need real-time updates as they can only read pages
+    if (isGuestUser) {
+      logger.debug('Socket.IO connection skipped for guest user');
+      return;
+    }
+
+    if (socket != null) {
       return;
       return;
     }
     }
 
 
     mutate(async() => {
     mutate(async() => {
       const { io } = await import('socket.io-client');
       const { io } = await import('socket.io-client');
-      const socket = io(GLOBAL_SOCKET_NS, {
+      const newSocket = io(GLOBAL_SOCKET_NS, {
         transports: ['websocket'],
         transports: ['websocket'],
       });
       });
 
 
-      socket.on('error', (err) => { logger.error(err) });
-      socket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
+      newSocket.on('error', (err) => { logger.error(err) });
+      newSocket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
 
 
-      return socket;
+      return newSocket;
     });
     });
-  }, [data, mutate]);
+
+    // Cleanup function to disconnect socket when component unmounts or user logs out
+    return () => {
+      if (socket != null && typeof socket === 'object' && 'disconnect' in socket) {
+        logger.debug('Disconnecting Socket.IO connection');
+        (socket as Socket).disconnect();
+        mutate(undefined, false); // Clear the SWR cache without revalidation
+      }
+    };
+  }, [socket, isGuestUser, mutate]);
 };
 };
 
 
 // comment out for porduction build error: https://github.com/growilabs/growi/pull/7131
 // comment out for porduction build error: https://github.com/growilabs/growi/pull/7131

+ 3 - 1
apps/app/src/stores/yjs.ts

@@ -6,6 +6,7 @@ import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import type { CurrentPageYjsData } from '~/interfaces/yjs';
 import type { CurrentPageYjsData } from '~/interfaces/yjs';
+import { useIsGuestUser } from '~/stores-universal/context';
 
 
 import { useCurrentPageId } from './page';
 import { useCurrentPageId } from './page';
 
 
@@ -16,8 +17,9 @@ type CurrentPageYjsDataUtils = {
 
 
 export const useCurrentPageYjsData = (): SWRResponse<CurrentPageYjsData, Error> & CurrentPageYjsDataUtils => {
 export const useCurrentPageYjsData = (): SWRResponse<CurrentPageYjsData, Error> & CurrentPageYjsDataUtils => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
+  const { data: isGuestUser } = useIsGuestUser();
 
 
-  const key = currentPageId != null
+  const key = !isGuestUser && currentPageId != null
     ? `/page/${currentPageId}/yjs-data`
     ? `/page/${currentPageId}/yjs-data`
     : null;
     : null;
 
 

+ 1 - 3
apps/app/test-with-vite/.eslintrc.cjs

@@ -1,5 +1,3 @@
 module.exports = {
 module.exports = {
-  extends: [
-    'plugin:vitest/recommended',
-  ],
+  extends: ['plugin:vitest/recommended'],
 };
 };

+ 2 - 3
apps/app/test-with-vite/setup/mongoms.ts

@@ -3,8 +3,7 @@ import mongoose from 'mongoose';
 
 
 import { mongoOptions } from '~/server/util/mongoose-utils';
 import { mongoOptions } from '~/server/util/mongoose-utils';
 
 
-
-beforeAll(async() => {
+beforeAll(async () => {
   // set debug flag
   // set debug flag
   process.env.MONGOMS_DEBUG = process.env.VITE_MONGOMS_DEBUG;
   process.env.MONGOMS_DEBUG = process.env.VITE_MONGOMS_DEBUG;
 
 
@@ -25,6 +24,6 @@ beforeAll(async() => {
   await mongoose.connect(mongoServer.getUri(), mongoOptions);
   await mongoose.connect(mongoServer.getUri(), mongoOptions);
 });
 });
 
 
-afterAll(async() => {
+afterAll(async () => {
   await mongoose.disconnect();
   await mongoose.disconnect();
 });
 });

+ 1 - 1
apps/app/test-with-vite/tsconfig.json

@@ -5,7 +5,7 @@
     "baseUrl": "../",
     "baseUrl": "../",
     "paths": {
     "paths": {
       "~/*": ["./src/*"],
       "~/*": ["./src/*"],
-      "^/*": ["./*"],
+      "^/*": ["./*"]
     }
     }
   }
   }
 }
 }

+ 3 - 2
biome.json

@@ -22,18 +22,19 @@
       "!**/.eslintrc.js",
       "!**/.eslintrc.js",
       "!**/.stylelintrc.json",
       "!**/.stylelintrc.json",
       "!**/package.json",
       "!**/package.json",
+      "!apps/app/src/styles/prebuilt/**",
+      "!apps/app/tmp/**",
       "!apps/slackbot-proxy/src/public/bootstrap/**",
       "!apps/slackbot-proxy/src/public/bootstrap/**",
       "!packages/editor/**",
       "!packages/editor/**",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs/**",
       "!packages/pdf-converter-client/specs/**",
       "!apps/app/playwright/**",
       "!apps/app/playwright/**",
-      "!apps/app/public/**",
       "!apps/app/src/client/**",
       "!apps/app/src/client/**",
       "!apps/app/src/components/**",
       "!apps/app/src/components/**",
       "!apps/app/src/features/openai/**",
       "!apps/app/src/features/openai/**",
-      "!apps/app/src/models/**",
       "!apps/app/src/pages/**",
       "!apps/app/src/pages/**",
       "!apps/app/src/server/**",
       "!apps/app/src/server/**",
+      "!apps/app/src/services/**",
       "!apps/app/src/stores/**",
       "!apps/app/src/stores/**",
       "!apps/app/src/styles/**",
       "!apps/app/src/styles/**",
       "!apps/app/test-with-vite/**",
       "!apps/app/test-with-vite/**",