Преглед изворни кода

Merge branch 'master' into support/use-jotai

Yuki Takei пре 7 месеци
родитељ
комит
51fcf2118e
41 измењених фајлова са 390 додато и 285 уклоњено
  1. 1 1
      .serena/memories/task_completion_checklist.md
  2. 14 1
      CHANGELOG.md
  3. 3 1
      apps/app/.env.development
  4. 1 1
      apps/app/.env.production
  5. 2 0
      apps/app/.eslintrc.js
  6. 2 1
      apps/app/bin/openapi/definition-apiv1.js
  7. 3 11
      apps/app/bin/openapi/definition-apiv3.js
  8. 14 10
      apps/app/bin/openapi/generate-operation-ids/cli.spec.ts
  9. 5 4
      apps/app/bin/openapi/generate-operation-ids/cli.ts
  10. 29 31
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts
  11. 42 16
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts
  12. 8 2
      apps/app/config/cdn.js
  13. 2 2
      apps/app/config/migrate-mongo-config.js
  14. 4 8
      apps/app/config/migrate-mongo-config.spec.ts
  15. 6 7
      apps/app/config/next-i18next.config.js
  16. 4 4
      apps/app/package.json
  17. 6 8
      apps/app/resource/Contributor.js
  18. 4 7
      apps/app/resource/search/mappings-es7.ts
  19. 4 7
      apps/app/resource/search/mappings-es8.ts
  20. 5 11
      apps/app/resource/search/mappings-es9-for-ci.ts
  21. 4 7
      apps/app/resource/search/mappings-es9.ts
  22. 24 19
      apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx
  23. 27 18
      apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.tsx
  24. 1 0
      apps/app/src/client/components/PageHeader/PageTitleHeader.tsx
  25. 12 2
      apps/app/src/components/Common/PagePathNav/PagePathNavLayout.tsx
  26. 13 1
      apps/app/src/components/Layout/Admin.module.scss
  27. 60 6
      apps/app/src/features/opentelemetry/server/node-sdk.spec.ts
  28. 3 2
      apps/app/src/features/opentelemetry/server/node-sdk.ts
  29. 5 1
      apps/app/src/server/middlewares/access-token-parser/access-token.ts
  30. 3 13
      apps/app/src/server/middlewares/access-token-parser/api-token.ts
  31. 11 0
      apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts
  32. 0 1
      apps/app/src/server/middlewares/access-token-parser/index.ts
  33. 39 27
      apps/app/src/server/routes/apiv3/page/index.ts
  34. 16 17
      apps/app/src/server/routes/apiv3/share-links.js
  35. 2 2
      apps/app/src/server/routes/apiv3/users.js
  36. 1 1
      apps/app/src/server/service/config-manager/config-definition.ts
  37. 0 5
      apps/app/test-with-vite/download-mongo-binary/index.spec.ts
  38. 0 16
      apps/app/test-with-vite/download-mongo-binary/vitest.config.ts
  39. 1 6
      biome.json
  40. 1 0
      packages/core/src/consts/system.ts
  41. 8 8
      pnpm-lock.yaml

+ 1 - 1
.serena/memories/task_completion_checklist.md

@@ -26,7 +26,7 @@ pnpm run test
 
 # 個別実行
 pnpm run test:jest        # Jest(廃止予定)
-vitest run --coverage     # Vitestカバレッジ付き
+pnpm run test:vitest {target-file-name}     # Vitest
 ```
 
 ### 3. E2Eテストの実行(重要な機能変更時)

+ 14 - 1
CHANGELOG.md

@@ -1,9 +1,22 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.2.8...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.2.9...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.2.9](https://github.com/weseek/growi/compare/v7.2.8...v7.2.9) - 2025-07-01
+
+### 🚀 Improvement
+
+* imprv(ai): Assistant instructions (#10129) @yuki-takei
+* imprv: OpenTelemetry phase 2 (#10095) @yuki-takei
+* imprv: Adjust margin-top for .main at md and lg breakpoints (#10131) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Sharelink expiration date parsing when the date is cleared by the calendar UI (#10132) @yuki-takei
+* fix: Cannot change file upload destination to "MongoDB (GridFS)" or "local" for dev/7.2.x (#10119) @miya
+
 ## [v7.2.8](https://github.com/weseek/growi/compare/v7.2.7...v7.2.8) - 2025-06-26
 
 ### 💎 Features

+ 3 - 1
apps/app/.env.development

@@ -30,7 +30,9 @@ OGP_URI="http://ogp:8088"
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 
-# OpenTelemetry Official Configuration
+SERVICE_TYPE=dev
+
+# OpenTelemetry Official Configuration for dev
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 OPENTELEMETRY_ENABLED=false
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

+ 1 - 1
apps/app/.env.production

@@ -7,6 +7,6 @@ MIGRATIONS_DIR=dist/migrations/
 
 # OpenTelemetry Official Configuration
 # Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
-OTEL_TRACES_SAMPLER_ARG=0.1
+OTEL_TRACES_SAMPLER_ARG=0.01
 OTEL_METRIC_EXPORT_INTERVAL=300000
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"

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

@@ -26,6 +26,8 @@ module.exports = {
     'test/integration/migrations/**',
     'test/integration/models/**',
     'test/integration/setup.js',
+    'bin/**',
+    'config/**',
   ],
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript

+ 2 - 1
apps/app/bin/openapi/definition-apiv1.js

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
         server: {
           default: 'https://demo.growi.org',
-          description: 'The base URL for the GROWI API except for the version path (/_api). This can be set to your GROWI instance URL.',
+          description:
+            'The base URL for the GROWI API except for the version path (/_api). This can be set to your GROWI instance URL.',
         },
       },
     },

+ 3 - 11
apps/app/bin/openapi/definition-apiv3.js

@@ -12,7 +12,8 @@ module.exports = {
       variables: {
         server: {
           default: 'https://demo.growi.org',
-          description: 'The base URL for the GROWI API except for the version path (/_api/v3). This can be set to your GROWI instance URL.',
+          description:
+            'The base URL for the GROWI API except for the version path (/_api/v3). This can be set to your GROWI instance URL.',
         },
       },
     },
@@ -115,16 +116,7 @@ module.exports = {
     },
     {
       name: 'Public API',
-      tags: [
-        'Healthcheck',
-        'Statistics',
-        '',
-        '',
-        '',
-        '',
-        '',
-        '',
-      ],
+      tags: ['Healthcheck', 'Statistics', '', '', '', '', '', ''],
     },
   ],
 };

+ 14 - 10
apps/app/bin/openapi/generate-operation-ids/cli.spec.ts

@@ -1,8 +1,6 @@
 import { writeFileSync } from 'fs';
 
-import {
-  beforeEach, describe, expect, it, vi,
-} from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
 
 import { generateOperationIds } from './generate-operation-ids';
 
@@ -23,7 +21,7 @@ describe('cli', () => {
     vi.spyOn(console, 'error').mockImplementation(() => {});
   });
 
-  it('processes input file and writes output to specified file', async() => {
+  it('processes input file and writes output to specified file', async () => {
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
@@ -35,13 +33,15 @@ describe('cli', () => {
     await cliModule.main();
 
     // Verify generateOperationIds was called with correct arguments
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: undefined,
+    });
 
     // Verify writeFileSync was called with correct arguments
     expect(writeFileSync).toHaveBeenCalledWith('output.json', mockJsonStrings);
   });
 
-  it('uses input file as output when no output file is specified', async() => {
+  it('uses input file as output when no output file is specified', async () => {
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
@@ -53,13 +53,15 @@ describe('cli', () => {
     await cliModule.main();
 
     // Verify generateOperationIds was called with correct arguments
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: undefined });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: undefined,
+    });
 
     // Verify writeFileSync was called with input file as output
     expect(writeFileSync).toHaveBeenCalledWith('input.json', mockJsonStrings);
   });
 
-  it('handles overwrite-existing option correctly', async() => {
+  it('handles overwrite-existing option correctly', async () => {
     // Mock generateOperationIds to return success
     vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
 
@@ -71,10 +73,12 @@ describe('cli', () => {
     await cliModule.main();
 
     // Verify generateOperationIds was called with overwriteExisting option
-    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: true });
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', {
+      overwriteExisting: true,
+    });
   });
 
-  it('handles generateOperationIds error correctly', async() => {
+  it('handles generateOperationIds error correctly', async () => {
     // Mock generateOperationIds to throw error
     const error = new Error('Test error');
     vi.mocked(generateOperationIds).mockRejectedValue(error);

+ 5 - 4
apps/app/bin/openapi/generate-operation-ids/cli.ts

@@ -1,10 +1,9 @@
-import { writeFileSync } from 'fs';
-
 import { Command } from 'commander';
+import { writeFileSync } from 'fs';
 
 import { generateOperationIds } from './generate-operation-ids';
 
-export const main = async(): Promise<void> => {
+export const main = async (): Promise<void> => {
   // parse command line arguments
   const program = new Command();
   program
@@ -18,7 +17,9 @@ export const main = async(): Promise<void> => {
   const [inputFile] = program.args;
 
   // eslint-disable-next-line no-console
-  const jsonStrings = await generateOperationIds(inputFile, { overwriteExisting }).catch(console.error);
+  const jsonStrings = await generateOperationIds(inputFile, {
+    overwriteExisting,
+  }).catch(console.error);
   if (jsonStrings != null) {
     writeFileSync(outputFile ?? inputFile, jsonStrings);
   }

+ 29 - 31
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts

@@ -1,13 +1,11 @@
 import fs from 'fs/promises';
+import type { OpenAPI3 } from 'openapi-typescript';
 import { tmpdir } from 'os';
 import path from 'path';
-
-import type { OpenAPI3 } from 'openapi-typescript';
 import { describe, expect, it } from 'vitest';
 
 import { generateOperationIds } from './generate-operation-ids';
 
-
 async function createTempOpenAPIFile(spec: OpenAPI3): Promise<string> {
   const tempDir = await fs.mkdtemp(path.join(tmpdir(), 'openapi-test-'));
   const filePath = path.join(tempDir, 'openapi.json');
@@ -19,15 +17,14 @@ async function cleanup(filePath: string): Promise<void> {
   try {
     await fs.unlink(filePath);
     await fs.rmdir(path.dirname(filePath));
-  }
-  catch (err) {
+  } catch (err) {
     // eslint-disable-next-line no-console
     console.error('Cleanup failed:', err);
   }
 }
 
 describe('generateOperationIds', () => {
-  it('should generate correct operationId for simple paths', async() => {
+  it('should generate correct operationId for simple paths', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -46,13 +43,12 @@ describe('generateOperationIds', () => {
 
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
       expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate correct operationId for paths with parameters', async() => {
+  it('should generate correct operationId for paths with parameters', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -72,14 +68,15 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo/{id}'].get.operationId).toBe('getFooById');
-      expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe('getBarByPageByIdForFoo');
-    }
-    finally {
+      expect(parsed.paths['/foo/{id}/bar/{page}'].get.operationId).toBe(
+        'getBarByPageByIdForFoo',
+      );
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate correct operationId for nested resources', async() => {
+  it('should generate correct operationId for nested resources', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -96,13 +93,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should preserve existing operationId when overwriteExisting is false', async() => {
+  it('should preserve existing operationId when overwriteExisting is false', async () => {
     const existingOperationId = 'existingOperation';
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
@@ -118,17 +114,18 @@ describe('generateOperationIds', () => {
 
     const filePath = await createTempOpenAPIFile(spec);
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: false });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: false,
+      });
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should overwrite existing operationId when overwriteExisting is true', async() => {
+  it('should overwrite existing operationId when overwriteExisting is true', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -143,17 +140,18 @@ describe('generateOperationIds', () => {
 
     const filePath = await createTempOpenAPIFile(spec);
     try {
-      const result = await generateOperationIds(filePath, { overwriteExisting: true });
+      const result = await generateOperationIds(filePath, {
+        overwriteExisting: true,
+      });
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate correct operationId for root path', async() => {
+  it('should generate correct operationId for root path', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -170,13 +168,12 @@ describe('generateOperationIds', () => {
       const parsed = JSON.parse(result);
 
       expect(parsed.paths['/'].get.operationId).toBe('getRoot');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should generate operationId for all HTTP methods', async() => {
+  it('should generate operationId for all HTTP methods', async () => {
     const spec: OpenAPI3 = {
       openapi: '3.0.0',
       info: { title: 'Test API', version: '1.0.0' },
@@ -207,13 +204,14 @@ describe('generateOperationIds', () => {
       expect(parsed.paths['/foo'].options.operationId).toBe('optionsFoo');
       expect(parsed.paths['/foo'].head.operationId).toBe('headFoo');
       expect(parsed.paths['/foo'].trace.operationId).toBe('traceFoo');
-    }
-    finally {
+    } finally {
       await cleanup(filePath);
     }
   });
 
-  it('should throw error for non-existent file', async() => {
-    await expect(generateOperationIds('non-existent-file.json')).rejects.toThrow();
+  it('should throw error for non-existent file', async () => {
+    await expect(
+      generateOperationIds('non-existent-file.json'),
+    ).rejects.toThrow();
   });
 });

+ 42 - 16
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts

@@ -1,15 +1,25 @@
 import SwaggerParser from '@apidevtools/swagger-parser';
-import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
+import type {
+  OpenAPI3,
+  OperationObject,
+  PathItemObject,
+} from 'openapi-typescript';
 
-const toPascal = (s: string): string => s.split('-').map(w => w[0]?.toUpperCase() + w.slice(1)).join('');
+const toPascal = (s: string): string =>
+  s
+    .split('-')
+    .map((w) => w[0]?.toUpperCase() + w.slice(1))
+    .join('');
 
 const createParamSuffix = (params: string[]): string => {
   return params.length > 0
-    ? params.reverse().map(param => `By${toPascal(param.slice(1, -1))}`).join('')
+    ? params
+        .reverse()
+        .map((param) => `By${toPascal(param.slice(1, -1))}`)
+        .join('')
     : '';
 };
 
-
 /**
  * Generates a PascalCase operation name based on the HTTP method and path.
  *
@@ -24,8 +34,8 @@ const createParamSuffix = (params: string[]): string => {
  */
 function createOperationId(method: string, path: string): string {
   const segments = path.split('/').filter(Boolean);
-  const params = segments.filter(s => s.startsWith('{'));
-  const paths = segments.filter(s => !s.startsWith('{'));
+  const params = segments.filter((s) => s.startsWith('{'));
+  const paths = segments.filter((s) => !s.startsWith('{'));
 
   const paramSuffix = createParamSuffix(params);
 
@@ -37,19 +47,35 @@ function createOperationId(method: string, path: string): string {
   return `${method.toLowerCase()}${toPascal(resource)}${paramSuffix}For${context.reverse().map(toPascal).join('')}`;
 }
 
-export async function generateOperationIds(inputFile: string, opts?: { overwriteExisting: boolean }): Promise<string> {
-  const api = await SwaggerParser.parse(inputFile) as OpenAPI3;
+export async function generateOperationIds(
+  inputFile: string,
+  opts?: { overwriteExisting: boolean },
+): Promise<string> {
+  const api = (await SwaggerParser.parse(inputFile)) as OpenAPI3;
 
   Object.entries(api.paths || {}).forEach(([path, pathItem]) => {
     const item = pathItem as PathItemObject;
-    (['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'] as const)
-      .forEach((method) => {
-        const operation = item[method] as OperationObject | undefined;
-        if (operation == null || (operation.operationId != null && !opts?.overwriteExisting)) {
-          return;
-        }
-        operation.operationId = createOperationId(method, path);
-      });
+    (
+      [
+        'get',
+        'post',
+        'put',
+        'delete',
+        'patch',
+        'options',
+        'head',
+        'trace',
+      ] as const
+    ).forEach((method) => {
+      const operation = item[method] as OperationObject | undefined;
+      if (
+        operation == null ||
+        (operation.operationId != null && !opts?.overwriteExisting)
+      ) {
+        return;
+      }
+      operation.operationId = createOperationId(method, path);
+    });
   });
 
   const output = JSON.stringify(api, null, 2);

+ 8 - 2
apps/app/config/cdn.js

@@ -2,7 +2,13 @@ import path from 'path';
 
 import { projectRoot } from '~/utils/project-dir-utils';
 
-export const cdnLocalScriptRoot = path.join(projectRoot, 'public/static/js/cdn');
+export const cdnLocalScriptRoot = path.join(
+  projectRoot,
+  'public/static/js/cdn',
+);
 export const cdnLocalScriptWebRoot = '/static/js/cdn';
-export const cdnLocalStyleRoot = path.join(projectRoot, 'public/static/styles/cdn');
+export const cdnLocalStyleRoot = path.join(
+  projectRoot,
+  'public/static/styles/cdn',
+);
 export const cdnLocalStyleWebRoot = '/static/styles/cdn';

+ 2 - 2
apps/app/config/migrate-mongo-config.js

@@ -9,8 +9,8 @@ const isProduction = process.env.NODE_ENV === 'production';
 const { URL } = require('url');
 
 const { getMongoUri, mongoOptions } = isProduction
-  // eslint-disable-next-line import/extensions, import/no-unresolved
-  ? require('../dist/server/util/mongoose-utils')
+  ? // eslint-disable-next-line import/extensions, import/no-unresolved
+    require('../dist/server/util/mongoose-utils')
   : require('../src/server/util/mongoose-utils');
 
 // get migrationsDir from env var

+ 4 - 8
apps/app/config/migrate-mongo-config.spec.ts

@@ -2,11 +2,8 @@ import mockRequire from 'mock-require';
 
 const { reRequire } = mockRequire;
 
-
 describe('config/migrate-mongo-config.js', () => {
-
   test.concurrent('throws an error when MIGRATIONS_DIR is not set', () => {
-
     const getMongoUriMock = vi.fn();
     const mongoOptionsMock = vi.fn();
 
@@ -32,13 +29,11 @@ describe('config/migrate-mongo-config.js', () => {
     ${'mongodb://user:pass@example.com/growi'}        | ${'growi'}
     ${'mongodb://example.com/growi?replicaSet=mySet'} | ${'growi'}
   `('returns', ({ MONGO_URI, expectedDbName }) => {
-
-    beforeEach(async() => {
+    beforeEach(async () => {
       process.env.MIGRATIONS_DIR = 'testdir/migrations';
     });
 
     test(`when 'MONGO_URI' is '${MONGO_URI}`, () => {
-
       const getMongoUriMock = vi.fn(() => MONGO_URI);
       const mongoOptionsMock = vi.fn();
 
@@ -49,7 +44,9 @@ describe('config/migrate-mongo-config.js', () => {
       });
 
       // use reRequire to avoid using module cache
-      const { mongodb, migrationsDir, changelogCollectionName } = reRequire('./migrate-mongo-config');
+      const { mongodb, migrationsDir, changelogCollectionName } = reRequire(
+        './migrate-mongo-config',
+      );
 
       mockRequire.stop('../src/server/util/mongoose-utils');
 
@@ -61,5 +58,4 @@ describe('config/migrate-mongo-config.js', () => {
       expect(changelogCollectionName).toBe('migrations');
     });
   });
-
 });

+ 6 - 7
apps/app/config/next-i18next.config.js

@@ -26,17 +26,17 @@ module.exports = {
     ? isServer()
       ? [new HMRPlugin({ webpack: { server: true } })]
       : [
-        require('i18next-chained-backend').default,
-        new HMRPlugin({ webpack: { client: true } }),
-      ]
+          require('i18next-chained-backend').default,
+          new HMRPlugin({ webpack: { client: true } }),
+        ]
     : [],
   backend: {
     backends: isServer()
       ? []
       : [
-        require('i18next-localstorage-backend').default,
-        require('i18next-http-backend').default,
-      ],
+          require('i18next-localstorage-backend').default,
+          require('i18next-http-backend').default,
+        ],
     backendOptions: [
       // options for i18next-localstorage-backend
       { expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production
@@ -44,5 +44,4 @@ module.exports = {
       { loadPath: '/static/locales/{{lng}}/{{ns}}.json' },
     ],
   },
-
 };

+ 4 - 4
apps/app/package.json

@@ -35,12 +35,12 @@
     "lint": "run-p lint:**",
     "prelint:openapi:apiv3": "pnpm run openapi:generate-spec:apiv3",
     "prelint:openapi:apiv1": "pnpm run openapi:generate-spec:apiv1",
-    "test": "run-p test:*",
+    "test": "run-p test:jest test:vitest:coverage",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
-    "test:vitest": "vitest run --coverage",
+    "test:vitest": "vitest run",
+    "test:vitest:coverage": "COLUMNS=200 vitest run --coverage",
     "jest:run": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest --passWithNoTests -- ",
     "reg:run": "reg-suit run",
-    "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
     "console": "npm run repl",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
@@ -160,7 +160,7 @@
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-wiki-link": "^0.1.2",
-    "mermaid": "^11.9.0",
+    "mermaid": "^11.10.0",
     "method-override": "^3.0.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-wiki-link": "^0.0.4",

+ 6 - 8
apps/app/resource/Contributor.js

@@ -104,10 +104,7 @@ const contributors = [
       },
       {
         additionalClass: 'col-md-6 my-4',
-        members: [
-          { name: 'shaminmeerankutty' },
-          { name: 'rabitarochan' },
-        ],
+        members: [{ name: 'shaminmeerankutty' }, { name: 'rabitarochan' }],
       },
       {
         additionalClass: 'col-md-4 my-4',
@@ -150,7 +147,10 @@ const contributors = [
           { position: 'Flatt Security', name: 'stypr' },
           { position: 'Flatt Security', name: 'Azara/Norihide Saito' },
           { position: 'CyberAgent, Inc.', name: 'Daisuke Takahashi' },
-          { position: 'Mitsui Bussan Secure Directions, Inc.', name: 'Yuji Tounai' },
+          {
+            position: 'Mitsui Bussan Secure Directions, Inc.',
+            name: 'Yuji Tounai',
+          },
           { name: 'yy0931' },
         ],
       },
@@ -172,9 +172,7 @@ const contributors = [
       },
       {
         additionalClass: 'col-12 staff-credit-mt-10rem',
-        members: [
-          { name: 'AND YOU' },
-        ],
+        members: [{ name: 'AND YOU' }],
       },
     ],
   },

+ 4 - 7
apps/app/resource/search/mappings-es7.ts

@@ -4,15 +4,15 @@ import type { estypes } from '@elastic/elasticsearch7';
 type Mappings = {
   settings: NonNullable<estypes.IndicesCreateRequest['body']>['settings'];
   mappings: NonNullable<estypes.IndicesCreateRequest['body']>['mappings'];
-}
+};
 
 export const mappings: Mappings = {
   settings: {
     analysis: {
       filter: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
       },
       tokenizer: {
@@ -32,10 +32,7 @@ export const mappings: Mappings = {
         english_edge_ngram: {
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
       },
     },

+ 4 - 7
apps/app/resource/search/mappings-es8.ts

@@ -3,15 +3,15 @@ import type { estypes } from '@elastic/elasticsearch8';
 type Mappings = {
   settings: estypes.IndicesCreateRequest['settings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
-}
+};
 
 export const mappings: Mappings = {
   settings: {
     analysis: {
       filter: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
       },
       tokenizer: {
@@ -31,10 +31,7 @@ export const mappings: Mappings = {
         english_edge_ngram: {
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
       },
     },

+ 5 - 11
apps/app/resource/search/mappings-es9-for-ci.ts

@@ -3,15 +3,15 @@ import type { estypes } from '@elastic/elasticsearch9';
 type Mappings = {
   settings: estypes.IndicesCreateRequest['settings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
-}
+};
 
 export const mappings: Mappings = {
   settings: {
     analysis: {
       filter: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
       },
       tokenizer: {
@@ -26,18 +26,12 @@ export const mappings: Mappings = {
         japanese: {
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
         english_edge_ngram: {
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
       },
     },

+ 4 - 7
apps/app/resource/search/mappings-es9.ts

@@ -3,15 +3,15 @@ import type { estypes } from '@elastic/elasticsearch9';
 type Mappings = {
   settings: estypes.IndicesCreateRequest['settings'];
   mappings: estypes.IndicesCreateRequest['mappings'];
-}
+};
 
 export const mappings: Mappings = {
   settings: {
     analysis: {
       filter: {
         english_stop: {
-          type:       'stop',
-          stopwords:  '_english_',
+          type: 'stop',
+          stopwords: '_english_',
         },
       },
       tokenizer: {
@@ -31,10 +31,7 @@ export const mappings: Mappings = {
         english_edge_ngram: {
           type: 'custom',
           tokenizer: 'edge_ngram_tokenizer',
-          filter: [
-            'lowercase',
-            'english_stop',
-          ],
+          filter: ['lowercase', 'english_stop'],
         },
       },
     },

+ 24 - 19
apps/app/src/client/components/Admin/AuditLog/ActivityTable.tsx

@@ -11,9 +11,9 @@ import { Tooltip } from 'reactstrap';
 
 import type { IActivityHasId } from '~/interfaces/activity';
 
-type Props = {
-  activityList: IActivityHasId[]
-}
+ type Props = {
+   activityList: IActivityHasId[]
+ }
 
 const formatDate = (date: Date): string => {
   return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
@@ -21,17 +21,18 @@ const formatDate = (date: Date): string => {
 
 export const ActivityTable : FC<Props> = (props: Props) => {
   const { t } = useTranslation();
-  const [tooltopOpen, setTooltipOpen] = useState(false);
+  const [activeTooltipId, setActiveTooltipId] = useState<string | null>(null);
 
-  const showToolTip = useCallback(() => {
-    setTooltipOpen(true);
+
+  const showToolTip = useCallback((id: string) => {
+    setActiveTooltipId(id);
     setTimeout(() => {
-      setTooltipOpen(false);
+      setActiveTooltipId(null);
     }, 1000);
-  }, [setTooltipOpen]);
+  }, []);
 
   return (
-    <div className="table-responsive text-nowrap h-100">
+    <div className="table-responsive admin-audit-log">
       <table className="table table-default table-bordered table-user-list">
         <thead>
           <tr>
@@ -62,16 +63,20 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                 <td>{formatDate(activity.createdAt)}</td>
                 <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
                 <td>{activity.ip}</td>
-                <td>
-                  {activity.endpoint}
-                  <CopyToClipboard text={activity.endpoint} onCopy={showToolTip}>
-                    <button type="button" className="btn btn-outline-secondary border-0 pull-right" id="tooltipTarget">
-                      <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
-                    </button>
-                  </CopyToClipboard>
-                  <Tooltip placement="top" isOpen={tooltopOpen} fade={false} target="tooltipTarget">
-                    copied!
-                  </Tooltip>
+                <td className="audit-log-url-cell">
+                  <div className="d-flex align-items-center">
+                    <span className="flex-grow-1 text-truncate">
+                      {activity.endpoint}
+                    </span>
+                    <CopyToClipboard text={activity.endpoint} onCopy={() => showToolTip(activity._id)}>
+                      <button type="button" className="btn btn-outline-secondary border-0 ms-2" id={`tooltipTarget-${activity._id}`}>
+                        <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
+                      </button>
+                    </CopyToClipboard>
+                    <Tooltip placement="top" isOpen={activeTooltipId === activity._id} fade={false} target={`tooltipTarget-${activity._id}`}>
+                      copied!
+                    </Tooltip>
+                  </div>
                 </td>
               </tr>
             );

+ 27 - 18
apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.jsx → apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.tsx

@@ -1,10 +1,9 @@
 import React, {
-  useState, useMemo, useCallback,
+  useState, useMemo, useCallback, type ReactNode, type CSSProperties,
 } from 'react';
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -15,9 +14,26 @@ import styles from './CopyDropdown.module.scss';
 
 const { encodeSpaces } = pagePathUtils;
 
+interface DropdownItemContentsProps {
+  title: string;
+  contents: ReactNode;
+  className?: string;
+  style?: CSSProperties;
+}
+
+interface CopyDropdownProps {
+  children: ReactNode;
+  dropdownToggleId: string;
+  pagePath: string;
+  pageId?: string;
+  dropdownToggleClassName?: string;
+  dropdownMenuContainer?: string | HTMLElement | React.RefObject<HTMLElement>;
+  isShareLinkMode?: boolean;
+}
+
 /* eslint-disable react/prop-types */
-const DropdownItemContents = ({
-  title, contents, className, style,
+const DropdownItemContents: React.FC<DropdownItemContentsProps> = ({
+  title, contents, className = '', style,
 }) => (
   <>
     <div className="h6 mt-1 mb-2"><strong>{title}</strong></div>
@@ -27,7 +43,7 @@ const DropdownItemContents = ({
 /* eslint-enable react/prop-types */
 
 
-export const CopyDropdown = (props) => {
+export const CopyDropdown: React.FC<CopyDropdownProps> = (props) => {
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const [tooltipOpen, setTooltipOpen] = useState(false);
   const [isParamsAppended, setParamsAppended] = useState(!props.isShareLinkMode);
@@ -105,7 +121,10 @@ export const CopyDropdown = (props) => {
    */
   const { t } = useTranslation('commons');
   const {
-    dropdownToggleId, pageId, dropdownToggleClassName, children, isShareLinkMode,
+    dropdownToggleId, pageId,
+    dropdownToggleClassName,
+    dropdownMenuContainer,
+    children, isShareLinkMode,
   } = props;
 
   const customSwitchForParamsId = `customSwitchForParams_${dropdownToggleId}`;
@@ -128,7 +147,7 @@ export const CopyDropdown = (props) => {
         <DropdownMenu
           className={`${styles['copy-clipboard-dropdown-menu']}`}
           strategy="fixed"
-          container="body"
+          container={dropdownMenuContainer}
         >
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
@@ -209,7 +228,7 @@ export const CopyDropdown = (props) => {
           { pageId && (
             <CopyToClipboard text={markdownLink} onCopy={showToolTip}>
               <DropdownItem className="px-3 text-wrap">
-                <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={markdownLink} isContentsWrap />
+                <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={markdownLink} />
               </DropdownItem>
             </CopyToClipboard>
           )}
@@ -223,13 +242,3 @@ export const CopyDropdown = (props) => {
     </>
   );
 };
-
-CopyDropdown.propTypes = {
-  children: PropTypes.node.isRequired,
-  dropdownToggleId: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-
-  pageId: PropTypes.string,
-  dropdownToggleClassName: PropTypes.string,
-  isShareLinkMode: PropTypes.bool,
-};

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

@@ -166,6 +166,7 @@ export const PageTitleHeader = (props: Props): JSX.Element => {
           pagePath={currentPage.path}
           dropdownToggleId={`copydropdown-in-pagetitleheader-${currentPage._id}`}
           dropdownToggleClassName="p-1"
+          dropdownMenuContainer="body"
         >
           <span className="material-symbols-outlined fs-6">content_paste</span>
         </CopyDropdown>

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

@@ -49,7 +49,11 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
       className={`${className} ${moduleClass}`}
       style={{ maxWidth }}
     >
-      <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']}`}>{formerLink}</span>
+      {formerLink && (
+        <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']} mb-2 d-block`}>
+          {formerLink}
+        </span>
+      )}
       <div className={containerLayoutClass}>
         <h1 className={`m-0 d-inline align-bottom ${latterLinkClassName}`}>
           {latterLink}
@@ -60,7 +64,13 @@ export const PagePathNavLayout = (props: Props): JSX.Element => {
               <span className="badge text-bg-secondary">WIP</span>
             )}
             <span className="grw-page-path-nav-copydropdown">
-              <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
+              <CopyDropdown
+                pageId={pageId}
+                pagePath={pagePath}
+                dropdownToggleId={copyDropdownId}
+                dropdownToggleClassName="p-2"
+                dropdownMenuContainer="body"
+              >
                 <span className="material-symbols-outlined">content_paste</span>
               </CopyDropdown>
             </span>

+ 13 - 1
apps/app/src/components/Layout/Admin.module.scss

@@ -230,14 +230,22 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     .date-range-picker {
       width: 188px;
+      height: auto;
     }
     .jump-page-input {
       width: 50px;
     }
+
+    .table-bordered {
+      table-layout: fixed;
+    }
+
   }
 
+
+
+
   .settings-table {
-    table-layout: fixed;
 
     .item-name {
       width: 150px;
@@ -252,6 +260,10 @@ $slack-work-space-name-card-border: #efc1f6;
     }
   }
 
+
+
+
+
 }
 
 

+ 60 - 6
apps/app/src/features/opentelemetry/server/node-sdk.spec.ts

@@ -3,7 +3,7 @@ import { NodeSDK } from '@opentelemetry/sdk-node';
 
 import { configManager } from '~/server/service/config-manager';
 
-import { setupAdditionalResourceAttributes, initInstrumentation } from './node-sdk';
+import { setupAdditionalResourceAttributes, initInstrumentation, startOpenTelemetry } from './node-sdk';
 import { getResource } from './node-sdk-resource';
 
 // Only mock configManager as it's external to what we're testing
@@ -67,15 +67,10 @@ describe('node-sdk', () => {
 
   describe('initInstrumentation', () => {
     it('should call setupCustomMetrics when instrumentation is enabled', async() => {
-      const { setupCustomMetrics } = await import('./custom-metrics');
-
       // Mock instrumentation as enabled
       mockInstrumentationEnabled();
 
       await initInstrumentation();
-
-      // Verify setupCustomMetrics was called
-      expect(setupCustomMetrics).toHaveBeenCalledOnce();
     });
 
     it('should not call setupCustomMetrics when instrumentation is disabled', async() => {
@@ -203,4 +198,63 @@ describe('node-sdk', () => {
       await expect(setupAdditionalResourceAttributes()).resolves.toBeUndefined();
     });
   });
+
+  describe('startOpenTelemetry', () => {
+    it('should start SDK and call setupCustomMetrics when instrumentation is enabled and SDK instance exists', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as enabled
+      mockInstrumentationEnabled();
+
+      // Initialize SDK first
+      await initInstrumentation();
+
+      // Get SDK instance and mock its start method
+      const { __testing__ } = await import('./node-sdk');
+      const sdkInstance = __testing__.getSdkInstance();
+      expect(sdkInstance).toBeDefined();
+
+      if (sdkInstance != null) {
+        const startSpy = vi.spyOn(sdkInstance, 'start');
+
+        // Call startOpenTelemetry
+        startOpenTelemetry();
+
+        // Verify that start method was called
+        expect(startSpy).toHaveBeenCalledOnce();
+
+        // Verify that setupCustomMetrics was called
+        expect(setupCustomMetrics).toHaveBeenCalledOnce();
+      }
+    });
+
+    it('should not start SDK when instrumentation is disabled', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as disabled
+      mockInstrumentationDisabled();
+
+      // Initialize SDK (should not create instance)
+      await initInstrumentation();
+
+      // Call startOpenTelemetry
+      startOpenTelemetry();
+
+      // Verify that setupCustomMetrics was not called
+      expect(setupCustomMetrics).not.toHaveBeenCalled();
+    });
+
+    it('should not start SDK when SDK instance does not exist', async() => {
+      const { setupCustomMetrics } = await import('./custom-metrics');
+
+      // Mock instrumentation as enabled but don't initialize SDK
+      mockInstrumentationEnabled();
+
+      // Call startOpenTelemetry without initializing SDK
+      startOpenTelemetry();
+
+      // Verify that setupCustomMetrics was not called
+      expect(setupCustomMetrics).not.toHaveBeenCalled();
+    });
+  });
 });

+ 3 - 2
apps/app/src/features/opentelemetry/server/node-sdk.ts

@@ -72,8 +72,6 @@ For more information, see https://docs.growi.org/en/admin-guide/admin-cookbook/t
 
     const sdkConfig = generateNodeSDKConfiguration({ enableAnonymization });
 
-    setupCustomMetrics();
-
     sdkInstance = new NodeSDK(sdkConfig);
   }
 };
@@ -106,6 +104,9 @@ export const startOpenTelemetry = (): void => {
       throw new Error('OpenTelemetry instrumentation is not initialized');
     }
     sdkInstance.start();
+
+    // setup custom metrics after SDK start
+    setupCustomMetrics();
   }
 };
 

+ 5 - 1
apps/app/src/server/middlewares/access-token-parser/access-token.ts

@@ -5,14 +5,18 @@ import type { Response } from 'express';
 import { AccessToken } from '~/server/models/access-token';
 import loggerFactory from '~/utils/logger';
 
+import { extractBearerToken } from './extract-bearer-token';
 import type { AccessTokenParserReq } from './interfaces';
 
 const logger = loggerFactory('growi:middleware:access-token-parser:access-token');
 
 export const parserForAccessToken = (scopes: Scope[]) => {
   return async(req: AccessTokenParserReq, res: Response): Promise<void> => {
+    // Extract token from Authorization header first
+    // It is more efficient to call it only once in "AccessTokenParser," which is the caller of the method
+    const bearerToken = extractBearerToken(req.headers.authorization);
 
-    const accessToken = req.query.access_token ?? req.body.access_token;
+    const accessToken = bearerToken ?? req.query.access_token ?? req.body.access_token;
     if (accessToken == null || typeof accessToken !== 'string') {
       return;
     }

+ 3 - 13
apps/app/src/server/middlewares/access-token-parser/api-token.ts

@@ -1,30 +1,20 @@
 import type { IUser, IUserHasId } from '@growi/core/dist/interfaces';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
-import type { NextFunction, Response } from 'express';
+import type { Response } from 'express';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
 import loggerFactory from '~/utils/logger';
 
+import { extractBearerToken } from './extract-bearer-token';
 import type { AccessTokenParserReq } from './interfaces';
 
 const logger = loggerFactory('growi:middleware:access-token-parser:api-token');
 
-const extractBearerToken = (authHeader: string | undefined): string | null => {
-  if (authHeader == null) {
-    return null;
-  }
-
-  if (!authHeader.startsWith('Bearer ')) {
-    return null;
-  }
-
-  return authHeader.substring(7); // Remove 'Bearer ' prefix
-};
-
 
 export const parserForApiToken = async(req: AccessTokenParserReq, res: Response): Promise<void> => {
   // Extract token from Authorization header first
+  // It is more efficient to call it only once in "AccessTokenParser," which is the caller of the method
   const bearerToken = extractBearerToken(req.headers.authorization);
 
   // Try all possible token sources in order of priority

+ 11 - 0
apps/app/src/server/middlewares/access-token-parser/extract-bearer-token.ts

@@ -0,0 +1,11 @@
+export const extractBearerToken = (authHeader: string | undefined): string | null => {
+  if (authHeader == null) {
+    return null;
+  }
+
+  if (!authHeader.startsWith('Bearer ')) {
+    return null;
+  }
+
+  return authHeader.substring(7); // Remove 'Bearer ' prefix
+};

+ 0 - 1
apps/app/src/server/middlewares/access-token-parser/index.ts

@@ -14,7 +14,6 @@ export type AccessTokenParser = (scopes?: Scope[], opts?: {acceptLegacy: boolean
 
 export const accessTokenParser: AccessTokenParser = (scopes, opts) => {
   return async(req, res, next): Promise<void> => {
-    // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
     if (scopes == null || scopes.length === 0) {
       logger.warn('scopes is empty');
       return next();

+ 39 - 27
apps/app/src/server/routes/apiv3/page/index.ts

@@ -4,7 +4,7 @@ import { pipeline } from 'stream/promises';
 
 import type { IPage, IRevision } from '@growi/core';
 import {
-  AllSubscriptionStatusType, PageGrant, SubscriptionStatusType,
+  AllSubscriptionStatusType, PageGrant, SCOPE, SubscriptionStatusType,
   getIdForRef,
 } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
@@ -16,7 +16,6 @@ import sanitize from 'sanitize-filename';
 
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
 import type { IPageGrantData } from '~/interfaces/page';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
@@ -279,9 +278,18 @@ module.exports = (crowi) => {
    *                  grant:
    *                    $ref: '#/components/schemas/PageGrant'
    *                  grantUserGroupIds:
-   *                    type: string
-   *                    description: UserGroup ID
-   *                    example: 5ae5fccfc5577b0004dbd8ab
+   *                    type: array
+   *                    items:
+   *                      type: object
+   *                      properties:
+   *                        type:
+   *                          type: string
+   *                          description: Group type
+   *                          example: 'UserGroup'
+   *                        item:
+   *                          type: string
+   *                          description: UserGroup ID
+   *                          example: '5ae5fccfc5577b0004dbd8ab'
    *                  pageTags:
    *                    type: array
    *                    items:
@@ -295,18 +303,16 @@ module.exports = (crowi) => {
    *            content:
    *              application/json:
    *                schema:
+   *                  type: object
    *                  properties:
-   *                    data:
-   *                      type: object
-   *                      properties:
-   *                        page:
-   *                          $ref: '#/components/schemas/Page'
-   *                        tags:
-   *                          type: array
-   *                          items:
-   *                            $ref: '#/components/schemas/Tags'
-   *                        revision:
-   *                          $ref: '#/components/schemas/Revision'
+   *                    page:
+   *                      $ref: '#/components/schemas/Page'
+   *                    tags:
+   *                      type: array
+   *                      items:
+   *                        $ref: '#/components/schemas/Tags'
+   *                    revision:
+   *                       $ref: '#/components/schemas/Revision'
    *          409:
    *            description: page path is already existed
    */
@@ -335,8 +341,16 @@ module.exports = (crowi) => {
    *                  userRelatedGrantUserGroupIds:
    *                    type: array
    *                    items:
-   *                      type: string
-   *                      description: UserGroup ID
+   *                      type: object
+   *                      properties:
+   *                        type:
+   *                          type: string
+   *                          description: Group type
+   *                          example: 'UserGroup'
+   *                        item:
+   *                          type: string
+   *                          description: UserGroup ID
+   *                          example: '5ae5fccfc5577b0004dbd8ab'
    *                  overwriteScopesOfDescendants:
    *                    type: boolean
    *                    description: Determine whether the scopes of descendants should be overwritten
@@ -362,14 +376,12 @@ module.exports = (crowi) => {
    *            content:
    *              application/json:
    *                schema:
+   *                  type: object
    *                  properties:
-   *                    data:
-   *                      type: object
-   *                      properties:
-   *                        page:
-   *                          $ref: '#/components/schemas/Page'
-   *                        revision:
-   *                          $ref: '#/components/schemas/Revision'
+   *                    page:
+   *                      $ref: '#/components/schemas/Page'
+   *                    revision:
+   *                      $ref: '#/components/schemas/Revision'
    *          403:
    *            $ref: '#/components/responses/Forbidden'
    *          500:
@@ -1077,7 +1089,7 @@ module.exports = (crowi) => {
 
   /**
    * @swagger
-   *   /{pageId}/publish:
+   *   /page/{pageId}/publish:
    *     put:
    *       tags: [Page]
    *       summary: Publish page
@@ -1101,7 +1113,7 @@ module.exports = (crowi) => {
 
   /**
    * @swagger
-   *   /{pageId}/unpublish:
+   *   /page/{pageId}/unpublish:
    *     put:
    *       tags: [Page]
    *       summary: Unpublish page

+ 16 - 17
apps/app/src/server/routes/apiv3/share-links.js

@@ -185,23 +185,22 @@ module.exports = (crowi) => {
    *        security:
    *          - cookieAuth: []
    *        description: Create new share link
-   *        parameters:
-   *          - name: relatedPage
-   *            in: query
-   *            required: true
-   *            description: page id of share link
-   *            schema:
-   *              type: string
-   *          - name: expiredAt
-   *            in: query
-   *            description: expiration date of share link
-   *            schema:
-   *              type: string
-   *          - name: description
-   *            in: query
-   *            description: description of share link
-   *            schema:
-   *              type: string
+   *        requestBody:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                required:
+   *                  - relatedPage
+   *                properties:
+   *                  relatedPage:
+   *                    description: page id of share link
+   *                    type: string
+   *                  expiredAt:
+   *                    description: expiration date of share link
+   *                    type: string
+   *                  description:
+   *                    description: description of share link
+   *                    type: string
    *        responses:
    *          200:
    *            description: Succeeded to create one share link

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

@@ -1,5 +1,6 @@
 import path from 'path';
 
+import { SCOPE } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
@@ -11,7 +12,6 @@ import { isEmail } from 'validator';
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { deleteUserAiAssistant } from '~/features/openai/server/services/delete-ai-assistant';
 import { SupportedAction } from '~/interfaces/activity';
-import { SCOPE } from '@growi/core/dist/interfaces';
 import { accessTokenParser } from '~/server/middlewares/access-token-parser';
 import Activity from '~/server/models/activity';
 import ExternalAccount from '~/server/models/external-account';
@@ -371,7 +371,7 @@ module.exports = (crowi) => {
    * @swagger
    *
    *  paths:
-   *    /{id}/recent:
+   *    /users/{id}/recent:
    *      get:
    *        tags: [Users]
    *        summary: /usersIdReacent

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

@@ -503,7 +503,7 @@ export const CONFIG_DEFINITIONS = {
   }),
   'app:deploymentType': defineConfig<GrowiDeploymentType>({
     envVarName: 'DEPLOYMENT_TYPE',
-    defaultValue: GrowiDeploymentType.others,
+    defaultValue: GrowiDeploymentType.node,
   }),
   'app:ssrMaxRevisionBodyLength': defineConfig<number>({
     envVarName: 'SSR_MAX_REVISION_BODY_LENGTH',

+ 0 - 5
apps/app/test-with-vite/download-mongo-binary/index.spec.ts

@@ -1,5 +0,0 @@
-describe('Download mongo-binary', () => {
-  it('should be success', () => {
-    expect(true).toBeTruthy();
-  });
-});

+ 0 - 16
apps/app/test-with-vite/download-mongo-binary/vitest.config.ts

@@ -1,16 +0,0 @@
-import tsconfigPaths from 'vite-tsconfig-paths';
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
-  plugins: [
-    tsconfigPaths(),
-  ],
-  test: {
-    clearMocks: true,
-    globals: true,
-    hookTimeout: 60000, // increased for downloading MongoDB binary file
-    setupFiles: [
-      './test-with-vite/setup/mongoms.ts',
-    ],
-  },
-});

+ 1 - 6
biome.json

@@ -23,16 +23,11 @@
       "!apps/slackbot-proxy/src/public/bootstrap/**",
       "!packages/editor/**",
       "!packages/pdf-converter-client/src/index.ts",
-      "!apps/app/bin/**",
-      "!apps/app/config/**",
-      "!apps/app/docker/**",
       "!apps/app/playwright/**",
       "!apps/app/public/**",
-      "!apps/app/resource/**",
       "!apps/app/src/**",
       "!apps/app/test/integration/service/**",
-      "!apps/app/test-with-vite/**",
-      "!apps/app/tmp/**"
+      "!apps/app/test-with-vite/**"
     ]
   },
   "formatter": {

+ 1 - 0
packages/core/src/consts/system.ts

@@ -2,6 +2,7 @@ export const GrowiServiceType = {
   cloud: 'cloud',
   privateCloud: 'private-cloud',
   onPremise: 'on-premise',
+  dev: 'dev',
   others: 'others',
 } as const;
 

+ 8 - 8
pnpm-lock.yaml

@@ -491,8 +491,8 @@ importers:
         specifier: ^0.1.2
         version: 0.1.2
       mermaid:
-        specifier: ^11.9.0
-        version: 11.9.0
+        specifier: ^11.10.0
+        version: 11.10.0
       method-override:
         specifier: ^3.0.0
         version: 3.0.0
@@ -11018,8 +11018,8 @@ packages:
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     engines: {node: '>= 8'}
 
-  mermaid@11.9.0:
-    resolution: {integrity: sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==}
+  mermaid@11.10.0:
+    resolution: {integrity: sha512-oQsFzPBy9xlpnGxUqLbVY8pvknLlsNIJ0NWwi8SUJjhbP1IT0E0o1lfhU4iYV3ubpy+xkzkaOyDUQMn06vQElQ==}
 
   method-override@3.0.0:
     resolution: {integrity: sha512-IJ2NNN/mSl9w3kzWB92rcdHpz+HjkxhDJWNDBqSlas+zQdP8wBiJzITPg08M/k2uVvMow7Sk41atndNtt/PHSA==}
@@ -17516,7 +17516,7 @@ snapshots:
       '@antfu/install-pkg': 1.1.0
       '@antfu/utils': 8.1.1
       '@iconify/types': 2.0.0
-      debug: 4.4.0(supports-color@5.5.0)
+      debug: 4.4.1
       globals: 15.15.0
       kolorist: 1.8.0
       local-pkg: 1.1.1
@@ -27393,7 +27393,7 @@ snapshots:
 
   merge2@1.4.1: {}
 
-  mermaid@11.9.0:
+  mermaid@11.10.0:
     dependencies:
       '@braintree/sanitize-url': 7.1.0
       '@iconify/utils': 2.3.0
@@ -30588,7 +30588,7 @@ snapshots:
   streamroller@3.1.5:
     dependencies:
       date-format: 4.0.14
-      debug: 4.4.0(supports-color@5.5.0)
+      debug: 4.4.1
       fs-extra: 8.1.0
     transitivePeerDependencies:
       - supports-color
@@ -31989,7 +31989,7 @@ snapshots:
 
   vue-eslint-parser@7.11.0(eslint@8.41.0):
     dependencies:
-      debug: 4.4.0(supports-color@5.5.0)
+      debug: 4.4.1
       eslint: 8.41.0
       eslint-scope: 5.1.1
       eslint-visitor-keys: 1.3.0