Browse Source

Merge remote-tracking branch 'origin/master' into feat/97800-159429-add-shortcuts-when-editing

WNomunomu 10 months ago
parent
commit
bd0e8f8cc7
100 changed files with 2042 additions and 976 deletions
  1. 0 1
      .eslintignore
  2. 7 1
      .eslintrc.js
  3. 46 1
      CHANGELOG.md
  4. 0 6
      apps/app/.eslintignore
  5. 14 2
      apps/app/.eslintrc.js
  6. 9 0
      apps/app/bin/openapi/definition-apiv1.js
  7. 9 0
      apps/app/bin/openapi/definition-apiv3.js
  8. 96 0
      apps/app/bin/openapi/generate-operation-ids/cli.spec.ts
  9. 29 0
      apps/app/bin/openapi/generate-operation-ids/cli.ts
  10. 219 0
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts
  11. 62 0
      apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts
  12. 15 0
      apps/app/bin/openapi/generate-spec-apiv1.sh
  13. 9 4
      apps/app/bin/openapi/generate-spec-apiv3.sh
  14. 0 15
      apps/app/bin/swagger-jsdoc/generate-spec-apiv1.sh
  15. 12 8
      apps/app/package.json
  16. 2 2
      apps/app/playwright/40-admin/access-to-admin-page.spec.ts
  17. 0 1
      apps/app/public/static/locales/en_US/admin.json
  18. 0 1
      apps/app/public/static/locales/fr_FR/admin.json
  19. 20 21
      apps/app/public/static/locales/ja_JP/admin.json
  20. 0 1
      apps/app/public/static/locales/zh_CN/admin.json
  21. 2 2
      apps/app/src/client/components/Admin/App/AppSetting.jsx
  22. 4 4
      apps/app/src/client/components/Admin/App/AwsSetting.tsx
  23. 8 8
      apps/app/src/client/components/Admin/App/AzureSetting.tsx
  24. 3 3
      apps/app/src/client/components/Admin/App/GcsSetting.tsx
  25. 1 1
      apps/app/src/client/components/Admin/App/MailSetting.tsx
  26. 3 3
      apps/app/src/client/components/Admin/App/MaskedInput.tsx
  27. 2 2
      apps/app/src/client/components/Admin/App/SesSetting.tsx
  28. 1 1
      apps/app/src/client/components/Admin/App/SiteUrlSetting.tsx
  29. 4 4
      apps/app/src/client/components/Admin/App/SmtpSetting.tsx
  30. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx
  31. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  32. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx
  33. 1 1
      apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx
  34. 2 2
      apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  35. 2 2
      apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx
  36. 1 1
      apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.jsx
  37. 3 3
      apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.jsx
  38. 10 10
      apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.jsx
  39. 1 1
      apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.jsx
  40. 16 16
      apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.jsx
  41. 9 9
      apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.jsx
  42. 95 53
      apps/app/src/client/components/Admin/Security/SecuritySetting.jsx
  43. 1 1
      apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  44. 2 2
      apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  45. 1 1
      apps/app/src/client/components/Admin/SlackIntegration/ManageCommandsProcess.jsx
  46. 26 3
      apps/app/src/client/components/PageControls/PageControls.tsx
  47. 0 1
      apps/app/src/client/components/Sidebar/InAppNotification/PrimaryItemForNotification.tsx
  48. 15 4
      apps/app/src/client/components/Sidebar/SidebarContents.tsx
  49. 3 2
      apps/app/src/client/components/Sidebar/SidebarNav/PrimaryItems.tsx
  50. 23 19
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  51. 4 2
      apps/app/src/client/services/page-operation.ts
  52. 0 15
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts
  53. 110 113
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx
  54. 0 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/MessageCard.module.scss
  55. 13 10
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/MessageCard.tsx
  56. 25 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/ReactMarkdownComponents/Header.tsx
  57. 13 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/ReactMarkdownComponents/NextLinkWrapper.tsx
  58. 11 6
      apps/app/src/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton.tsx
  59. 57 23
      apps/app/src/features/openai/server/routes/edit/index.ts
  60. 6 2
      apps/app/src/pages/[[...path]].page.tsx
  61. 10 0
      apps/app/src/server/models/openapi/object-id.ts
  62. 11 9
      apps/app/src/server/models/openapi/page.ts
  63. 12 8
      apps/app/src/server/models/openapi/paginate-result.ts
  64. 12 10
      apps/app/src/server/models/openapi/revision.ts
  65. 32 0
      apps/app/src/server/models/openapi/tag.ts
  66. 0 19
      apps/app/src/server/models/openapi/v1-response.js
  67. 131 0
      apps/app/src/server/models/openapi/v1-response.ts
  68. 0 1
      apps/app/src/server/routes/apiv3/admin-home.ts
  69. 79 41
      apps/app/src/server/routes/apiv3/app-settings.js
  70. 4 6
      apps/app/src/server/routes/apiv3/attachment.js
  71. 0 6
      apps/app/src/server/routes/apiv3/bookmark-folder.ts
  72. 1 4
      apps/app/src/server/routes/apiv3/bookmarks.js
  73. 1 17
      apps/app/src/server/routes/apiv3/customize-setting.js
  74. 0 3
      apps/app/src/server/routes/apiv3/export.js
  75. 2 0
      apps/app/src/server/routes/apiv3/g2g-transfer.ts
  76. 0 1
      apps/app/src/server/routes/apiv3/healthcheck.ts
  77. 1 5
      apps/app/src/server/routes/apiv3/import.js
  78. 0 4
      apps/app/src/server/routes/apiv3/in-app-notification.ts
  79. 0 1
      apps/app/src/server/routes/apiv3/installer.ts
  80. 0 1
      apps/app/src/server/routes/apiv3/invited.ts
  81. 0 4
      apps/app/src/server/routes/apiv3/markdown-setting.js
  82. 0 1
      apps/app/src/server/routes/apiv3/mongo.js
  83. 12 20
      apps/app/src/server/routes/apiv3/page/index.ts
  84. 5 42
      apps/app/src/server/routes/apiv3/pages/index.js
  85. 0 14
      apps/app/src/server/routes/apiv3/personal-setting.js
  86. 10 10
      apps/app/src/server/routes/apiv3/security-settings/index.js
  87. 0 13
      apps/app/src/server/routes/apiv3/slack-integration-settings.js
  88. 0 1
      apps/app/src/server/routes/apiv3/statistics.js
  89. 0 1
      apps/app/src/server/routes/apiv3/user-activation.ts
  90. 0 1
      apps/app/src/server/routes/apiv3/user-group-relation.js
  91. 0 15
      apps/app/src/server/routes/apiv3/user-group.js
  92. 2 20
      apps/app/src/server/routes/apiv3/users.js
  93. 17 20
      apps/app/src/server/routes/attachment/api.js
  94. 57 54
      apps/app/src/server/routes/comment.js
  95. 331 117
      apps/app/src/server/routes/page.js
  96. 38 34
      apps/app/src/server/routes/search.ts
  97. 33 56
      apps/app/src/server/routes/tag.js
  98. 20 16
      apps/app/src/server/service/config-manager/config-definition.ts
  99. 87 0
      apps/app/src/server/service/config-manager/config-manager.integ.ts
  100. 104 4
      apps/app/src/server/service/config-manager/config-manager.spec.ts

+ 0 - 1
.eslintignore

@@ -1 +0,0 @@
-node_modules/**

+ 7 - 1
.eslintrc.js

@@ -1,3 +1,6 @@
+/**
+ * @type {import('eslint').Linter.Config}
+ */
 module.exports = {
 module.exports = {
   root: true, // https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy
   root: true, // https://eslint.org/docs/user-guide/configuring/configuration-files#cascading-and-hierarchy
   extends: [
   extends: [
@@ -7,6 +10,9 @@ module.exports = {
   plugins: [
   plugins: [
     'regex',
     'regex',
   ],
   ],
+  ignorePatterns: [
+    'node_modules/**',
+  ],
   rules: {
   rules: {
     'import/prefer-default-export': 'off',
     'import/prefer-default-export': 'off',
     'import/order': [
     'import/order': [
@@ -73,7 +79,7 @@ module.exports = {
   overrides: [
   overrides: [
     {
     {
       // enable the rule specifically for TypeScript files
       // enable the rule specifically for TypeScript files
-      files: ['*.ts', '*.tsx'],
+      files: ['*.ts', '*.mts', '*.tsx'],
       rules: {
       rules: {
         '@typescript-eslint/explicit-module-boundary-types': ['error'],
         '@typescript-eslint/explicit-module-boundary-types': ['error'],
       },
       },

+ 46 - 1
CHANGELOG.md

@@ -1,9 +1,54 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.2.5...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.2.7...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.2.7](https://github.com/weseek/growi/compare/v7.2.6...v7.2.7) - 2025-06-11
+
+### 🐛 Bug Fixes
+
+* fix: Input values ​​in the admin settings form are sometimes not reflected (#10051) @yuki-takei
+* fix: Hide Google OAuth client secret field (#10049) @yuki-takei
+* fix: Prevent unnecessary API request when the user is guest (#10046) @yuki-takei
+* fix(ai): Prevent unnecessary API request when GROWI AI is disabled (#10044) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Configure biome for preset-templates package (#10058) @arafubeatbox
+* support: Configure biome for preset-themes package (#10055) @arafubeatbox
+* support: Configure biome for remark-drawio package (#10033) @arafubeatbox
+
+## [v7.2.6](https://github.com/weseek/growi/compare/v7.2.5...v7.2.6) - 2025-06-10
+
+### 💎 Features
+
+* feat(ai): Display spinner while creating diff (#9991) @miya
+
+### 🚀 Improvement
+
+* imprv: Message card markdown header size (#10038) @miya
+* imprv: Type safe configuration for file uploading (#10032) @yuki-takei
+* imprv: EditorAssistant instruction (#10030) @miya
+* imprv: Add NonEmptyString type (#10031) @yuki-takei
+* imprv: Security settings search results redesign (#9992) @arvid-e
+* imprv: OpenAPI spec properties ref (#10023) @yuki-takei
+* imprv(ai): Make input form position sticky (#10002) @miya
+* imprv: Prevent path traversal attack in pdf converter (#9993) @arafubeatbox
+* imprv: Discard when form is submitted without Accept/Discard after showing diff (#9980) @miya
+
+### 🐛 Bug Fixes
+
+* imprv:  The delete button on the user home page is now hidden for unauthorized users. (#9915) @taikou-m
+* fix: OpenAI threads can be retrieved regardless of assistant's public permissions (#9994) @miya
+* fix: Editor assistant button is being displayed when ai functionality is not enabled (#9985) @miya
+* fix: Improve attribute handling in Lsx (#9989) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: OpenAPI operationId generation (#10009) @yuki-takei
+* support: Configure biome for remark-growi-directive (#9999) @arafubeatbox
+
 ## [v7.2.5](https://github.com/weseek/growi/compare/v7.2.4...v7.2.5) - 2025-05-28
 ## [v7.2.5](https://github.com/weseek/growi/compare/v7.2.4...v7.2.5) - 2025-05-28
 
 
 ### 💎 Features
 ### 💎 Features

+ 0 - 6
apps/app/.eslintignore

@@ -1,6 +0,0 @@
-/dist/**
-/transpiled/**
-/public/**
-/src/linter-checker/**
-/tmp/**
-/next-env.d.ts

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

@@ -1,3 +1,6 @@
+/**
+ * @type {import('eslint').Linter.Config}
+ */
 module.exports = {
 module.exports = {
   extends: [
   extends: [
     'next/core-web-vitals',
     'next/core-web-vitals',
@@ -5,6 +8,15 @@ module.exports = {
   ],
   ],
   plugins: [
   plugins: [
   ],
   ],
+  ignorePatterns: [
+    'dist/**',
+    '**/dist/**',
+    'transpiled/**',
+    'public/**',
+    'src/linter-checker/**',
+    'tmp/**',
+    'next-env.d.ts',
+  ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript
     'import/resolver': {
     'import/resolver': {
@@ -25,7 +37,7 @@ module.exports = {
   overrides: [
   overrides: [
     {
     {
       // enable the rule specifically for JavaScript files
       // enable the rule specifically for JavaScript files
-      files: ['*.js', '*.jsx'],
+      files: ['*.js', '*.mjs', '*.jsx'],
       rules: {
       rules: {
         // set 'warn' temporarily -- 2023.08.14 Yuki Takei
         // set 'warn' temporarily -- 2023.08.14 Yuki Takei
         'react/prop-types': 'warn',
         'react/prop-types': 'warn',
@@ -35,7 +47,7 @@ module.exports = {
     },
     },
     {
     {
       // enable the rule specifically for TypeScript files
       // enable the rule specifically for TypeScript files
-      files: ['*.ts', '*.tsx'],
+      files: ['*.ts', '*.mts', '*.tsx'],
       rules: {
       rules: {
         'no-unused-vars': 'off',
         'no-unused-vars': 'off',
         // set 'warn' temporarily -- 2023.08.14 Yuki Takei
         // set 'warn' temporarily -- 2023.08.14 Yuki Takei

+ 9 - 0
apps/app/bin/swagger-jsdoc/definition-apiv1.js → apps/app/bin/openapi/definition-apiv1.js

@@ -7,6 +7,15 @@ module.exports = {
     version: pkg.version,
     version: pkg.version,
   },
   },
   servers: [
   servers: [
+    {
+      url: '{server}/_api',
+      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.',
+        },
+      },
+    },
     {
     {
       url: 'https://demo.growi.org/_api',
       url: 'https://demo.growi.org/_api',
     },
     },

+ 9 - 0
apps/app/bin/swagger-jsdoc/definition-apiv3.js → apps/app/bin/openapi/definition-apiv3.js

@@ -7,6 +7,15 @@ module.exports = {
     version: pkg.version,
     version: pkg.version,
   },
   },
   servers: [
   servers: [
+    {
+      url: '{server}/_api/v3',
+      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.',
+        },
+      },
+    },
     {
     {
       url: 'https://demo.growi.org/_api/v3',
       url: 'https://demo.growi.org/_api/v3',
     },
     },

+ 96 - 0
apps/app/bin/openapi/generate-operation-ids/cli.spec.ts

@@ -0,0 +1,96 @@
+import { writeFileSync } from 'fs';
+
+import {
+  beforeEach, describe, expect, it, vi,
+} from 'vitest';
+
+import { generateOperationIds } from './generate-operation-ids';
+
+// Mock the modules
+vi.mock('fs');
+vi.mock('./generate-operation-ids');
+
+const originalArgv = process.argv;
+
+describe('cli', () => {
+  const mockJsonStrings = '{"test": "data"}';
+
+  beforeEach(() => {
+    vi.resetModules();
+    vi.resetAllMocks();
+    process.argv = [...originalArgv]; // Reset process.argv
+    // Mock console.error to avoid actual console output during tests
+    vi.spyOn(console, 'error').mockImplementation(() => {});
+  });
+
+  it('processes input file and writes output to specified file', async() => {
+    // Mock generateOperationIds to return success
+    vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json', '-o', 'output.json'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify generateOperationIds was called with correct arguments
+    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() => {
+    // Mock generateOperationIds to return success
+    vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify generateOperationIds was called with correct arguments
+    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() => {
+    // Mock generateOperationIds to return success
+    vi.mocked(generateOperationIds).mockResolvedValue(mockJsonStrings);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json', '--overwrite-existing'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify generateOperationIds was called with overwriteExisting option
+    expect(generateOperationIds).toHaveBeenCalledWith('input.json', { overwriteExisting: true });
+  });
+
+  it('handles generateOperationIds error correctly', async() => {
+    // Mock generateOperationIds to throw error
+    const error = new Error('Test error');
+    vi.mocked(generateOperationIds).mockRejectedValue(error);
+
+    // Mock process.argv
+    process.argv = ['node', 'cli.js', 'input.json'];
+
+    // Import the module that contains the main function
+    const cliModule = await import('./cli');
+    await cliModule.main();
+
+    // Verify error was logged
+    // eslint-disable-next-line no-console
+    expect(console.error).toHaveBeenCalledWith(error);
+
+    // Verify writeFileSync was not called
+    expect(writeFileSync).not.toHaveBeenCalled();
+  });
+});

+ 29 - 0
apps/app/bin/openapi/generate-operation-ids/cli.ts

@@ -0,0 +1,29 @@
+import { writeFileSync } from 'fs';
+
+import { Command } from 'commander';
+
+import { generateOperationIds } from './generate-operation-ids';
+
+export const main = async(): Promise<void> => {
+  // parse command line arguments
+  const program = new Command();
+  program
+    .name('generate-operation-ids')
+    .description('Generate operationId for OpenAPI specification')
+    .argument('<input-file>', 'OpenAPI specification file')
+    .option('-o, --out <output-file>', 'Output file (defaults to input file)')
+    .option('--overwrite-existing', 'Overwrite existing operationId values')
+    .parse();
+  const { out: outputFile, overwriteExisting } = program.opts();
+  const [inputFile] = program.args;
+
+  // eslint-disable-next-line no-console
+  const jsonStrings = await generateOperationIds(inputFile, { overwriteExisting }).catch(console.error);
+  if (jsonStrings != null) {
+    writeFileSync(outputFile ?? inputFile, jsonStrings);
+  }
+};
+
+if (import.meta.url === `file://${process.argv[1]}`) {
+  main();
+}

+ 219 - 0
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.spec.ts

@@ -0,0 +1,219 @@
+import fs from 'fs/promises';
+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');
+  await fs.writeFile(filePath, JSON.stringify(spec));
+  return filePath;
+}
+
+async function cleanup(filePath: string): Promise<void> {
+  try {
+    await fs.unlink(filePath);
+    await fs.rmdir(path.dirname(filePath));
+  }
+  catch (err) {
+    // eslint-disable-next-line no-console
+    console.error('Cleanup failed:', err);
+  }
+}
+
+describe('generateOperationIds', () => {
+  it('should generate correct operationId for simple paths', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo': {
+          get: {},
+          post: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
+      expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  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' },
+      paths: {
+        '/foo/{id}': {
+          get: {},
+        },
+        '/foo/{id}/bar/{page}': {
+          get: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      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 {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should generate correct operationId for nested resources', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo/bar': {
+          get: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo/bar'].get.operationId).toBe('getBarForFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should preserve existing operationId when overwriteExisting is false', async() => {
+    const existingOperationId = 'existingOperation';
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo': {
+          get: {
+            operationId: existingOperationId,
+          },
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath, { overwriteExisting: false });
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe(existingOperationId);
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  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' },
+      paths: {
+        '/foo': {
+          get: {
+            operationId: 'existingOperation',
+          },
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath, { overwriteExisting: true });
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should generate correct operationId for root path', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/': {
+          get: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/'].get.operationId).toBe('getRoot');
+    }
+    finally {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should generate operationId for all HTTP methods', async() => {
+    const spec: OpenAPI3 = {
+      openapi: '3.0.0',
+      info: { title: 'Test API', version: '1.0.0' },
+      paths: {
+        '/foo': {
+          get: {},
+          post: {},
+          put: {},
+          delete: {},
+          patch: {},
+          options: {},
+          head: {},
+          trace: {},
+        },
+      },
+    };
+
+    const filePath = await createTempOpenAPIFile(spec);
+    try {
+      const result = await generateOperationIds(filePath);
+      const parsed = JSON.parse(result);
+
+      expect(parsed.paths['/foo'].get.operationId).toBe('getFoo');
+      expect(parsed.paths['/foo'].post.operationId).toBe('postFoo');
+      expect(parsed.paths['/foo'].put.operationId).toBe('putFoo');
+      expect(parsed.paths['/foo'].delete.operationId).toBe('deleteFoo');
+      expect(parsed.paths['/foo'].patch.operationId).toBe('patchFoo');
+      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 {
+      await cleanup(filePath);
+    }
+  });
+
+  it('should throw error for non-existent file', async() => {
+    await expect(generateOperationIds('non-existent-file.json')).rejects.toThrow();
+  });
+});

+ 62 - 0
apps/app/bin/openapi/generate-operation-ids/generate-operation-ids.ts

@@ -0,0 +1,62 @@
+import SwaggerParser from '@apidevtools/swagger-parser';
+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 createParamSuffix = (params: string[]): string => {
+  return params.length > 0
+    ? params.reverse().map(param => `By${toPascal(param.slice(1, -1))}`).join('')
+    : '';
+};
+
+
+/**
+ * Generates a PascalCase operation name based on the HTTP method and path.
+ *
+ * e.g.
+ * - `GET /foo` -> `getFoo`
+ * - `POST /bar` -> `postBar`
+ * - `Get /foo/bar` -> `getBarForFoo`
+ * - `GET /foo/{id}` -> `getFooById`
+ * - `GET /foo/{id}/bar` -> `getBarByIdForFoo`
+ * - `GET /foo/{id}/{page}/bar` -> `getBarByPageByIdForFoo`
+ *
+ */
+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 paramSuffix = createParamSuffix(params);
+
+  if (paths.length <= 1) {
+    return `${method.toLowerCase()}${toPascal(paths[0] || 'root')}${paramSuffix}`;
+  }
+
+  const [resource, ...context] = paths.reverse();
+  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;
+
+  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);
+      });
+  });
+
+  const output = JSON.stringify(api, null, 2);
+
+  if (output == null) {
+    throw new Error(`Failed to generate operation IDs for ${inputFile}`);
+  }
+
+  return output;
+}

+ 15 - 0
apps/app/bin/openapi/generate-spec-apiv1.sh

@@ -0,0 +1,15 @@
+# USAGE:
+#   cd apps/app && sh bin/openapi/generate-spec-apiv1.sh
+#   APP_PATH=/path/to/apps/app sh bin/openapi/generate-spec-apiv1.sh
+#   APP_PATH=/path/to/apps/app OUT=/path/to/output sh bin/openapi/generate-spec-apiv1.sh
+
+APP_PATH=${APP_PATH:-"."}
+
+OUT=${OUT:-"${APP_PATH}/tmp/openapi-spec-apiv1.json"}
+
+swagger-jsdoc \
+  -o "${OUT}" \
+  -d "${APP_PATH}/bin/openapi/definition-apiv1.js" \
+  "${APP_PATH}/src/server/routes/*.{js,ts}" \
+  "${APP_PATH}/src/server/routes/attachment/**/*.{js,ts}" \
+  "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"

+ 9 - 4
apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh → apps/app/bin/openapi/generate-spec-apiv3.sh

@@ -1,7 +1,7 @@
 # USAGE:
 # USAGE:
-#   cd apps/app && sh bin/swagger-jsdoc/generate-spec-apiv3.sh
-#   APP_PATH=/path/to/apps/app sh bin/swagger-jsdoc/generate-spec-apiv3.sh
-#   APP_PATH=/path/to/apps/app OUT=/path/to/output sh bin/swagger-jsdoc/generate-spec-apiv3.sh
+#   cd apps/app && sh bin/openapi/generate-spec-apiv3.sh
+#   APP_PATH=/path/to/apps/app sh bin/openapi/generate-spec-apiv3.sh
+#   APP_PATH=/path/to/apps/app OUT=/path/to/output sh bin/openapi/generate-spec-apiv3.sh
 
 
 APP_PATH=${APP_PATH:-"."}
 APP_PATH=${APP_PATH:-"."}
 
 
@@ -9,7 +9,7 @@ OUT=${OUT:-"${APP_PATH}/tmp/openapi-spec-apiv3.json"}
 
 
 swagger-jsdoc \
 swagger-jsdoc \
   -o "${OUT}" \
   -o "${OUT}" \
-  -d "${APP_PATH}/bin/swagger-jsdoc/definition-apiv3.js" \
+  -d "${APP_PATH}/bin/openapi/definition-apiv3.js" \
   "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/questionnaire/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/questionnaire/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
@@ -17,3 +17,8 @@ swagger-jsdoc \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \
   "${APP_PATH}/src/server/routes/login.js" \
   "${APP_PATH}/src/server/routes/login.js" \
   "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"
   "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"
+
+if [ $? -eq 0 ]; then
+  npx tsx "${APP_PATH}/bin/openapi/generate-operation-ids/cli.ts" "${OUT}" --out "${OUT}" --overwrite-existing
+  echo "OpenAPI spec generated and transformed: ${OUT}"
+fi

+ 0 - 15
apps/app/bin/swagger-jsdoc/generate-spec-apiv1.sh

@@ -1,15 +0,0 @@
-# USAGE:
-#   cd apps/app && sh bin/swagger-jsdoc/generate-spec-apiv1.sh
-#   APP_PATH=/path/to/apps/app sh bin/swagger-jsdoc/generate-spec-apiv1.sh
-#   APP_PATH=/path/to/apps/app OUT=/path/to/output sh bin/swagger-jsdoc/generate-spec-apiv1.sh
-
-APP_PATH=${APP_PATH:-"."}
-
-OUT=${OUT:-"${APP_PATH}/tmp/openapi-spec-apiv1.json"}
-
-swagger-jsdoc \
-  -o "${OUT}" \
-  -d "${APP_PATH}/bin/swagger-jsdoc/definition-apiv1.js" \
-  "${APP_PATH}/src/server/routes/*.{js,ts}" \
-  "${APP_PATH}/src/server/routes/attachment/**/*.{js,ts}" \
-  "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"

+ 12 - 8
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.2.6-RC.0",
+  "version": "7.2.8-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": "true",
   "private": "true",
   "scripts": {
   "scripts": {
@@ -27,13 +27,13 @@
     "//// for CI": "",
     "//// for CI": "",
     "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
     "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
     "lint:typecheck": "vue-tsc --noEmit",
     "lint:typecheck": "vue-tsc --noEmit",
-    "lint:eslint": "eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
+    "lint:eslint": "eslint --quiet \"**/*.{js,mjs,jsx,ts,mts,tsx}\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",
-    "lint:swagger2openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
-    "lint:swagger2openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
+    "lint:openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
+    "lint:openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
     "lint": "run-p lint:**",
     "lint": "run-p lint:**",
-    "prelint:swagger2openapi:apiv3": "pnpm run swagger2openapi:apiv3",
-    "prelint:swagger2openapi:apiv1": "pnpm run swagger2openapi:apiv1",
+    "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:*",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
     "test:jest": "cross-env NODE_ENV=test TS_NODE_PROJECT=test/integration/tsconfig.json jest",
     "test:vitest": "vitest run --coverage",
     "test:vitest": "vitest run --coverage",
@@ -43,8 +43,9 @@
     "//// misc": "",
     "//// misc": "",
     "console": "npm run repl",
     "console": "npm run repl",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
-    "swagger2openapi:apiv3": "sh bin/swagger-jsdoc/generate-spec-apiv3.sh",
-    "swagger2openapi:apiv1": "sh bin/swagger-jsdoc/generate-spec-apiv1.sh",
+    "openapi:build:generate-operation-ids": "vite build -c bin/openapi/generate-operation-ids/vite.config.ts",
+    "openapi:generate-spec:apiv3": "sh bin/openapi/generate-spec-apiv3.sh",
+    "openapi:generate-spec:apiv1": "sh bin/openapi/generate-spec-apiv1.sh",
     "ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config",
     "ts-node": "node -r ts-node/register/transpile-only -r tsconfig-paths/register -r dotenv-flow/config",
     "version:patch": "pnpm version patch",
     "version:patch": "pnpm version patch",
     "version:prerelease": "pnpm version prerelease --preid=RC",
     "version:prerelease": "pnpm version prerelease --preid=RC",
@@ -258,6 +259,7 @@
     "mongodb": "mongoose which is used requires mongo@4.16.0."
     "mongodb": "mongoose which is used requires mongo@4.16.0."
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@apidevtools/swagger-parser": "^10.1.1",
     "@emoji-mart/data": "^1.2.1",
     "@emoji-mart/data": "^1.2.1",
     "@growi/core-styles": "workspace:^",
     "@growi/core-styles": "workspace:^",
     "@growi/custom-icons": "workspace:^",
     "@growi/custom-icons": "workspace:^",
@@ -293,6 +295,7 @@
     "@types/uuid": "^10.0.0",
     "@types/uuid": "^10.0.0",
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
     "bootstrap": "=5.3.2",
     "bootstrap": "=5.3.2",
+    "commander": "^14.0.0",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
     "diff2html": "^3.4.47",
     "diff2html": "^3.4.47",
     "downshift": "^8.2.3",
     "downshift": "^8.2.3",
@@ -315,6 +318,7 @@
     "mongodb-memory-server-core": "^9.1.1",
     "mongodb-memory-server-core": "^9.1.1",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",
     "null-loader": "^4.0.1",
+    "openapi-typescript": "^7.8.0",
     "pretty-bytes": "^6.1.1",
     "pretty-bytes": "^6.1.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dnd": "^14.0.5",
     "react-dnd": "^14.0.5",

+ 2 - 2
apps/app/playwright/40-admin/access-to-admin-page.spec.ts

@@ -21,8 +21,8 @@ test('admin/security is successfully loaded', async({ page }) => {
   await page.goto('/admin/security');
   await page.goto('/admin/security');
 
 
   await expect(page.getByTestId('admin-security')).toBeVisible();
   await expect(page.getByTestId('admin-security')).toBeVisible();
-  await expect(page.locator('#isShowRestrictedByOwner')).not.toBeChecked();
-  await expect(page.locator('#isShowRestrictedByGroup')).not.toBeChecked();
+  await expect(page.locator('#isShowRestrictedByOwner')).toHaveText('Always displayed');
+  await expect(page.locator('#isShowRestrictedByGroup')).toHaveText('Always displayed');
 });
 });
 
 
 test('admin/markdown is successfully loaded', async({ page }) => {
 test('admin/markdown is successfully loaded', async({ page }) => {

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

@@ -19,7 +19,6 @@
     "readonly_users_access": "Read only users' access",
     "readonly_users_access": "Read only users' access",
     "always_hidden": "Always hidden",
     "always_hidden": "Always hidden",
     "always_displayed": "Always displayed",
     "always_displayed": "Always displayed",
-    "displayed_or_hidden": "Hidden / Displayed",
     "Fixed by env var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
     "Fixed by env var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
     "register_limitation": "Register limitation",
     "register_limitation": "Register limitation",
     "register_limitation_desc": "Restriction of new users' registration",
     "register_limitation_desc": "Restriction of new users' registration",

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

@@ -19,7 +19,6 @@
     "readonly_users_access": "Accès des utilisateurs lecture seule",
     "readonly_users_access": "Accès des utilisateurs lecture seule",
     "always_hidden": "Toujours caché",
     "always_hidden": "Toujours caché",
     "always_displayed": "Toujours affiché",
     "always_displayed": "Toujours affiché",
-    "displayed_or_hidden": "Caché / Affiché",
     "Fixed by env var": "Configuré par la variable d'environnement <code>{{key}}={{value}}</code>.",
     "Fixed by env var": "Configuré par la variable d'environnement <code>{{key}}={{value}}</code>.",
     "register_limitation": "Paramètres d'inscription",
     "register_limitation": "Paramètres d'inscription",
     "register_limitation_desc": "Restreindre l'inscription de nouveaux utilisateurs",
     "register_limitation_desc": "Restreindre l'inscription de nouveaux utilisateurs",

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

@@ -13,22 +13,21 @@
   "Execute": "実行",
   "Execute": "実行",
   "last_login": "最終ログイン",
   "last_login": "最終ログイン",
   "wiki_management_homepage": "Wiki管理トップ",
   "wiki_management_homepage": "Wiki管理トップ",
-  "public": "公開",
-  "anyone_with_the_link": "リンクを知っている人のみ",
+  "public": "公開」のページ",
+  "anyone_with_the_link": "リンクを知っている人のみ」のページ",
   "specified_users": "特定ユーザーのみ",
   "specified_users": "特定ユーザーのみ",
-  "only_me": "自分のみ",
-  "only_inside_the_group": "特定グループのみ",
+  "only_me": "自分のみ」のページ",
+  "only_inside_the_group": "特定グループのみ」のページ",
   "optional": "オプション",
   "optional": "オプション",
   "days": "日",
   "days": "日",
   "security_settings": {
   "security_settings": {
     "security_settings": "セキュリティ設定",
     "security_settings": "セキュリティ設定",
     "scope_of_page_disclosure": "ページの公開範囲",
     "scope_of_page_disclosure": "ページの公開範囲",
     "set_point": "設定値",
     "set_point": "設定値",
-    "Guest Users Access":"ゲストユーザーのアクセス",
+    "Guest Users Access": "ゲストユーザーのアクセス",
     "readonly_users_access": "閲覧のみユーザーのアクセス",
     "readonly_users_access": "閲覧のみユーザーのアクセス",
-    "always_hidden": "非表示 (固定)",
-    "always_displayed": "表示 (固定)",
-    "displayed_or_hidden": "非表示 / 表示",
+    "always_hidden": "表示しない",
+    "always_displayed": "表示する",
     "Fixed by env var": "環境変数 <code>{{forcewikimode}}={{wikimode}}</code> により固定されています。",
     "Fixed by env var": "環境変数 <code>{{forcewikimode}}={{wikimode}}</code> により固定されています。",
     "register_limitation": "登録の制限",
     "register_limitation": "登録の制限",
     "register_limitation_desc": "新しいユーザーを登録する方法を制限します。",
     "register_limitation_desc": "新しいユーザーを登録する方法を制限します。",
@@ -73,7 +72,7 @@
     "forced_update_desc": "設定が強制変更されました。前回の設定: ",
     "forced_update_desc": "設定が強制変更されました。前回の設定: ",
     "page_delete_rights_caution": "「(子孫ページを含む)ゴミ箱に入れる操作 / 完全に削除する」の権限は、「ゴミ箱に入れる操作 / 完全に削除する」よりも強い権限になるように強制されます。 <br><br> 管理者のみ可能 > 管理者とページ作者が可能 > 誰でも可能",
     "page_delete_rights_caution": "「(子孫ページを含む)ゴミ箱に入れる操作 / 完全に削除する」の権限は、「ゴミ箱に入れる操作 / 完全に削除する」よりも強い権限になるように強制されます。 <br><br> 管理者のみ可能 > 管理者とページ作者が可能 > 誰でも可能",
     "Authentication mechanism settings": "認証機構設定",
     "Authentication mechanism settings": "認証機構設定",
-    "setup_is_not_yet_complete":"セットアップはまだ完了してません",
+    "setup_is_not_yet_complete": "セットアップはまだ完了してません",
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "callback_URL": "コールバックURL",
@@ -107,9 +106,9 @@
       "closed": "非公開 (登録には管理者による招待が必要)"
       "closed": "非公開 (登録には管理者による招待が必要)"
     },
     },
     "share_link_management": "共有リンク管理",
     "share_link_management": "共有リンク管理",
-    "No_share_links":"共有リンクが存在しません",
-    "share_link_notice":"共有リンクを全て削除します",
-    "delete_all_share_links":"全ての共有リンクを削除します",
+    "No_share_links": "共有リンクが存在しません",
+    "share_link_notice": "共有リンクを全て削除します",
+    "delete_all_share_links": "全ての共有リンクを削除します",
     "share_link_rights": "シェアリンクの権限",
     "share_link_rights": "シェアリンクの権限",
     "enable_link_sharing": "リンクのシェアを許可",
     "enable_link_sharing": "リンクのシェアを許可",
     "all_share_links": "全てのシェアリンク",
     "all_share_links": "全てのシェアリンク",
@@ -512,13 +511,13 @@
       "show_page_side_authors": "作成者・更新者を目次上部に常時表示する",
       "show_page_side_authors": "作成者・更新者を目次上部に常時表示する",
       "show_page_side_authors_desc": "ページサイドバーの目次上部に作成者と最終更新者の情報を表示します。"
       "show_page_side_authors_desc": "ページサイドバーの目次上部に作成者と最終更新者の情報を表示します。"
     },
     },
-    "presentation":"プレゼンテーション",
-    "presentation_options":{
+    "presentation": "プレゼンテーション",
+    "presentation_options": {
       "enable_marp": "Marp を有効化する",
       "enable_marp": "Marp を有効化する",
       "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
       "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
       "marp_official_site": "参考:Marp 公式サイト",
       "marp_official_site": "参考:Marp 公式サイト",
       "marp_official_site_link": "https://marp.app",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "参考:GROWI Docs - Marp でスライドを作成する",
+      "marp_in_growi": "参考:GROWI Docs - Marp でスライドを作成する",
       "marp_in_growi_link": "https://docs.growi.org/ja/guide/features/marp.html"
       "marp_in_growi_link": "https://docs.growi.org/ja/guide/features/marp.html"
     },
     },
     "custom_title": "カスタム Title",
     "custom_title": "カスタム Title",
@@ -532,7 +531,7 @@
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "ctrl_space": "Ctrl+Space でコード補完",
     "ctrl_space": "Ctrl+Space でコード補完",
     "custom_script": "カスタムスクリプト",
     "custom_script": "カスタムスクリプト",
-    "custom_presentation":"プレゼンテーション",
+    "custom_presentation": "プレゼンテーション",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "reflect_change": "変更の反映はページの更新が必要です。",
     "reflect_change": "変更の反映はページの更新が必要です。",
     "custom_logo": "カスタムロゴ",
     "custom_logo": "カスタムロゴ",
@@ -541,7 +540,7 @@
     "current_logo": "現在のロゴ",
     "current_logo": "現在のロゴ",
     "upload_new_logo": "新しいロゴをアップロードする",
     "upload_new_logo": "新しいロゴをアップロードする",
     "delete_logo": "ロゴを削除"
     "delete_logo": "ロゴを削除"
-   },
+  },
   "importer_management": {
   "importer_management": {
     "import_data": "データインポート",
     "import_data": "データインポート",
     "article": "記事",
     "article": "記事",
@@ -681,7 +680,7 @@
     "delete": "削除",
     "delete": "削除",
     "integration_procedure": "連携手順",
     "integration_procedure": "連携手順",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy 設定",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy 設定",
-    "integration_failed":"連携に失敗しました",
+    "integration_failed": "連携に失敗しました",
     "reset": "リセット",
     "reset": "リセット",
     "reset_all_settings": "全ての設定をリセット",
     "reset_all_settings": "全ての設定をリセット",
     "delete_slackbot_settings": "Slack Bot 設定を削除する",
     "delete_slackbot_settings": "Slack Bot 設定を削除する",
@@ -728,7 +727,7 @@
       "allow_specified_long": "特定のチャンネルを許可 (テキストボックスに入力されたチャンネルのみ許可されます)",
       "allow_specified_long": "特定のチャンネルを許可 (テキストボックスに入力されたチャンネルのみ許可されます)",
       "test_connection": "連携状況のテストをする",
       "test_connection": "連携状況のテストをする",
       "test_connection_by_pressing_button": "以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう",
       "test_connection_by_pressing_button": "以下のテストボタンを押して、Slack連携が完了しているかの確認をしましょう",
-      "test_connection_only_public_channel":"連携テストは public チャンネルで確認してください",
+      "test_connection_only_public_channel": "連携テストは public チャンネルで確認してください",
       "error_check_logs_below": "エラーが発生しました。下記のログを確認してください。",
       "error_check_logs_below": "エラーが発生しました。下記のログを確認してください。",
       "send_message_to_slack_work_space": "Slack ワークスペースに送信しました",
       "send_message_to_slack_work_space": "Slack ワークスペースに送信しました",
       "add_slack_workspace": "Slackワークスペースを追加"
       "add_slack_workspace": "Slackワークスペースを追加"
@@ -752,7 +751,7 @@
     }
     }
   },
   },
   "slack_integration_legacy": {
   "slack_integration_legacy": {
-    "slack_integration_legacy":  "Slack連携 (レガシー)",
+    "slack_integration_legacy": "Slack連携 (レガシー)",
     "alert_disabled": "<a href='/admin/slack-integration'>新しい設定</a>が有効になっているため、この 'Slack連携 (レガシー)' は現在無効になっています。",
     "alert_disabled": "<a href='/admin/slack-integration'>新しい設定</a>が有効になっているため、この 'Slack連携 (レガシー)' は現在無効になっています。",
     "alert_deplicated": "この 'Slack連携 (レガシー)' は将来廃止されます。代わりに<a href='/admin/slack-integration'>新しいSlack連携機能</a>を利用してください。"
     "alert_deplicated": "この 'Slack連携 (レガシー)' は将来廃止されます。代わりに<a href='/admin/slack-integration'>新しいSlack連携機能</a>を利用してください。"
   },
   },
@@ -989,7 +988,7 @@
     "ADMIN_SITE_URL_UPDATE": "サイトURL設定の更新",
     "ADMIN_SITE_URL_UPDATE": "サイトURL設定の更新",
     "ADMIN_MAIL_SMTP_UPDATE": "メール設定(SMTP)の更新",
     "ADMIN_MAIL_SMTP_UPDATE": "メール設定(SMTP)の更新",
     "ADMIN_MAIL_SES_UPDATE": "メール設定(SES)の更新",
     "ADMIN_MAIL_SES_UPDATE": "メール設定(SES)の更新",
-    "ADMIN_MAIL_TEST_SUBMIT" : "テストメールの送信",
+    "ADMIN_MAIL_TEST_SUBMIT": "テストメールの送信",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "ファイルアップロード設定の更新",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "ファイルアップロード設定の更新",
     "ADMIN_PLUGIN_UPDATE": "プラグイン設定の更新",
     "ADMIN_PLUGIN_UPDATE": "プラグイン設定の更新",
     "ADMIN_MAINTENANCEMODE_ENABLED": "メンテナンスモードの開始",
     "ADMIN_MAINTENANCEMODE_ENABLED": "メンテナンスモードの開始",

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

@@ -26,7 +26,6 @@
     "set_point": "设定值",
     "set_point": "设定值",
     "always_displayed": "始终显示",
     "always_displayed": "始终显示",
     "always_hidden": "总是隐藏",
     "always_hidden": "总是隐藏",
-    "displayed_or_hidden": "隐藏 / 显示",
     "Guest Users Access": "来宾用户访问",
     "Guest Users Access": "来宾用户访问",
     "readonly_users_access": "只浏览用户的访问",
     "readonly_users_access": "只浏览用户的访问",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",

+ 2 - 2
apps/app/src/client/components/Admin/App/AppSetting.jsx

@@ -40,7 +40,7 @@ const AppSetting = (props) => {
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            defaletValue={adminAppContainer.state.title || ''}
+            value={adminAppContainer.state.title || ''}
             onChange={(e) => {
             onChange={(e) => {
               adminAppContainer.changeTitle(e.target.value);
               adminAppContainer.changeTitle(e.target.value);
             }}
             }}
@@ -60,7 +60,7 @@ const AppSetting = (props) => {
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            defaultValue={adminAppContainer.state.confidential || ''}
+            value={adminAppContainer.state.confidential || ''}
             onChange={(e) => {
             onChange={(e) => {
               adminAppContainer.changeConfidential(e.target.value);
               adminAppContainer.changeConfidential(e.target.value);
             }}
             }}

+ 4 - 4
apps/app/src/client/components/Admin/App/AwsSetting.tsx

@@ -76,7 +76,7 @@ export const AwsSettingMolecule = (props: AwsSettingMoleculeProps): JSX.Element
           <input
           <input
             className="form-control"
             className="form-control"
             placeholder={`${t('eg')} ap-northeast-1`}
             placeholder={`${t('eg')} ap-northeast-1`}
-            defaultValue={props.s3Region || ''}
+            value={props.s3Region || ''}
             onChange={(e) => {
             onChange={(e) => {
               props?.onChangeS3Region(e.target.value);
               props?.onChangeS3Region(e.target.value);
             }}
             }}
@@ -93,7 +93,7 @@ export const AwsSettingMolecule = (props: AwsSettingMoleculeProps): JSX.Element
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             placeholder={`${t('eg')} http://localhost:9000`}
             placeholder={`${t('eg')} http://localhost:9000`}
-            defaultValue={props.s3CustomEndpoint || ''}
+            value={props.s3CustomEndpoint || ''}
             onChange={(e) => {
             onChange={(e) => {
               props?.onChangeS3CustomEndpoint(e.target.value);
               props?.onChangeS3CustomEndpoint(e.target.value);
             }}
             }}
@@ -111,7 +111,7 @@ export const AwsSettingMolecule = (props: AwsSettingMoleculeProps): JSX.Element
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             placeholder={`${t('eg')} crowi`}
             placeholder={`${t('eg')} crowi`}
-            defaultValue={props.s3Bucket || ''}
+            value={props.s3Bucket || ''}
             onChange={(e) => {
             onChange={(e) => {
               props.onChangeS3Bucket(e.target.value);
               props.onChangeS3Bucket(e.target.value);
             }}
             }}
@@ -127,7 +127,7 @@ export const AwsSettingMolecule = (props: AwsSettingMoleculeProps): JSX.Element
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            defaultValue={props.s3AccessKeyId || ''}
+            value={props.s3AccessKeyId || ''}
             onChange={(e) => {
             onChange={(e) => {
               props?.onChangeS3AccessKeyId(e.target.value);
               props?.onChangeS3AccessKeyId(e.target.value);
             }}
             }}

+ 8 - 8
apps/app/src/client/components/Admin/App/AzureSetting.tsx

@@ -118,12 +118,12 @@ export const AzureSettingMolecule = (props: AzureSettingMoleculeProps): JSX.Elem
               <MaskedInput
               <MaskedInput
                 name="azureTenantId"
                 name="azureTenantId"
                 readOnly={azureUseOnlyEnvVars}
                 readOnly={azureUseOnlyEnvVars}
-                defaultValue={azureTenantId}
+                value={azureTenantId}
                 onChange={e => props?.onChangeAzureTenantId(e.target.value)}
                 onChange={e => props?.onChangeAzureTenantId(e.target.value)}
               />
               />
             </td>
             </td>
             <td>
             <td>
-              <MaskedInput name="envAzureTenantId" defaultValue={envAzureTenantId || ''} readOnly tabIndex={-1} />
+              <MaskedInput name="envAzureTenantId" value={envAzureTenantId || ''} readOnly tabIndex={-1} />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_TENANT_ID' }) }} />
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_TENANT_ID' }) }} />
@@ -136,12 +136,12 @@ export const AzureSettingMolecule = (props: AzureSettingMoleculeProps): JSX.Elem
               <MaskedInput
               <MaskedInput
                 name="azureClientId"
                 name="azureClientId"
                 readOnly={azureUseOnlyEnvVars}
                 readOnly={azureUseOnlyEnvVars}
-                defaultValue={azureClientId}
+                value={azureClientId}
                 onChange={e => props?.onChangeAzureClientId(e.target.value)}
                 onChange={e => props?.onChangeAzureClientId(e.target.value)}
               />
               />
             </td>
             </td>
             <td>
             <td>
-              <MaskedInput name="envAzureClientId" defaultValue={envAzureClientId || ''} readOnly tabIndex={-1} />
+              <MaskedInput name="envAzureClientId" value={envAzureClientId || ''} readOnly tabIndex={-1} />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_CLIENT_ID' }) }} />
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_CLIENT_ID' }) }} />
@@ -154,12 +154,12 @@ export const AzureSettingMolecule = (props: AzureSettingMoleculeProps): JSX.Elem
               <MaskedInput
               <MaskedInput
                 name="azureClientSecret"
                 name="azureClientSecret"
                 readOnly={azureUseOnlyEnvVars}
                 readOnly={azureUseOnlyEnvVars}
-                defaultValue={azureClientSecret}
+                value={azureClientSecret}
                 onChange={e => props?.onChangeAzureClientSecret(e.target.value)}
                 onChange={e => props?.onChangeAzureClientSecret(e.target.value)}
               />
               />
             </td>
             </td>
             <td>
             <td>
-              <MaskedInput name="envAzureClientSecret" defaultValue={envAzureClientSecret || ''} readOnly tabIndex={-1} />
+              <MaskedInput name="envAzureClientSecret" value={envAzureClientSecret || ''} readOnly tabIndex={-1} />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_CLIENT_SECRET' }) }} />
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'AZURE_CLIENT_SECRET' }) }} />
@@ -174,7 +174,7 @@ export const AzureSettingMolecule = (props: AzureSettingMoleculeProps): JSX.Elem
                 type="text"
                 type="text"
                 name="azureStorageAccountName"
                 name="azureStorageAccountName"
                 readOnly={azureUseOnlyEnvVars}
                 readOnly={azureUseOnlyEnvVars}
-                defaultValue={azureStorageAccountName}
+                value={azureStorageAccountName}
                 onChange={e => props?.onChangeAzureStorageAccountName(e.target.value)}
                 onChange={e => props?.onChangeAzureStorageAccountName(e.target.value)}
               />
               />
             </td>
             </td>
@@ -194,7 +194,7 @@ export const AzureSettingMolecule = (props: AzureSettingMoleculeProps): JSX.Elem
                 type="text"
                 type="text"
                 name="azureStorageContainerName"
                 name="azureStorageContainerName"
                 readOnly={azureUseOnlyEnvVars}
                 readOnly={azureUseOnlyEnvVars}
-                defaultValue={azureStorageContainerName}
+                value={azureStorageContainerName}
                 onChange={e => props?.onChangeAzureStorageContainerName(e.target.value)}
                 onChange={e => props?.onChangeAzureStorageContainerName(e.target.value)}
               />
               />
             </td>
             </td>

+ 3 - 3
apps/app/src/client/components/Admin/App/GcsSetting.tsx

@@ -108,7 +108,7 @@ export const GcsSettingMolecule = (props: GcsSettingMoleculeProps): JSX.Element
                 type="text"
                 type="text"
                 name="gcsApiKeyJsonPath"
                 name="gcsApiKeyJsonPath"
                 readOnly={gcsUseOnlyEnvVars}
                 readOnly={gcsUseOnlyEnvVars}
-                defaultValue={gcsApiKeyJsonPath}
+                value={gcsApiKeyJsonPath}
                 onChange={e => props?.onChangeGcsApiKeyJsonPath(e.target.value)}
                 onChange={e => props?.onChangeGcsApiKeyJsonPath(e.target.value)}
               />
               />
             </td>
             </td>
@@ -128,7 +128,7 @@ export const GcsSettingMolecule = (props: GcsSettingMoleculeProps): JSX.Element
                 type="text"
                 type="text"
                 name="gcsBucket"
                 name="gcsBucket"
                 readOnly={gcsUseOnlyEnvVars}
                 readOnly={gcsUseOnlyEnvVars}
-                defaultValue={gcsBucket}
+                value={gcsBucket}
                 onChange={e => props?.onChangeGcsBucket(e.target.value)}
                 onChange={e => props?.onChangeGcsBucket(e.target.value)}
               />
               />
             </td>
             </td>
@@ -148,7 +148,7 @@ export const GcsSettingMolecule = (props: GcsSettingMoleculeProps): JSX.Element
                 type="text"
                 type="text"
                 name="gcsUploadNamespace"
                 name="gcsUploadNamespace"
                 readOnly={gcsUseOnlyEnvVars}
                 readOnly={gcsUseOnlyEnvVars}
-                defaultValue={gcsUploadNamespace}
+                value={gcsUploadNamespace}
                 onChange={e => props?.onChangeGcsUploadNamespace(e.target.value)}
                 onChange={e => props?.onChangeGcsUploadNamespace(e.target.value)}
               />
               />
             </td>
             </td>

+ 1 - 1
apps/app/src/client/components/Admin/App/MailSetting.tsx

@@ -56,7 +56,7 @@ const MailSetting = (props: Props) => {
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             placeholder={`${t('eg')} mail@growi.org`}
             placeholder={`${t('eg')} mail@growi.org`}
-            defaultValue={adminAppContainer.state.fromAddress || ''}
+            value={adminAppContainer.state.fromAddress || ''}
             onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
             onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
           />
           />
         </div>
         </div>

+ 3 - 3
apps/app/src/client/components/Admin/App/MaskedInput.tsx

@@ -5,7 +5,7 @@ import styles from './MaskedInput.module.scss';
 type Props = {
 type Props = {
   name: string
   name: string
   readOnly: boolean
   readOnly: boolean
-  defaultValue: string
+  value: string
   onChange?: (e: any) => void
   onChange?: (e: any) => void
   tabIndex?: number | undefined
   tabIndex?: number | undefined
 };
 };
@@ -17,7 +17,7 @@ export default function MaskedInput(props: Props): JSX.Element {
   };
   };
 
 
   const {
   const {
-    name, readOnly, defaultValue, onChange, tabIndex,
+    name, readOnly, value, onChange, tabIndex,
   } = props;
   } = props;
 
 
   return (
   return (
@@ -27,7 +27,7 @@ export default function MaskedInput(props: Props): JSX.Element {
         type={passwordShown ? 'text' : 'password'}
         type={passwordShown ? 'text' : 'password'}
         name={name}
         name={name}
         readOnly={readOnly}
         readOnly={readOnly}
-        defaultValue={defaultValue}
+        value={value}
         onChange={onChange}
         onChange={onChange}
         tabIndex={tabIndex}
         tabIndex={tabIndex}
       />
       />

+ 2 - 2
apps/app/src/client/components/Admin/App/SesSetting.tsx

@@ -24,7 +24,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              defaultValue={adminAppContainer.state.sesAccessKeyId || ''}
+              value={adminAppContainer.state.sesAccessKeyId || ''}
               onChange={(e) => {
               onChange={(e) => {
                 adminAppContainer.changeSesAccessKeyId(e.target.value);
                 adminAppContainer.changeSesAccessKeyId(e.target.value);
               }}
               }}
@@ -40,7 +40,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              defaultValue={adminAppContainer.state.sesSecretAccessKey || ''}
+              value={adminAppContainer.state.sesSecretAccessKey || ''}
               onChange={(e) => {
               onChange={(e) => {
                 adminAppContainer.changeSesSecretAccessKey(e.target.value);
                 adminAppContainer.changeSesSecretAccessKey(e.target.value);
               }}
               }}

+ 1 - 1
apps/app/src/client/components/Admin/App/SiteUrlSetting.tsx

@@ -70,7 +70,7 @@ const SiteUrlSetting = (props: Props) => {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="settingForm[app:siteUrl]"
                   name="settingForm[app:siteUrl]"
-                  defaultValue={adminAppContainer.state.siteUrl || ''}
+                  value={adminAppContainer.state.siteUrl || ''}
                   disabled={adminAppContainer.state.siteUrlUseOnlyEnvVars ?? true}
                   disabled={adminAppContainer.state.siteUrlUseOnlyEnvVars ?? true}
                   onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
                   onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
                   placeholder="e.g. https://my.growi.org"
                   placeholder="e.g. https://my.growi.org"

+ 4 - 4
apps/app/src/client/components/Admin/App/SmtpSetting.tsx

@@ -27,7 +27,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              defaultValue={adminAppContainer.state.smtpHost || ''}
+              value={adminAppContainer.state.smtpHost || ''}
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
             />
             />
           </div>
           </div>
@@ -40,7 +40,7 @@ const SmtpSetting = (props: Props) => {
           <div className="col-md-6">
           <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
-              defaultValue={adminAppContainer.state.smtpPort || ''}
+              value={adminAppContainer.state.smtpPort || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
             />
             />
           </div>
           </div>
@@ -54,7 +54,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              defaultValue={adminAppContainer.state.smtpUser || ''}
+              value={adminAppContainer.state.smtpUser || ''}
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
             />
             />
           </div>
           </div>
@@ -68,7 +68,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="password"
               type="password"
-              defaultValue={adminAppContainer.state.smtpPassword || ''}
+              value={adminAppContainer.state.smtpPassword || ''}
               onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
               onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
             />
             />
           </div>
           </div>

+ 1 - 1
apps/app/src/client/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -46,7 +46,7 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
               className="form-control"
               className="form-control"
               name="customizeCss"
               name="customizeCss"
               rows={8}
               rows={8}
-              defaultValue={adminCustomizeContainer.state.currentCustomizeCss || ''}
+              value={adminCustomizeContainer.state.currentCustomizeCss || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
             />
             />
           </div>
           </div>

+ 1 - 1
apps/app/src/client/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -50,7 +50,7 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
               className="form-control mb-2"
               className="form-control mb-2"
               name="customizeNoscript"
               name="customizeNoscript"
               rows={8}
               rows={8}
-              defaultValue={adminCustomizeContainer.state.currentCustomizeNoscript || ''}
+              value={adminCustomizeContainer.state.currentCustomizeNoscript || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeNoscript(e.target.value) }}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeNoscript(e.target.value) }}
             />
             />
           </div>
           </div>

+ 1 - 1
apps/app/src/client/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -47,7 +47,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
               className="form-control mb-2"
               className="form-control mb-2"
               name="customizeScript"
               name="customizeScript"
               rows={8}
               rows={8}
-              defaultValue={adminCustomizeContainer.state.currentCustomizeScript || ''}
+              value={adminCustomizeContainer.state.currentCustomizeScript || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
             />
             />
           </div>
           </div>

+ 1 - 1
apps/app/src/client/components/Admin/Customize/CustomizeTitle.tsx

@@ -67,7 +67,7 @@ export const CustomizeTitle: FC = () => {
         <div className="col-12">
         <div className="col-12">
           <input
           <input
             className="form-control"
             className="form-control"
-            defaultValue={currentCustomizeTitle}
+            value={currentCustomizeTitle}
             onChange={(e) => { setCrrentCustomizeTitle(e.target.value) }}
             onChange={(e) => { setCrrentCustomizeTitle(e.target.value) }}
           />
           />
         </div>
         </div>

+ 2 - 2
apps/app/src/client/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -70,7 +70,7 @@ class SlackConfiguration extends React.Component {
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
-                  defaultValue={adminSlackIntegrationLegacyContainer.state.webhookUrl || ''}
+                  value={adminSlackIntegrationLegacyContainer.state.webhookUrl || ''}
                   onChange={e => adminSlackIntegrationLegacyContainer.changeWebhookUrl(e.target.value)}
                   onChange={e => adminSlackIntegrationLegacyContainer.changeWebhookUrl(e.target.value)}
                 />
                 />
               </div>
               </div>
@@ -122,7 +122,7 @@ class SlackConfiguration extends React.Component {
                   <input
                   <input
                     className="form-control"
                     className="form-control"
                     type="text"
                     type="text"
-                    defaultValue={adminSlackIntegrationLegacyContainer.state.slackToken || ''}
+                    value={adminSlackIntegrationLegacyContainer.state.slackToken || ''}
                     onChange={e => adminSlackIntegrationLegacyContainer.changeSlackToken(e.target.value)}
                     onChange={e => adminSlackIntegrationLegacyContainer.changeSlackToken(e.target.value)}
                   />
                   />
                 </div>
                 </div>

+ 2 - 2
apps/app/src/client/components/Admin/MarkdownSetting/WhitelistInput.tsx

@@ -52,7 +52,7 @@ export const WhitelistInput = (props: Props): JSX.Element => {
           name="recommendedTags"
           name="recommendedTags"
           rows={6}
           rows={6}
           cols={40}
           cols={40}
-          defaultValue={adminMarkDownContainer.state.tagWhitelist}
+          value={adminMarkDownContainer.state.tagWhitelist}
           onChange={(e) => { adminMarkDownContainer.setState({ tagWhitelist: e.target.value }) }}
           onChange={(e) => { adminMarkDownContainer.setState({ tagWhitelist: e.target.value }) }}
         />
         />
       </div>
       </div>
@@ -69,7 +69,7 @@ export const WhitelistInput = (props: Props): JSX.Element => {
           name="recommendedAttrs"
           name="recommendedAttrs"
           rows={6}
           rows={6}
           cols={40}
           cols={40}
-          defaultValue={adminMarkDownContainer.state.attrWhitelist}
+          value={adminMarkDownContainer.state.attrWhitelist}
           onChange={(e) => { adminMarkDownContainer.setState({ attrWhitelist: e.target.value }) }}
           onChange={(e) => { adminMarkDownContainer.setState({ attrWhitelist: e.target.value }) }}
         />
         />
       </div>
       </div>

+ 1 - 1
apps/app/src/client/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -125,7 +125,7 @@ class GitHubSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="githubClientSecret"
                   name="githubClientSecret"
-                  defaultValue={adminGitHubSecurityContainer.state.githubClientSecret || ''}
+                  value={adminGitHubSecurityContainer.state.githubClientSecret || ''}
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientSecret(e.target.value)}
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientSecret(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">

+ 3 - 3
apps/app/src/client/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -108,7 +108,7 @@ class GoogleSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="googleClientId"
                   name="googleClientId"
-                  defaultValue={adminGoogleSecurityContainer.state.googleClientId || ''}
+                  value={adminGoogleSecurityContainer.state.googleClientId || ''}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientId(e.target.value)}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientId(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -122,9 +122,9 @@ class GoogleSecurityManagementContents extends React.Component {
               <div className="col-6">
               <div className="col-6">
                 <input
                 <input
                   className="form-control"
                   className="form-control"
-                  type="text"
+                  type="password"
                   name="googleClientSecret"
                   name="googleClientSecret"
-                  defaultValue={adminGoogleSecurityContainer.state.googleClientSecret || ''}
+                  value={adminGoogleSecurityContainer.state.googleClientSecret || ''}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientSecret(e.target.value)}
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientSecret(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">

+ 10 - 10
apps/app/src/client/components/Admin/Security/LdapSecuritySettingContents.jsx

@@ -92,7 +92,7 @@ class LdapSecuritySettingContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="serverUrl"
                   name="serverUrl"
-                  defaultValue={adminLdapSecurityContainer.state.serverUrl || ''}
+                  value={adminLdapSecurityContainer.state.serverUrl || ''}
                   onChange={e => adminLdapSecurityContainer.changeServerUrl(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeServerUrl(e.target.value)}
                 />
                 />
                 <small>
                 <small>
@@ -145,7 +145,7 @@ class LdapSecuritySettingContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="bindDN"
                   name="bindDN"
-                  defaultValue={adminLdapSecurityContainer.state.ldapBindDN || ''}
+                  value={adminLdapSecurityContainer.state.ldapBindDN || ''}
                   onChange={e => adminLdapSecurityContainer.changeBindDN(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeBindDN(e.target.value)}
                 />
                 />
                 {(adminLdapSecurityContainer.state.isUserBind === true) ? (
                 {(adminLdapSecurityContainer.state.isUserBind === true) ? (
@@ -194,7 +194,7 @@ class LdapSecuritySettingContents extends React.Component {
                         className="form-control passport-ldap-managerbind"
                         className="form-control passport-ldap-managerbind"
                         type="password"
                         type="password"
                         name="bindDNPassword"
                         name="bindDNPassword"
-                        defaultValue={adminLdapSecurityContainer.state.ldapBindDNPassword || ''}
+                        value={adminLdapSecurityContainer.state.ldapBindDNPassword || ''}
                         onChange={e => adminLdapSecurityContainer.changeBindDNPassword(e.target.value)}
                         onChange={e => adminLdapSecurityContainer.changeBindDNPassword(e.target.value)}
                       />
                       />
                     </>
                     </>
@@ -211,7 +211,7 @@ class LdapSecuritySettingContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="searchFilter"
                   name="searchFilter"
-                  defaultValue={adminLdapSecurityContainer.state.ldapSearchFilter || ''}
+                  value={adminLdapSecurityContainer.state.ldapSearchFilter || ''}
                   onChange={e => adminLdapSecurityContainer.changeSearchFilter(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeSearchFilter(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -248,7 +248,7 @@ class LdapSecuritySettingContents extends React.Component {
                   type="text"
                   type="text"
                   placeholder="Default: uid"
                   placeholder="Default: uid"
                   name="attrMapUsername"
                   name="attrMapUsername"
-                  defaultValue={adminLdapSecurityContainer.state.ldapAttrMapUsername || ''}
+                  value={adminLdapSecurityContainer.state.ldapAttrMapUsername || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapUsername(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapUsername(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -292,7 +292,7 @@ class LdapSecuritySettingContents extends React.Component {
                   type="text"
                   type="text"
                   placeholder="Default: mail"
                   placeholder="Default: mail"
                   name="attrMapMail"
                   name="attrMapMail"
-                  defaultValue={adminLdapSecurityContainer.state.ldapAttrMapMail || ''}
+                  value={adminLdapSecurityContainer.state.ldapAttrMapMail || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapMail(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapMail(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -312,7 +312,7 @@ class LdapSecuritySettingContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="attrMapName"
                   name="attrMapName"
-                  defaultValue={adminLdapSecurityContainer.state.ldapAttrMapName || ''}
+                  value={adminLdapSecurityContainer.state.ldapAttrMapName || ''}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapName(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeAttrMapName(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -337,7 +337,7 @@ class LdapSecuritySettingContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="groupSearchBase"
                   name="groupSearchBase"
-                  defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchBase || ''}
+                  value={adminLdapSecurityContainer.state.ldapGroupSearchBase || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchBase(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchBase(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -359,7 +359,7 @@ class LdapSecuritySettingContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="groupSearchFilter"
                   name="groupSearchFilter"
-                  defaultValue={adminLdapSecurityContainer.state.ldapGroupSearchFilter || ''}
+                  value={adminLdapSecurityContainer.state.ldapGroupSearchFilter || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchFilter(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeGroupSearchFilter(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -391,7 +391,7 @@ class LdapSecuritySettingContents extends React.Component {
                   type="text"
                   type="text"
                   placeholder="Default: uid"
                   placeholder="Default: uid"
                   name="groupDnProperty"
                   name="groupDnProperty"
-                  defaultValue={adminLdapSecurityContainer.state.ldapGroupDnProperty || ''}
+                  value={adminLdapSecurityContainer.state.ldapGroupDnProperty || ''}
                   onChange={e => adminLdapSecurityContainer.changeGroupDnProperty(e.target.value)}
                   onChange={e => adminLdapSecurityContainer.changeGroupDnProperty(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">

+ 1 - 1
apps/app/src/client/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -147,7 +147,7 @@ class LocalSecuritySettingContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="textarea"
                   type="textarea"
                   name="registrationWhitelist"
                   name="registrationWhitelist"
-                  defaultValue={adminLocalSecurityContainer.state.registrationWhitelist.join('\n')}
+                  value={adminLocalSecurityContainer.state.registrationWhitelist.join('\n')}
                   onChange={e => adminLocalSecurityContainer.changeRegistrationWhitelist(e.target.value)}
                   onChange={e => adminLocalSecurityContainer.changeRegistrationWhitelist(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted small">
                 <p className="form-text text-muted small">

+ 16 - 16
apps/app/src/client/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -101,7 +101,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcProviderName"
                   name="oidcProviderName"
-                  defaultValue={adminOidcSecurityContainer.state.oidcProviderName || ''}
+                  value={adminOidcSecurityContainer.state.oidcProviderName || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcProviderName(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcProviderName(e.target.value)}
                 />
                 />
               </div>
               </div>
@@ -114,7 +114,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcIssuerHost"
                   name="oidcIssuerHost"
-                  defaultValue={adminOidcSecurityContainer.state.oidcIssuerHost || ''}
+                  value={adminOidcSecurityContainer.state.oidcIssuerHost || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcIssuerHost(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcIssuerHost(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -130,7 +130,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcClientId"
                   name="oidcClientId"
-                  defaultValue={adminOidcSecurityContainer.state.oidcClientId || ''}
+                  value={adminOidcSecurityContainer.state.oidcClientId || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientId(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientId(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -146,7 +146,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcClientSecret"
                   name="oidcClientSecret"
-                  defaultValue={adminOidcSecurityContainer.state.oidcClientSecret || ''}
+                  value={adminOidcSecurityContainer.state.oidcClientSecret || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientSecret(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcClientSecret(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -164,7 +164,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcAuthorizationEndpoint"
                   name="oidcAuthorizationEndpoint"
-                  defaultValue={adminOidcSecurityContainer.state.oidcAuthorizationEndpoint || ''}
+                  value={adminOidcSecurityContainer.state.oidcAuthorizationEndpoint || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAuthorizationEndpoint(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAuthorizationEndpoint(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -180,7 +180,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcTokenEndpoint"
                   name="oidcTokenEndpoint"
-                  defaultValue={adminOidcSecurityContainer.state.oidcTokenEndpoint || ''}
+                  value={adminOidcSecurityContainer.state.oidcTokenEndpoint || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcTokenEndpoint(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcTokenEndpoint(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -198,7 +198,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcRevocationEndpoint"
                   name="oidcRevocationEndpoint"
-                  defaultValue={adminOidcSecurityContainer.state.oidcRevocationEndpoint || ''}
+                  value={adminOidcSecurityContainer.state.oidcRevocationEndpoint || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcRevocationEndpoint(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcRevocationEndpoint(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -216,7 +216,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcIntrospectionEndpoint"
                   name="oidcIntrospectionEndpoint"
-                  defaultValue={adminOidcSecurityContainer.state.oidcIntrospectionEndpoint || ''}
+                  value={adminOidcSecurityContainer.state.oidcIntrospectionEndpoint || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcIntrospectionEndpoint(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcIntrospectionEndpoint(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -234,7 +234,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcUserInfoEndpoint"
                   name="oidcUserInfoEndpoint"
-                  defaultValue={adminOidcSecurityContainer.state.oidcUserInfoEndpoint || ''}
+                  value={adminOidcSecurityContainer.state.oidcUserInfoEndpoint || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcUserInfoEndpoint(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcUserInfoEndpoint(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -252,7 +252,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcEndSessionEndpoint"
                   name="oidcEndSessionEndpoint"
-                  defaultValue={adminOidcSecurityContainer.state.oidcEndSessionEndpoint || ''}
+                  value={adminOidcSecurityContainer.state.oidcEndSessionEndpoint || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcEndSessionEndpoint(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcEndSessionEndpoint(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -270,7 +270,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcRegistrationEndpoint"
                   name="oidcRegistrationEndpoint"
-                  defaultValue={adminOidcSecurityContainer.state.oidcRegistrationEndpoint || ''}
+                  value={adminOidcSecurityContainer.state.oidcRegistrationEndpoint || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcRegistrationEndpoint(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcRegistrationEndpoint(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -286,7 +286,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcJWKSUri"
                   name="oidcJWKSUri"
-                  defaultValue={adminOidcSecurityContainer.state.oidcJWKSUri || ''}
+                  value={adminOidcSecurityContainer.state.oidcJWKSUri || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcJWKSUri(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcJWKSUri(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -306,7 +306,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcAttrMapId"
                   name="oidcAttrMapId"
-                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapId || ''}
+                  value={adminOidcSecurityContainer.state.oidcAttrMapId || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapId(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapId(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -322,7 +322,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcAttrMapUserName"
                   name="oidcAttrMapUserName"
-                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapUserName || ''}
+                  value={adminOidcSecurityContainer.state.oidcAttrMapUserName || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapUserName(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapUserName(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -338,7 +338,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcAttrMapName"
                   name="oidcAttrMapName"
-                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapName || ''}
+                  value={adminOidcSecurityContainer.state.oidcAttrMapName || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapName(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapName(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
@@ -354,7 +354,7 @@ class OidcSecurityManagementContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="oidcAttrMapEmail"
                   name="oidcAttrMapEmail"
-                  defaultValue={adminOidcSecurityContainer.state.oidcAttrMapEmail || ''}
+                  value={adminOidcSecurityContainer.state.oidcAttrMapEmail || ''}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapEmail(e.target.value)}
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapEmail(e.target.value)}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">

+ 9 - 9
apps/app/src/client/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -150,7 +150,7 @@ class SamlSecurityManagementContents extends React.Component {
                       type="text"
                       type="text"
                       name="samlEntryPoint"
                       name="samlEntryPoint"
                       readOnly={useOnlyEnvVars}
                       readOnly={useOnlyEnvVars}
-                      defaultValue={adminSamlSecurityContainer.state.samlEntryPoint}
+                      value={adminSamlSecurityContainer.state.samlEntryPoint}
                       onChange={e => adminSamlSecurityContainer.changeSamlEntryPoint(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlEntryPoint(e.target.value)}
                     />
                     />
                   </td>
                   </td>
@@ -174,7 +174,7 @@ class SamlSecurityManagementContents extends React.Component {
                       type="text"
                       type="text"
                       name="samlEnvVarissuer"
                       name="samlEnvVarissuer"
                       readOnly={useOnlyEnvVars}
                       readOnly={useOnlyEnvVars}
-                      defaultValue={adminSamlSecurityContainer.state.samlIssuer}
+                      value={adminSamlSecurityContainer.state.samlIssuer}
                       onChange={e => adminSamlSecurityContainer.changeSamlIssuer(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlIssuer(e.target.value)}
                     />
                     />
                   </td>
                   </td>
@@ -199,7 +199,7 @@ class SamlSecurityManagementContents extends React.Component {
                       rows="5"
                       rows="5"
                       name="samlCert"
                       name="samlCert"
                       readOnly={useOnlyEnvVars}
                       readOnly={useOnlyEnvVars}
-                      defaultValue={adminSamlSecurityContainer.state.samlCert}
+                      value={adminSamlSecurityContainer.state.samlCert}
                       onChange={e => adminSamlSecurityContainer.changeSamlCert(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlCert(e.target.value)}
                     />
                     />
                     <p>
                     <p>
@@ -258,7 +258,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapId}
+                      value={adminSamlSecurityContainer.state.samlAttrMapId}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
                     />
                     />
                     <p className="form-text text-muted">
                     <p className="form-text text-muted">
@@ -285,7 +285,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapUsername}
+                      value={adminSamlSecurityContainer.state.samlAttrMapUsername}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
                     />
                     />
                     <p className="form-text text-muted">
                     <p className="form-text text-muted">
@@ -310,7 +310,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapMail}
+                      value={adminSamlSecurityContainer.state.samlAttrMapMail}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
                     />
                     />
                     <p className="form-text text-muted">
                     <p className="form-text text-muted">
@@ -335,7 +335,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapFirstName}
+                      value={adminSamlSecurityContainer.state.samlAttrMapFirstName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
                     />
                     />
                     <p className="form-text text-muted">
                     <p className="form-text text-muted">
@@ -365,7 +365,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      defaultValue={adminSamlSecurityContainer.state.samlAttrMapLastName}
+                      value={adminSamlSecurityContainer.state.samlAttrMapLastName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
                     />
                     />
                     <p className="form-text text-muted">
                     <p className="form-text text-muted">
@@ -462,7 +462,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <textarea
                     <textarea
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      defaultValue={adminSamlSecurityContainer.state.samlABLCRule || ''}
+                      value={adminSamlSecurityContainer.state.samlABLCRule || ''}
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
                     />
                     />
                     <div className="mt-2">
                     <div className="mt-2">

+ 95 - 53
apps/app/src/client/components/Admin/Security/SecuritySetting.jsx

@@ -297,7 +297,7 @@ class SecuritySetting extends React.Component {
                     onClick={() => this.setExpantOtherDeleteOptionsState(deletionType, !expantDeleteOptionsState)}
                     onClick={() => this.setExpantOtherDeleteOptionsState(deletionType, !expantDeleteOptionsState)}
                   >
                   >
                     <span className={`material-symbols-outlined me-1 ${expantDeleteOptionsState ? 'rotate-90' : ''}`}>navigate_next</span>
                     <span className={`material-symbols-outlined me-1 ${expantDeleteOptionsState ? 'rotate-90' : ''}`}>navigate_next</span>
-                    { t('security_settings.other_options') }
+                    {t('security_settings.other_options')}
                   </button>
                   </button>
                   <Collapse isOpen={expantDeleteOptionsState}>
                   <Collapse isOpen={expantDeleteOptionsState}>
                     <div className="pb-4">
                     <div className="pb-4">
@@ -308,7 +308,7 @@ class SecuritySetting extends React.Component {
                           <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
                           <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
                         </span>
                         </span>
                       </p>
                       </p>
-                      { this.previousPageRecursiveAuthorityState(deletionType) !== null && (
+                      {this.previousPageRecursiveAuthorityState(deletionType) !== null && (
                         <div className="mb-3">
                         <div className="mb-3">
                           <strong>
                           <strong>
                             {t('security_settings.forced_update_desc')}
                             {t('security_settings.forced_update_desc')}
@@ -356,60 +356,102 @@ class SecuritySetting extends React.Component {
           </div>
           </div>
         )}
         )}
 
 
-        <h4 className="mt-4">{ t('security_settings.page_list_and_search_results') }</h4>
-        <div className="row justify-content-md-center">
-          <table className="table table-bordered col-lg-9 mb-5">
-            <thead>
-              <tr>
-                <th scope="col">{ t('security_settings.scope_of_page_disclosure') }</th>
-                <th scope="col">{ t('security_settings.set_point') }</th>
-              </tr>
-            </thead>
-            <tbody>
-              <tr>
-                <th scope="row">{ t('public') }</th>
-                <td><span className="material-symbols-outlined text-success me-1">check_circle</span>{ t('security_settings.always_displayed') }</td>
-              </tr>
-              <tr>
-                <th scope="row">{ t('anyone_with_the_link') }</th>
-                <td><span className="material-symbols-outlined text-danger me-1">cancel</span>{ t('security_settings.always_hidden') }</td>
-              </tr>
-              <tr>
-                <th scope="row">{ t('only_me') }</th>
-                <td>
-                  <div className="form-check form-switch form-check-success">
-                    <input
-                      type="checkbox"
-                      className="form-check-input"
+        <h4 className="alert-anchor border-bottom mt-4">{t('security_settings.page_list_and_search_results')}</h4>
+        <div className="row mb-4">
+          <div className="col-md-10">
+            <div className="row">
+
+              {/* Left Column: Labels */}
+              <div className="col-5 d-flex flex-column align-items-end p-4">
+                <div className="fw-bold mb-4">{t('public')}</div>
+                <div className="fw-bold mb-4">{t('anyone_with_the_link')}</div>
+                <div className="fw-bold mb-4">{t('only_me')}</div>
+                <div className="fw-bold">{t('only_inside_the_group')}</div>
+              </div>
+
+              {/* Right Column: Content */}
+              <div className="col-7 d-flex flex-column align-items-start pt-4 pb-4">
+                <div className="mb-4 d-flex align-items-center">
+                  <span className="material-symbols-outlined text-success me-1"></span>
+                  {t('security_settings.always_displayed')}
+                </div>
+                <div className="mb-3 d-flex align-items-center">
+                  <span className="material-symbols-outlined text-danger me-1"></span>
+                  {t('security_settings.always_hidden')}
+                </div>
+
+                {/* Owner Restriction Dropdown */}
+                <div className="mb-3">
+                  <div className="dropdown">
+                    <button
+                      className="btn btn-outline-secondary dropdown-toggle text-end col-12 col-md-auto"
+                      type="button"
                       id="isShowRestrictedByOwner"
                       id="isShowRestrictedByOwner"
-                      checked={!adminGeneralSecurityContainer.state.isShowRestrictedByOwner}
-                      onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByOwner() }}
-                    />
-                    <label className="form-label form-check-label" htmlFor="isShowRestrictedByOwner">
-                      {t('security_settings.displayed_or_hidden')}
-                    </label>
+                      data-bs-toggle="dropdown"
+                      aria-haspopup="true"
+                      aria-expanded="true"
+                    >
+                      <span className="float-start">
+                        {adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Displayed' && t('security_settings.always_displayed')}
+                        {adminGeneralSecurityContainer.state.currentOwnerRestrictionDisplayMode === 'Hidden' && t('security_settings.always_hidden')}
+                      </span>
+                    </button>
+                    <div className="dropdown-menu" aria-labelledby="isShowRestrictedByOwner">
+                      <button
+                        className="dropdown-item"
+                        type="button"
+                        onClick={() => { adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode('Displayed') }}
+                      >
+                        {t('security_settings.always_displayed')}
+                      </button>
+                      <button
+                        className="dropdown-item"
+                        type="button"
+                        onClick={() => { adminGeneralSecurityContainer.changeOwnerRestrictionDisplayMode('Hidden') }}
+                      >
+                        {t('security_settings.always_hidden')}
+                      </button>
+                    </div>
                   </div>
                   </div>
-                </td>
-              </tr>
-              <tr>
-                <th scope="row">{ t('only_inside_the_group') }</th>
-                <td>
-                  <div className="form-check form-switch form-check-success">
-                    <input
-                      type="checkbox"
-                      className="form-check-input"
+                </div>
+
+                {/* Group Restriction Dropdown */}
+                <div className="">
+                  <div className="dropdown">
+                    <button
+                      className="btn btn-outline-secondary dropdown-toggle text-end col-12 col-md-auto"
+                      type="button"
                       id="isShowRestrictedByGroup"
                       id="isShowRestrictedByGroup"
-                      checked={!adminGeneralSecurityContainer.state.isShowRestrictedByGroup}
-                      onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByGroup() }}
-                    />
-                    <label className="form-label form-check-label" htmlFor="isShowRestrictedByGroup">
-                      {t('security_settings.displayed_or_hidden')}
-                    </label>
+                      data-bs-toggle="dropdown"
+                      aria-haspopup="true"
+                      aria-expanded="true"
+                    >
+                      <span className="float-start">
+                        {adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Displayed' && t('security_settings.always_displayed')}
+                        {adminGeneralSecurityContainer.state.currentGroupRestrictionDisplayMode === 'Hidden' && t('security_settings.always_hidden')}
+                      </span>
+                    </button>
+                    <div className="dropdown-menu" aria-labelledby="isShowRestrictedByGroup">
+                      <button
+                        className="dropdown-item"
+                        type="button"
+                        onClick={() => { adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode('Displayed') }}
+                      >
+                        {t('security_settings.always_displayed')}
+                      </button>
+                      <button
+                        className="dropdown-item"
+                        type="button"
+                        onClick={() => { adminGeneralSecurityContainer.changeGroupRestrictionDisplayMode('Hidden') }}
+                      >
+                        {t('security_settings.always_hidden')}
+                      </button>
+                    </div>
                   </div>
                   </div>
-                </td>
-              </tr>
-            </tbody>
-          </table>
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
         </div>
 
 
         <h4 className="mb-3">{t('security_settings.page_access_rights')}</h4>
         <h4 className="mb-3">{t('security_settings.page_access_rights')}</h4>
@@ -549,7 +591,7 @@ class SecuritySetting extends React.Component {
             <input
             <input
               className="form-control col-md-4"
               className="form-control col-md-4"
               type="text"
               type="text"
-              defaultValue={adminGeneralSecurityContainer.state.sessionMaxAge || ''}
+              value={adminGeneralSecurityContainer.state.sessionMaxAge || ''}
               onChange={(e) => {
               onChange={(e) => {
                 adminGeneralSecurityContainer.setSessionMaxAge(e.target.value);
                 adminGeneralSecurityContainer.setSessionMaxAge(e.target.value);
               }}
               }}

+ 1 - 1
apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -111,7 +111,7 @@ const CustomBotWithProxySettings = (props) => {
                 className="form-control"
                 className="form-control"
                 type="text"
                 type="text"
                 name="settingForm[proxyUrl]"
                 name="settingForm[proxyUrl]"
-                defaultValue={newProxyServerUri}
+                value={newProxyServerUri}
                 onChange={(e) => { setNewProxyServerUri(e.target.value) }}
                 onChange={(e) => { setNewProxyServerUri(e.target.value) }}
               />
               />
             </div>
             </div>

+ 2 - 2
apps/app/src/client/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -65,7 +65,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            defaultValue={slackSigningSecretEnv}
+            value={slackSigningSecretEnv}
             readOnly
             readOnly
           />
           />
           <p className="form-text text-muted">
           <p className="form-text text-muted">
@@ -94,7 +94,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            defaultValue={slackBotTokenEnv}
+            value={slackBotTokenEnv}
             readOnly
             readOnly
           />
           />
           <p className="form-text text-muted">
           <p className="form-text text-muted">

+ 1 - 1
apps/app/src/client/components/Admin/SlackIntegration/ManageCommandsProcess.jsx

@@ -147,7 +147,7 @@ const PermissionSettingForEachPermissionTypeComponent = ({
             className="form-control"
             className="form-control"
             type="textarea"
             type="textarea"
             name={keyName}
             name={keyName}
-            defaultValue={textareaDefaultValue}
+            value={textareaDefaultValue}
             onChange={onUpdateChannels}
             onChange={onUpdateChannels}
           />
           />
           <p className="form-text text-muted small">
           <p className="form-text text-muted small">

+ 26 - 3
apps/app/src/client/components/PageControls/PageControls.tsx

@@ -8,6 +8,7 @@ import type {
 import {
 import {
   isIPageInfoForEntity, isIPageInfoForOperation,
   isIPageInfoForEntity, isIPageInfoForOperation,
 } from '@growi/core';
 } from '@growi/core';
+import { pagePathUtils } from '@growi/core/dist/utils';
 import { useRect } from '@growi/ui/dist/utils';
 import { useRect } from '@growi/ui/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
@@ -17,7 +18,9 @@ import {
 } from '~/client/services/page-operation';
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import OpenDefaultAiAssistantButton from '~/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton';
 import OpenDefaultAiAssistantButton from '~/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton';
-import { useIsGuestUser, useIsReadOnlyUser, useIsSearchPage } from '~/stores-universal/context';
+import {
+  useIsGuestUser, useIsReadOnlyUser, useIsSearchPage, useIsUsersHomepageDeletionEnabled,
+} from '~/stores-universal/context';
 import {
 import {
   EditorMode, useEditorMode,
   EditorMode, useEditorMode,
 } from '~/stores-universal/ui';
 } from '~/stores-universal/ui';
@@ -27,7 +30,7 @@ import {
 } from '~/stores/ui';
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { useSWRxPageInfo, useSWRxTagsInfo } from '../../../stores/page';
+import { useSWRxPageInfo, useSWRxTagsInfo, useCurrentPagePath } from '../../../stores/page';
 import { useSWRxUsersList } from '../../../stores/user';
 import { useSWRxUsersList } from '../../../stores/user';
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import {
 import {
@@ -134,6 +137,10 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
   const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
   const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
   const { data: isSearchPage } = useIsSearchPage();
   const { data: isSearchPage } = useIsSearchPage();
+  const { data: isUsersHomepageDeletionEnabled } = useIsUsersHomepageDeletionEnabled();
+  const { data: currentPagePath } = useCurrentPagePath();
+
+  const isUsersHomepage = currentPagePath == null ? false : pagePathUtils.isUsersHomepage(currentPagePath);
 
 
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
 
 
@@ -249,6 +256,22 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     }
     }
   }, [expandContentWidth, isGuestUser, isReadOnlyUser, onClickSwitchContentWidth, pageId, pageInfo]);
   }, [expandContentWidth, isGuestUser, isReadOnlyUser, onClickSwitchContentWidth, pageId, pageInfo]);
 
 
+  const isEnableActions = useMemo(() => {
+    if (isGuestUser) {
+      return false;
+    }
+
+    if (currentPagePath == null) {
+      return false;
+    }
+
+    if (isUsersHomepage && !isUsersHomepageDeletionEnabled) {
+      return false;
+    }
+
+    return true;
+  }, [isGuestUser, isUsersHomepage, isUsersHomepageDeletionEnabled]);
+
   const additionalMenuItemOnTopRenderer = useMemo(() => {
   const additionalMenuItemOnTopRenderer = useMemo(() => {
     if (!isIPageInfoForEntity(pageInfo)) {
     if (!isIPageInfoForEntity(pageInfo)) {
       return undefined;
       return undefined;
@@ -332,7 +355,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
         <PageItemControl
         <PageItemControl
           pageId={pageId}
           pageId={pageId}
           pageInfo={pageInfo}
           pageInfo={pageInfo}
-          isEnableActions={!isGuestUser}
+          isEnableActions={isEnableActions}
           isReadOnlyUser={!!isReadOnlyUser}
           isReadOnlyUser={!!isReadOnlyUser}
           forceHideMenuItems={forceHideMenuItemsWithAdditions}
           forceHideMenuItems={forceHideMenuItemsWithAdditions}
           additionalMenuItemOnTopRenderer={!isReadOnlyUser ? additionalMenuItemOnTopRenderer : undefined}
           additionalMenuItemOnTopRenderer={!isReadOnlyUser ? additionalMenuItemOnTopRenderer : undefined}

+ 0 - 1
apps/app/src/client/components/Sidebar/InAppNotification/PrimaryItemForNotification.tsx

@@ -8,7 +8,6 @@ import { PrimaryItem, type PrimaryItemProps } from '../SidebarNav/PrimaryItem';
 
 
 type PrimaryItemForNotificationProps = Omit<PrimaryItemProps, 'onClick' | 'label' | 'iconName' | 'contents' | 'badgeContents' >
 type PrimaryItemForNotificationProps = Omit<PrimaryItemProps, 'onClick' | 'label' | 'iconName' | 'contents' | 'badgeContents' >
 
 
-// TODO(after v7 release): https://redmine.weseek.co.jp/issues/138463
 export const PrimaryItemForNotification = memo((props: PrimaryItemForNotificationProps) => {
 export const PrimaryItemForNotification = memo((props: PrimaryItemForNotificationProps) => {
   const { sidebarMode, onHover } = props;
   const { sidebarMode, onHover } = props;
 
 

+ 15 - 4
apps/app/src/client/components/Sidebar/SidebarContents.tsx

@@ -2,6 +2,7 @@ import React, { memo, useMemo } from 'react';
 
 
 import { AiAssistant } from '~/features/openai/client/components/AiAssistant/Sidebar/AiAssistant';
 import { AiAssistant } from '~/features/openai/client/components/AiAssistant/Sidebar/AiAssistant';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
+import { useIsAiEnabled, useIsGuestUser } from '~/stores-universal/context';
 import { useCollapsedContentsOpened, useCurrentSidebarContents, useSidebarMode } from '~/stores/ui';
 import { useCollapsedContentsOpened, useCurrentSidebarContents, useSidebarMode } from '~/stores/ui';
 
 
 
 
@@ -17,8 +18,10 @@ import styles from './SidebarContents.module.scss';
 
 
 export const SidebarContents = memo(() => {
 export const SidebarContents = memo(() => {
   const { isCollapsedMode } = useSidebarMode();
   const { isCollapsedMode } = useSidebarMode();
-  const { data: isCollapsedContentsOpened } = useCollapsedContentsOpened();
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isAiEnabled } = useIsAiEnabled();
 
 
+  const { data: isCollapsedContentsOpened } = useCollapsedContentsOpened();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
 
   const Contents = useMemo(() => {
   const Contents = useMemo(() => {
@@ -32,13 +35,21 @@ export const SidebarContents = memo(() => {
       case SidebarContentsType.BOOKMARKS:
       case SidebarContentsType.BOOKMARKS:
         return Bookmarks;
         return Bookmarks;
       case SidebarContentsType.NOTIFICATION:
       case SidebarContentsType.NOTIFICATION:
-        return InAppNotification;
+        if (isGuestUser == null) return () => <></>; // wait for isGuestUser to be determined
+        if (!isGuestUser) {
+          return InAppNotification;
+        }
+        return PageTree;
       case SidebarContentsType.AI_ASSISTANT:
       case SidebarContentsType.AI_ASSISTANT:
-        return AiAssistant;
+        if (isAiEnabled == null) return () => <></>; // wait for isAiEnabled to be determined
+        if (isAiEnabled) {
+          return AiAssistant;
+        }
+        return PageTree;
       default:
       default:
         return PageTree;
         return PageTree;
     }
     }
-  }, [currentSidebarContents]);
+  }, [currentSidebarContents, isAiEnabled, isGuestUser]);
 
 
   const isHidden = isCollapsedMode() && !isCollapsedContentsOpened;
   const isHidden = isCollapsedMode() && !isCollapsedContentsOpened;
   const classToHide = isHidden ? 'd-none' : '';
   const classToHide = isHidden ? 'd-none' : '';

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

@@ -3,7 +3,7 @@ import { memo } from 'react';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
-import { useIsAiEnabled } from '~/stores-universal/context';
+import { useIsAiEnabled, useIsGuestUser } from '~/stores-universal/context';
 import { useSidebarMode } from '~/stores/ui';
 import { useSidebarMode } from '~/stores/ui';
 
 
 import { PrimaryItem } from './PrimaryItem';
 import { PrimaryItem } from './PrimaryItem';
@@ -24,6 +24,7 @@ export const PrimaryItems = memo((props: Props) => {
 
 
   const { data: sidebarMode } = useSidebarMode();
   const { data: sidebarMode } = useSidebarMode();
   const { data: isAiEnabled } = useIsAiEnabled();
   const { data: isAiEnabled } = useIsAiEnabled();
+  const { data: isGuestUser } = useIsGuestUser();
 
 
   if (sidebarMode == null) {
   if (sidebarMode == null) {
     return <></>;
     return <></>;
@@ -36,7 +37,7 @@ export const PrimaryItems = memo((props: Props) => {
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmarks" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmarks" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TAG} label="Tags" iconName="local_offer" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TAG} label="Tags" iconName="local_offer" onHover={onItemHover} />
-      <PrimaryItemForNotification sidebarMode={sidebarMode} onHover={onItemHover} />
+      {isGuestUser === false && <PrimaryItemForNotification sidebarMode={sidebarMode} onHover={onItemHover} />}
       {isAiEnabled && (
       {isAiEnabled && (
         <PrimaryItem
         <PrimaryItem
           sidebarMode={sidebarMode}
           sidebarMode={sidebarMode}

+ 23 - 19
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -32,13 +32,14 @@ export default class AdminGeneralSecurityContainer extends Container {
       currentPageRecursiveDeletionAuthority: PageRecursiveDeleteConfigValue.Inherit,
       currentPageRecursiveDeletionAuthority: PageRecursiveDeleteConfigValue.Inherit,
       currentPageCompleteDeletionAuthority: PageSingleDeleteCompConfigValue.AdminOnly,
       currentPageCompleteDeletionAuthority: PageSingleDeleteCompConfigValue.AdminOnly,
       currentPageRecursiveCompleteDeletionAuthority: PageRecursiveDeleteCompConfigValue.Inherit,
       currentPageRecursiveCompleteDeletionAuthority: PageRecursiveDeleteCompConfigValue.Inherit,
+      currentGroupRestrictionDisplayMode: 'Hidden',
+      currentOwnerRestrictionDisplayMode: 'Hidden',
       isAllGroupMembershipRequiredForPageCompleteDeletion: true,
       isAllGroupMembershipRequiredForPageCompleteDeletion: true,
       previousPageRecursiveDeletionAuthority: null,
       previousPageRecursiveDeletionAuthority: null,
       previousPageRecursiveCompleteDeletionAuthority: null,
       previousPageRecursiveCompleteDeletionAuthority: null,
       expandOtherOptionsForDeletion: false,
       expandOtherOptionsForDeletion: false,
       expandOtherOptionsForCompleteDeletion: false,
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
       isShowRestrictedByOwner: false,
-      isShowRestrictedByGroup: false,
       isUsersHomepageDeletionEnabled: false,
       isUsersHomepageDeletionEnabled: false,
       isForceDeleteUserHomepageOnUserDeletion: false,
       isForceDeleteUserHomepageOnUserDeletion: false,
       isRomUserAllowedToComment: false,
       isRomUserAllowedToComment: false,
@@ -56,6 +57,8 @@ export default class AdminGeneralSecurityContainer extends Container {
       shareLinksActivePage: 1,
       shareLinksActivePage: 1,
     };
     };
 
 
+    this.changeOwnerRestrictionDisplayMode = this.changeOwnerRestrictionDisplayMode.bind(this);
+    this.changeGroupRestrictionDisplayMode = this.changeGroupRestrictionDisplayMode.bind(this);
     this.changePageDeletionAuthority = this.changePageDeletionAuthority.bind(this);
     this.changePageDeletionAuthority = this.changePageDeletionAuthority.bind(this);
     this.changePageCompleteDeletionAuthority = this.changePageCompleteDeletionAuthority.bind(this);
     this.changePageCompleteDeletionAuthority = this.changePageCompleteDeletionAuthority.bind(this);
     this.changePageRecursiveDeletionAuthority = this.changePageRecursiveDeletionAuthority.bind(this);
     this.changePageRecursiveDeletionAuthority = this.changePageRecursiveDeletionAuthority.bind(this);
@@ -76,8 +79,9 @@ export default class AdminGeneralSecurityContainer extends Container {
       currentPageRecursiveDeletionAuthority: generalSetting.pageRecursiveDeletionAuthority,
       currentPageRecursiveDeletionAuthority: generalSetting.pageRecursiveDeletionAuthority,
       currentPageRecursiveCompleteDeletionAuthority: generalSetting.pageRecursiveCompleteDeletionAuthority,
       currentPageRecursiveCompleteDeletionAuthority: generalSetting.pageRecursiveCompleteDeletionAuthority,
       isAllGroupMembershipRequiredForPageCompleteDeletion: generalSetting.isAllGroupMembershipRequiredForPageCompleteDeletion,
       isAllGroupMembershipRequiredForPageCompleteDeletion: generalSetting.isAllGroupMembershipRequiredForPageCompleteDeletion,
-      isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
-      isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
+      // Set display to 'Hidden' if hideRestrictedByOwner is anything but false.
+      currentOwnerRestrictionDisplayMode: generalSetting.hideRestrictedByOwner === false ? 'Displayed' : 'Hidden',
+      currentGroupRestrictionDisplayMode: generalSetting.hideRestrictedByGroup === false ? 'Displayed' : 'Hidden',
       isUsersHomepageDeletionEnabled: generalSetting.isUsersHomepageDeletionEnabled,
       isUsersHomepageDeletionEnabled: generalSetting.isUsersHomepageDeletionEnabled,
       isForceDeleteUserHomepageOnUserDeletion: generalSetting.isForceDeleteUserHomepageOnUserDeletion,
       isForceDeleteUserHomepageOnUserDeletion: generalSetting.isForceDeleteUserHomepageOnUserDeletion,
       isRomUserAllowedToComment: generalSetting.isRomUserAllowedToComment,
       isRomUserAllowedToComment: generalSetting.isRomUserAllowedToComment,
@@ -123,6 +127,20 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ disableLinkSharing });
     this.setState({ disableLinkSharing });
   }
   }
 
 
+  /**
+   * Change ownerRestrictionDisplayMode
+   */
+  changeOwnerRestrictionDisplayMode(mode) {
+    this.setState({ currentOwnerRestrictionDisplayMode: mode });
+  }
+
+  /**
+   * Change groupRestrictionDisplayMode
+   */
+  changeGroupRestrictionDisplayMode(mode) {
+    this.setState({ currentGroupRestrictionDisplayMode: mode });
+  }
+
   /**
   /**
    * Change restrictGuestMode
    * Change restrictGuestMode
    */
    */
@@ -194,20 +212,6 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ expandOtherOptionsForCompleteDeletion: bool });
     this.setState({ expandOtherOptionsForCompleteDeletion: bool });
   }
   }
 
 
-  /**
-   * Switch showRestrictedByOwner
-   */
-  switchIsShowRestrictedByOwner() {
-    this.setState({ isShowRestrictedByOwner:  !this.state.isShowRestrictedByOwner });
-  }
-
-  /**
-   * Switch showRestrictedByGroup
-   */
-  switchIsShowRestrictedByGroup() {
-    this.setState({ isShowRestrictedByGroup:  !this.state.isShowRestrictedByGroup });
-  }
-
   /**
   /**
    * Switch isUsersHomepageDeletionEnabled
    * Switch isUsersHomepageDeletionEnabled
    */
    */
@@ -245,8 +249,8 @@ export default class AdminGeneralSecurityContainer extends Container {
       pageRecursiveDeletionAuthority: this.state.currentPageRecursiveDeletionAuthority,
       pageRecursiveDeletionAuthority: this.state.currentPageRecursiveDeletionAuthority,
       pageRecursiveCompleteDeletionAuthority: this.state.currentPageRecursiveCompleteDeletionAuthority,
       pageRecursiveCompleteDeletionAuthority: this.state.currentPageRecursiveCompleteDeletionAuthority,
       isAllGroupMembershipRequiredForPageCompleteDeletion: this.state.isAllGroupMembershipRequiredForPageCompleteDeletion,
       isAllGroupMembershipRequiredForPageCompleteDeletion: this.state.isAllGroupMembershipRequiredForPageCompleteDeletion,
-      hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
-      hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
+      hideRestrictedByGroup: this.state.currentGroupRestrictionDisplayMode === 'Hidden',
+      hideRestrictedByOwner: this.state.currentOwnerRestrictionDisplayMode === 'Hidden',
       isUsersHomepageDeletionEnabled: this.state.isUsersHomepageDeletionEnabled,
       isUsersHomepageDeletionEnabled: this.state.isUsersHomepageDeletionEnabled,
       isForceDeleteUserHomepageOnUserDeletion: this.state.isForceDeleteUserHomepageOnUserDeletion,
       isForceDeleteUserHomepageOnUserDeletion: this.state.isForceDeleteUserHomepageOnUserDeletion,
       isRomUserAllowedToComment: this.state.isRomUserAllowedToComment,
       isRomUserAllowedToComment: this.state.isRomUserAllowedToComment,

+ 4 - 2
apps/app/src/client/services/page-operation.ts

@@ -5,6 +5,7 @@ import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
 import type { SyncLatestRevisionBody } from '~/interfaces/yjs';
 import type { SyncLatestRevisionBody } from '~/interfaces/yjs';
+import { useIsGuestUser } from '~/stores-universal/context';
 import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import {
 import {
   useCurrentPageId, useSWRMUTxCurrentPage, useSWRxApplicableGrant, useSWRxTagsInfo,
   useCurrentPageId, useSWRMUTxCurrentPage, useSWRxApplicableGrant, useSWRxTagsInfo,
@@ -103,8 +104,9 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { mutate: mutateCurrentGrantData } = useSWRxCurrentGrantData(pageId);
-  const { mutate: mutateApplicableGrant } = useSWRxApplicableGrant(pageId);
+  const { data: isGuestUser } = useIsGuestUser();
+  const { mutate: mutateCurrentGrantData } = useSWRxCurrentGrantData(isGuestUser ? null : pageId);
+  const { mutate: mutateApplicableGrant } = useSWRxApplicableGrant(isGuestUser ? null : pageId);
 
 
   // update swr 'currentPageId', 'currentPage', remote states
   // update swr 'currentPageId', 'currentPage', remote states
   return useCallback(async() => {
   return useCallback(async() => {

+ 0 - 15
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -102,7 +102,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: listExternalUserGroups
    *         summary: /external-user-groups
    *         summary: /external-user-groups
    *         parameters:
    *         parameters:
    *           - name: page
    *           - name: page
@@ -172,7 +171,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: getAncestors
    *         summary: /external-user-groups/ancestors
    *         summary: /external-user-groups/ancestors
    *         parameters:
    *         parameters:
    *           - name: groupId
    *           - name: groupId
@@ -217,7 +215,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: listChildren
    *         summary: /external-user-groups/children
    *         summary: /external-user-groups/children
    *         parameters:
    *         parameters:
    *           - name: parentIds
    *           - name: parentIds
@@ -274,7 +271,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: getExternalUserGroup
    *         summary: /external-user-groups/{id}
    *         summary: /external-user-groups/{id}
    *         parameters:
    *         parameters:
    *           - name: id
    *           - name: id
@@ -316,7 +312,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: deleteExternalUserGroup
    *         summary: /external-user-groups/{id}
    *         summary: /external-user-groups/{id}
    *         parameters:
    *         parameters:
    *           - name: id
    *           - name: id
@@ -391,7 +386,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: updateExternalUserGroup
    *         summary: /external-user-groups/{id}
    *         summary: /external-user-groups/{id}
    *         parameters:
    *         parameters:
    *           - name: id
    *           - name: id
@@ -449,7 +443,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: getExternalUserGroupRelations
    *         summary: /external-user-groups/{id}/external-user-group-relations
    *         summary: /external-user-groups/{id}/external-user-group-relations
    *         parameters:
    *         parameters:
    *           - name: id
    *           - name: id
@@ -496,7 +489,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: getLdapSyncSettings
    *         summary: Get LDAP sync settings
    *         summary: Get LDAP sync settings
    *         responses:
    *         responses:
    *           200:
    *           200:
@@ -546,7 +538,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: getKeycloakSyncSettings
    *         summary: Get Keycloak sync settings
    *         summary: Get Keycloak sync settings
    *         responses:
    *         responses:
    *           200:
    *           200:
@@ -596,7 +587,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: updateLdapSyncSettings
    *         summary: Update LDAP sync settings
    *         summary: Update LDAP sync settings
    *         requestBody:
    *         requestBody:
    *           required: true
    *           required: true
@@ -673,7 +663,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: updateKeycloakSyncSettings
    *         summary: /external-user-groups/keycloak/sync-settings
    *         summary: /external-user-groups/keycloak/sync-settings
    *         requestBody:
    *         requestBody:
    *           required: true
    *           required: true
@@ -746,7 +735,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: syncExternalUserGroupsLdap
    *         summary: Start LDAP sync process
    *         summary: Start LDAP sync process
    *         responses:
    *         responses:
    *           202:
    *           202:
@@ -793,7 +781,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: syncExternalUserGroupsKeycloak
    *         summary: /external-user-groups/keycloak/sync
    *         summary: /external-user-groups/keycloak/sync
    *         responses:
    *         responses:
    *           202:
    *           202:
@@ -856,7 +843,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: getExternalUserGroupsLdapSyncStatus
    *         summary: Get LDAP sync status
    *         summary: Get LDAP sync status
    *         responses:
    *         responses:
    *           200:
    *           200:
@@ -879,7 +865,6 @@ module.exports = (crowi: Crowi): Router => {
    *         tags: [ExternalUserGroups]
    *         tags: [ExternalUserGroups]
    *         security:
    *         security:
    *           - cookieAuth: []
    *           - cookieAuth: []
-   *         operationId: getExternalUserGroupsLdapSyncStatus
    *         summary: /external-user-groups/ldap/sync-status
    *         summary: /external-user-groups/ldap/sync-status
    *         responses:
    *         responses:
    *           200:
    *           200:

+ 110 - 113
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx

@@ -1,6 +1,6 @@
 import type { KeyboardEvent, JSX } from 'react';
 import type { KeyboardEvent, JSX } from 'react';
 import {
 import {
-  type FC, memo, useRef, useEffect, useState, useCallback, useMemo,
+  type FC, memo, useEffect, useState, useCallback, useMemo,
 } from 'react';
 } from 'react';
 
 
 import { Controller } from 'react-hook-form';
 import { Controller } from 'react-hook-form';
@@ -29,7 +29,7 @@ import {
 import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
 import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
 import { useSWRxThreads } from '../../../stores/thread';
 import { useSWRxThreads } from '../../../stores/thread';
 
 
-import { MessageCard } from './MessageCard';
+import { MessageCard } from './MessageCard/MessageCard';
 import { ResizableTextarea } from './ResizableTextArea';
 import { ResizableTextarea } from './ResizableTextArea';
 
 
 import styles from './AiAssistantSidebar.module.scss';
 import styles from './AiAssistantSidebar.module.scss';
@@ -390,105 +390,112 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
             <span className="material-symbols-outlined">close</span>
             <span className="material-symbols-outlined">close</span>
           </button>
           </button>
         </div>
         </div>
-        <div className="p-4 d-flex flex-column gap-4 vh-100">
-
-          { threadData != null
-            ? (
-              <div className="vstack gap-4 pb-2">
-                { messageLogs.map(message => (
-                  <>
-                    <MessageCard
-                      role={message.isUserMessage ? 'user' : 'assistant'}
-                      additionalItem={messageCardAdditionalItemForGeneratedMessage(message.id)}
-                    >
-                      {message.content}
-                    </MessageCard>
-                  </>
-                )) }
-                { generatingAnswerMessage != null && (
-                  <MessageCard
-                    role="assistant"
-                    additionalItem={messageCardAdditionalItemForGeneratingMessage}
-                  >
-                    {generatingAnswerMessage.content}
-                  </MessageCard>
-                )}
-                { messageLogs.length > 0 && (
-                  <div className="d-flex justify-content-center">
-                    <span className="bg-body-tertiary text-body-secondary rounded-pill px-3 py-1" style={{ fontSize: 'smaller' }}>
-                      {t('sidebar_ai_assistant.caution_against_hallucination')}
-                    </span>
+
+        <div className="flex-grow-1 overflow-hidden">
+          <SimpleBar
+            className="h-100"
+            autoHide
+          >
+            <div className="p-4 d-flex flex-column gap-4 flex-grow-1">
+              { threadData != null
+                ? (
+                  <div className="vstack gap-4 pb-2">
+                    { messageLogs.map(message => (
+                      <>
+                        <MessageCard
+                          role={message.isUserMessage ? 'user' : 'assistant'}
+                          additionalItem={messageCardAdditionalItemForGeneratedMessage(message.id)}
+                        >
+                          {message.content}
+                        </MessageCard>
+                      </>
+                    )) }
+                    { generatingAnswerMessage != null && (
+                      <MessageCard
+                        role="assistant"
+                        additionalItem={messageCardAdditionalItemForGeneratingMessage}
+                      >
+                        {generatingAnswerMessage.content}
+                      </MessageCard>
+                    )}
+                    { messageLogs.length > 0 && (
+                      <div className="d-flex justify-content-center">
+                        <span className="bg-body-tertiary text-body-secondary rounded-pill px-3 py-1" style={{ fontSize: 'smaller' }}>
+                          {t('sidebar_ai_assistant.caution_against_hallucination')}
+                        </span>
+                      </div>
+                    )}
                   </div>
                   </div>
-                )}
-              </div>
-            )
-            : (
-              <>{ initialView }</>
-            )
-          }
+                )
+                : (
+                  <>{ initialView }</>
+                )
+              }
+            </div>
+          </SimpleBar>
+        </div>
 
 
-          <div className="mt-auto">
-            <form onSubmit={form.handleSubmit(submit)} className="flex-fill vstack gap-1">
-              <Controller
-                name="input"
-                control={form.control}
-                render={({ field }) => (
-                  <ResizableTextarea
-                    {...field}
-                    required
-                    className="form-control textarea-ask"
-                    style={{ resize: 'none' }}
-                    rows={1}
-                    placeholder={placeHolder}
-                    onKeyDown={keyDownHandler}
-                    disabled={form.formState.isSubmitting}
-                  />
-                )}
-              />
-              <div className="flex-fill hstack gap-2 justify-content-between m-0">
-                { !isEditorAssistant && generateModeSwitchesDropdownForKnowledgeAssistant(isGenerating) }
-                { isEditorAssistant && <div /> }
-                <button
-                  type="submit"
-                  className="btn btn-submit no-border"
-                  disabled={form.formState.isSubmitting || isGenerating}
-                >
-                  <span className="material-symbols-outlined">send</span>
-                </button>
+        <div className="position-sticky bottom-0 bg-body z-2 p-3 border-top">
+          <form onSubmit={form.handleSubmit(submit)} className="flex-fill vstack gap-1">
+            <Controller
+              name="input"
+              control={form.control}
+              render={({ field }) => (
+                <ResizableTextarea
+                  {...field}
+                  required
+                  className="form-control textarea-ask"
+                  style={{ resize: 'none' }}
+                  rows={1}
+                  placeholder={placeHolder}
+                  onKeyDown={keyDownHandler}
+                  disabled={form.formState.isSubmitting}
+                />
+              )}
+            />
+            <div className="flex-fill hstack gap-2 justify-content-between m-0">
+              { !isEditorAssistant && generateModeSwitchesDropdownForKnowledgeAssistant(isGenerating) }
+              { isEditorAssistant && <div /> }
+              <button
+                type="submit"
+                className="btn btn-submit no-border"
+                disabled={form.formState.isSubmitting || isGenerating}
+              >
+                <span className="material-symbols-outlined">send</span>
+              </button>
+            </div>
+          </form>
+
+          {form.formState.errors.input != null && (
+            <div className="mt-4 bg-danger bg-opacity-10 rounded-3 p-2 w-100">
+              <div>
+                <span className="material-symbols-outlined text-danger me-2">error</span>
+                <span className="text-danger">{ errorMessage != null ? t(errorMessage) : t('sidebar_ai_assistant.error_message') }</span>
               </div>
               </div>
-            </form>
 
 
-            {form.formState.errors.input != null && (
-              <div className="mt-4 bg-danger bg-opacity-10 rounded-3 p-2 w-100">
-                <div>
-                  <span className="material-symbols-outlined text-danger me-2">error</span>
-                  <span className="text-danger">{ errorMessage != null ? t(errorMessage) : t('sidebar_ai_assistant.error_message') }</span>
-                </div>
-
-                <button
-                  type="button"
-                  className="btn btn-link text-body-secondary p-0"
-                  aria-expanded={isErrorDetailCollapsed}
-                  onClick={() => setIsErrorDetailCollapsed(!isErrorDetailCollapsed)}
-                >
-                  <span className={`material-symbols-outlined mt-2 me-1 ${isErrorDetailCollapsed ? 'rotate-90' : ''}`}>
-                    chevron_right
-                  </span>
-                  <span className="small">{t('sidebar_ai_assistant.show_error_detail')}</span>
-                </button>
-
-                <Collapse isOpen={isErrorDetailCollapsed}>
-                  <div className="ms-2">
-                    <div className="">
-                      <div className="text-body-secondary small">
-                        {form.formState.errors.input?.message}
-                      </div>
+              <button
+                type="button"
+                className="btn btn-link text-body-secondary p-0"
+                aria-expanded={isErrorDetailCollapsed}
+                onClick={() => setIsErrorDetailCollapsed(!isErrorDetailCollapsed)}
+              >
+                <span className={`material-symbols-outlined mt-2 me-1 ${isErrorDetailCollapsed ? 'rotate-90' : ''}`}>
+                  chevron_right
+                </span>
+                <span className="small">{t('sidebar_ai_assistant.show_error_detail')}</span>
+              </button>
+
+              <Collapse isOpen={isErrorDetailCollapsed}>
+                <div className="ms-2">
+                  <div className="">
+                    <div className="text-body-secondary small">
+                      {form.formState.errors.input?.message}
                     </div>
                     </div>
                   </div>
                   </div>
-                </Collapse>
-              </div>
-            )}
-          </div>
+                </div>
+              </Collapse>
+            </div>
+          )}
         </div>
         </div>
       </div>
       </div>
     </>
     </>
@@ -497,9 +504,6 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
 
 
 
 
 export const AiAssistantSidebar: FC = memo((): JSX.Element => {
 export const AiAssistantSidebar: FC = memo((): JSX.Element => {
-  const sidebarRef = useRef<HTMLDivElement>(null);
-  const sidebarScrollerRef = useRef<HTMLDivElement>(null);
-
   const { data: aiAssistantSidebarData, close: closeAiAssistantSidebar, refreshThreadData } = useAiAssistantSidebar();
   const { data: aiAssistantSidebarData, close: closeAiAssistantSidebar, refreshThreadData } = useAiAssistantSidebar();
   const { mutate: mutateIsEnableUnifiedMergeView } = useIsEnableUnifiedMergeView();
   const { mutate: mutateIsEnableUnifiedMergeView } = useIsEnableUnifiedMergeView();
 
 
@@ -538,24 +542,17 @@ export const AiAssistantSidebar: FC = memo((): JSX.Element => {
 
 
   return (
   return (
     <div
     <div
-      ref={sidebarRef}
       className={`position-fixed top-0 end-0 h-100 border-start bg-body shadow-sm overflow-hidden ${moduleClass}`}
       className={`position-fixed top-0 end-0 h-100 border-start bg-body shadow-sm overflow-hidden ${moduleClass}`}
       data-testid="grw-right-sidebar"
       data-testid="grw-right-sidebar"
     >
     >
-      <SimpleBar
-        scrollableNodeProps={{ ref: sidebarScrollerRef }}
-        className="h-100 position-relative"
-        autoHide
-      >
-        <AiAssistantSidebarSubstance
-          isEditorAssistant={isEditorAssistant}
-          threadData={threadData}
-          aiAssistantData={aiAssistantData}
-          onMessageReceived={mutateThreads}
-          onNewThreadCreated={newThreadCreatedHandler}
-          onCloseButtonClicked={closeAiAssistantSidebar}
-        />
-      </SimpleBar>
+      <AiAssistantSidebarSubstance
+        isEditorAssistant={isEditorAssistant}
+        threadData={threadData}
+        aiAssistantData={aiAssistantData}
+        onMessageReceived={mutateThreads}
+        onNewThreadCreated={newThreadCreatedHandler}
+        onCloseButtonClicked={closeAiAssistantSidebar}
+      />
     </div>
     </div>
   );
   );
 });
 });

+ 0 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard.module.scss → apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/MessageCard.module.scss


+ 13 - 10
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard.tsx → apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/MessageCard.tsx

@@ -1,10 +1,10 @@
 import { type JSX } from 'react';
 import { type JSX } from 'react';
 
 
-import type { LinkProps } from 'next/link';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import ReactMarkdown from 'react-markdown';
 import ReactMarkdown from 'react-markdown';
 
 
-import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
+import { Header } from './ReactMarkdownComponents/Header';
+import { NextLinkWrapper } from './ReactMarkdownComponents/NextLinkWrapper';
 
 
 import styles from './MessageCard.module.scss';
 import styles from './MessageCard.module.scss';
 
 
@@ -24,13 +24,6 @@ const UserMessageCard = ({ children }: { children: string }): JSX.Element => (
 
 
 const assistantMessageCardModuleClass = styles['assistant-message-card'] ?? '';
 const assistantMessageCardModuleClass = styles['assistant-message-card'] ?? '';
 
 
-const NextLinkWrapper = (props: LinkProps & {children: string, href: string}): JSX.Element => {
-  return (
-    <NextLink href={props.href} className="link-primary">
-      {props.children}
-    </NextLink>
-  );
-};
 
 
 const AssistantMessageCard = ({
 const AssistantMessageCard = ({
   children,
   children,
@@ -51,7 +44,17 @@ const AssistantMessageCard = ({
           { children.length > 0
           { children.length > 0
             ? (
             ? (
               <>
               <>
-                <ReactMarkdown components={{ a: NextLinkWrapper }}>{children}</ReactMarkdown>
+                <ReactMarkdown components={{
+                  a: NextLinkWrapper,
+                  h1: ({ children }) => <Header level={1}>{children}</Header>,
+                  h2: ({ children }) => <Header level={2}>{children}</Header>,
+                  h3: ({ children }) => <Header level={3}>{children}</Header>,
+                  h4: ({ children }) => <Header level={4}>{children}</Header>,
+                  h5: ({ children }) => <Header level={5}>{children}</Header>,
+                  h6: ({ children }) => <Header level={6}>{children}</Header>,
+                }}
+                >{children}
+                </ReactMarkdown>
                 { additionalItem }
                 { additionalItem }
               </>
               </>
             )
             )

+ 25 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/ReactMarkdownComponents/Header.tsx

@@ -0,0 +1,25 @@
+type Level = 1 | 2 | 3 | 4 | 5 | 6;
+
+const fontSizes: Record<Level, string> = {
+  1: '1.5rem',
+  2: '1.25rem',
+  3: '1rem',
+  4: '0.875rem',
+  5: '0.75rem',
+  6: '0.625rem',
+};
+
+export const Header = ({ children, level }: { children: React.ReactNode, level: Level}): JSX.Element => {
+  const Tag = `h${level}` as keyof JSX.IntrinsicElements;
+
+  return (
+    <Tag
+      style={{
+        fontSize: fontSizes[level],
+        lineHeight: 1.4,
+      }}
+    >
+      {children}
+    </Tag>
+  );
+};

+ 13 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/MessageCard/ReactMarkdownComponents/NextLinkWrapper.tsx

@@ -0,0 +1,13 @@
+import React from 'react';
+
+import type { LinkProps } from 'next/link';
+
+import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
+
+export const NextLinkWrapper = (props: LinkProps & {children: React.ReactNode, href: string}): JSX.Element => {
+  return (
+    <NextLink href={props.href} className="link-primary">
+      {props.children}
+    </NextLink>
+  );
+};

+ 11 - 6
apps/app/src/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton.tsx

@@ -10,9 +10,8 @@ import { useAiAssistantSidebar, useSWRxAiAssistants } from '../../stores/ai-assi
 
 
 import styles from './OpenDefaultAiAssistantButton.module.scss';
 import styles from './OpenDefaultAiAssistantButton.module.scss';
 
 
-const OpenDefaultAiAssistantButton = (): JSX.Element => {
+const OpenDefaultAiAssistantButtonSubstance = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: isAiEnabled } = useIsAiEnabled();
   const { data: aiAssistantData } = useSWRxAiAssistants();
   const { data: aiAssistantData } = useSWRxAiAssistants();
   const { openChat } = useAiAssistantSidebar();
   const { openChat } = useAiAssistantSidebar();
 
 
@@ -33,10 +32,6 @@ const OpenDefaultAiAssistantButton = (): JSX.Element => {
     openChat(defaultAiAssistant);
     openChat(defaultAiAssistant);
   }, [defaultAiAssistant, openChat]);
   }, [defaultAiAssistant, openChat]);
 
 
-  if (!isAiEnabled) {
-    return <></>;
-  }
-
   return (
   return (
     <NotAvailableForGuest>
     <NotAvailableForGuest>
       <NotAvailable isDisabled={defaultAiAssistant == null} title={t('default_ai_assistant.not_set')}>
       <NotAvailable isDisabled={defaultAiAssistant == null} title={t('default_ai_assistant.not_set')}>
@@ -52,4 +47,14 @@ const OpenDefaultAiAssistantButton = (): JSX.Element => {
   );
   );
 };
 };
 
 
+const OpenDefaultAiAssistantButton = (): JSX.Element => {
+  const { data: isAiEnabled } = useIsAiEnabled();
+
+  if (!isAiEnabled) {
+    return <></>;
+  }
+
+  return <OpenDefaultAiAssistantButtonSubstance />;
+};
+
 export default OpenDefaultAiAssistantButton;
 export default OpenDefaultAiAssistantButton;

+ 57 - 23
apps/app/src/features/openai/server/routes/edit/index.ts

@@ -69,29 +69,63 @@ const withMarkdownCaution = `# IMPORTANT:
 `;
 `;
 
 
 function instruction(withMarkdown: boolean): string {
 function instruction(withMarkdown: boolean): string {
-  return `# RESPONSE FORMAT:
-You must respond with a JSON object in the following format example:
-{
-  "contents": [
-    { "message": "Your brief message about the upcoming change or proposal.\n\n" },
-    { "replace": "New text 1" },
-    { "message": "Additional explanation if needed" },
-    { "replace": "New text 2" },
-    ...more items if needed
-    { "message": "Your friendly message explaining what changes were made or suggested." }
-  ]
-}
-
-The array should contain:
-- [At the beginning of the list] A "message" object that has your brief message about the upcoming change or proposal. Be sure to add two consecutive line feeds ('\n\n') at the end.
-- Objects with a "message" key for explanatory text to the user if needed.
-- Edit markdown according to user instructions and include it line by line in the 'replace' object. ${withMarkdown ? 'Return original text for lines that do not need editing.' : ''}
-- [At the end of the list] A "message" object that contains your friendly message explaining that the operation was completed and what changes were made.
-
-${withMarkdown ? withMarkdownCaution : ''}
-
-# Multilingual Support:
-Always provide messages in the same language as the user's request.`;
+  return `
+  # USER INTENT DETECTION:
+  First, analyze the user's message to determine their intent:
+  - **Consultation Type**: Questions, discussions, explanations, or advice seeking WITHOUT explicit request to edit/modify/generate text
+  - **Edit Type**: Clear requests to edit, modify, fix, generate, create, or write content
+
+  ## EXAMPLES OF USER INTENT:
+  ### Consultation Type Examples:
+  - "What do you think about this code?"
+  - "Please give me advice on this text structure"
+  - "Why is this error occurring?"
+  - "Is there a better approach?"
+  - "Can you explain how this works?"
+  - "What are the pros and cons of this method?"
+  - "How should I organize this document?"
+
+  ### Edit Type Examples:
+  - "Please fix the following"
+  - "Add a function that..."
+  - "Rewrite this section to..."
+  - "Correct the errors in this code"
+  - "Generate a new paragraph about..."
+  - "Modify this to include..."
+  - "Create a template for..."
+
+  # RESPONSE FORMAT:
+  ## For Consultation Type (discussion/advice only):
+  Respond with a JSON object containing ONLY message objects:
+  {
+    "contents": [
+      { "message": "Your thoughtful response to the user's question or consultation.\n\nYou can use multiple paragraphs as needed." }
+    ]
+  }
+
+  ## For Edit Type (explicit editing request):
+  Respond with a JSON object in the following format:
+  {
+    "contents": [
+      { "message": "Your brief message about the upcoming change or proposal.\n\n" },
+      { "replace": "New text 1" },
+      { "message": "Additional explanation if needed" },
+      { "replace": "New text 2" },
+      ...more items if needed
+      { "message": "Your friendly message explaining what changes were made or suggested." }
+    ]
+  }
+
+  The array should contain:
+  - [At the beginning of the list] A "message" object that has your brief message about the upcoming change or proposal. Be sure to add two consecutive line feeds ('\n\n') at the end.
+  - Objects with a "message" key for explanatory text to the user if needed.
+  - Edit markdown according to user instructions and include it line by line in the 'replace' object. ${withMarkdown ? 'Return original text for lines that do not need editing.' : ''}
+  - [At the end of the list] A "message" object that contains your friendly message explaining that the operation was completed and what changes were made.
+
+  ${withMarkdown ? withMarkdownCaution : ''}
+
+  # Multilingual Support:
+  Always provide messages in the same language as the user's request.`;
 }
 }
 /* eslint-disable max-len */
 /* eslint-disable max-len */
 
 

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

@@ -47,7 +47,7 @@ import {
   useIsLocalAccountRegistrationEnabled,
   useIsLocalAccountRegistrationEnabled,
   useIsRomUserAllowedToComment,
   useIsRomUserAllowedToComment,
   useIsPdfBulkExportEnabled,
   useIsPdfBulkExportEnabled,
-  useIsAiEnabled, useLimitLearnablePageCountPerAssistant,
+  useIsAiEnabled, useLimitLearnablePageCountPerAssistant, useIsUsersHomepageDeletionEnabled,
 } from '~/stores-universal/context';
 } from '~/stores-universal/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import {
 import {
@@ -200,6 +200,7 @@ type Props = CommonProps & {
 
 
   aiEnabled: boolean,
   aiEnabled: boolean,
   limitLearnablePageCountPerAssistant: number,
   limitLearnablePageCountPerAssistant: number,
+  isUsersHomepageDeletionEnabled: boolean,
 };
 };
 
 
 const Page: NextPageWithLayout<Props> = (props: Props) => {
 const Page: NextPageWithLayout<Props> = (props: Props) => {
@@ -258,6 +259,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsAiEnabled(props.aiEnabled);
   useIsAiEnabled(props.aiEnabled);
   useLimitLearnablePageCountPerAssistant(props.limitLearnablePageCountPerAssistant);
   useLimitLearnablePageCountPerAssistant(props.limitLearnablePageCountPerAssistant);
 
 
+  useIsUsersHomepageDeletionEnabled(props.isUsersHomepageDeletionEnabled);
+
+
   const { pageWithMeta } = props;
   const { pageWithMeta } = props;
 
 
   const pageId = pageWithMeta?.data._id;
   const pageId = pageWithMeta?.data._id;
@@ -576,7 +580,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.aiEnabled = configManager.getConfig('app:aiEnabled');
   props.aiEnabled = configManager.getConfig('app:aiEnabled');
   props.limitLearnablePageCountPerAssistant = configManager.getConfig('openai:limitLearnablePageCountPerAssistant');
   props.limitLearnablePageCountPerAssistant = configManager.getConfig('openai:limitLearnablePageCountPerAssistant');
-
+  props.isUsersHomepageDeletionEnabled = configManager.getConfig('security:user-homepage-deletion:isEnabled');
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceConfigured = searchService.isConfigured;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchServiceReachable = searchService.isReachable;
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
   props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');

+ 10 - 0
apps/app/src/server/models/openapi/object-id.ts

@@ -0,0 +1,10 @@
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      ObjectId:
+ *        type: string
+ *        description: Object ID
+ *        example: 5ae5fccfc5577b0004dbd8ab
+ */

+ 11 - 9
apps/app/src/server/models/openapi/page.ts

@@ -3,14 +3,20 @@
  *
  *
  *  components:
  *  components:
  *    schemas:
  *    schemas:
+ *      PagePath:
+ *        description: Page path
+ *        type: string
+ *        example: /path/to/page
+ *      PageGrant:
+ *        description: Grant for page
+ *        type: number
+ *        example: 1
  *      Page:
  *      Page:
  *        description: Page
  *        description: Page
  *        type: object
  *        type: object
  *        properties:
  *        properties:
  *          _id:
  *          _id:
- *            type: string
- *            description: page ID
- *            example: 5e07345972560e001761fa63
+ *            $ref: '#/components/schemas/ObjectId'
  *          __v:
  *          __v:
  *            type: number
  *            type: number
  *            description: DB record version
  *            description: DB record version
@@ -30,9 +36,7 @@
  *            description: extend data
  *            description: extend data
  *            example: {}
  *            example: {}
  *          grant:
  *          grant:
- *            type: number
- *            description: grant
- *            example: 1
+ *            $ref: '#/components/schemas/PageGrant'
  *          grantedUsers:
  *          grantedUsers:
  *            type: array
  *            type: array
  *            description: granted users
  *            description: granted users
@@ -50,9 +54,7 @@
  *              description: user ID
  *              description: user ID
  *            example: []
  *            example: []
  *          path:
  *          path:
- *            type: string
- *            description: page path
- *            example: /
+ *            $ref: '#/components/schemas/PagePath'
  *          revision:
  *          revision:
  *            type: string
  *            type: string
  *            description: page revision
  *            description: page revision

+ 12 - 8
apps/app/src/server/models/openapi/paginate-result.js → apps/app/src/server/models/openapi/paginate-result.ts

@@ -4,6 +4,14 @@
  *
  *
  *  components:
  *  components:
  *    schemas:
  *    schemas:
+ *      Offset:
+ *        description: Offset for pagination
+ *        type: integer
+ *        example: 0
+ *      Limit:
+ *        description: Limit for pagination
+ *        type: integer
+ *        example: 10
  *      PaginateResult:
  *      PaginateResult:
  *        description: PaginateResult
  *        description: PaginateResult
  *        type: object
  *        type: object
@@ -17,8 +25,7 @@
  *            type: number
  *            type: number
  *            description: Total number of documents in collection that match a query
  *            description: Total number of documents in collection that match a query
  *          limit:
  *          limit:
- *            type: number
- *            description: Limit that was used
+ *            $ref: '#/components/schemas/Limit'
  *          hasPrevPage:
  *          hasPrevPage:
  *            type: number
  *            type: number
  *            description: Availability of prev page.
  *            description: Availability of prev page.
@@ -32,8 +39,8 @@
  *            type: number
  *            type: number
  *            description: Total number of pages.
  *            description: Total number of pages.
  *          offset:
  *          offset:
- *            type: number
  *            description: Only if specified or default page/offset values were used
  *            description: Only if specified or default page/offset values were used
+ *            $ref: '#/components/schemas/Offset'
  *          prefPage:
  *          prefPage:
  *            type: number
  *            type: number
  *            description: Previous page number if available or NULL
  *            description: Previous page number if available or NULL
@@ -66,13 +73,10 @@
  *                description: Total number of documents in collection that match a query
  *                description: Total number of documents in collection that match a query
  *                example: 35
  *                example: 35
  *              limit:
  *              limit:
- *                type: integer
- *                description: Limit that was used
- *                example: 10
+ *                $ref: '#/components/schemas/Limit'
  *              offset:
  *              offset:
- *                type: integer
  *                description: Only if specified or default page/offset values were used
  *                description: Only if specified or default page/offset values were used
- *                example: 20
+ *                $ref: '#/components/schemas/Offset'
  *          data:
  *          data:
  *            type: object
  *            type: object
  *            description: Object of pagination meta data.
  *            description: Object of pagination meta data.

+ 12 - 10
apps/app/src/server/models/openapi/revision.ts

@@ -3,27 +3,29 @@
  *
  *
  *  components:
  *  components:
  *    schemas:
  *    schemas:
+ *      RevisionBody:
+ *        description: Revision content body
+ *        type: string
+ *        example: |
+ *          # Header
+ *
+ *          - foo
+ *          - bar
+ *
  *      Revision:
  *      Revision:
  *        description: Revision
  *        description: Revision
  *        type: object
  *        type: object
  *        properties:
  *        properties:
  *          _id:
  *          _id:
- *            type: string
- *            description: revision ID
- *            example: 5e0734e472560e001761fa68
+ *            $ref: '#/components/schemas/ObjectId'
  *          __v:
  *          __v:
  *            type: number
  *            type: number
  *            description: DB record version
  *            description: DB record version
  *            example: 0
  *            example: 0
  *          author:
  *          author:
- *            $ref: '#/components/schemas/User/properties/_id'
+ *            $ref: '#/components/schemas/ObjectId'
  *          body:
  *          body:
- *            type: string
- *            description: content body
- *            example: |
- *              # test
- *
- *              test
+ *            $ref: '#/components/schemas/RevisionBody'
  *          format:
  *          format:
  *            type: string
  *            type: string
  *            description: format
  *            description: format

+ 32 - 0
apps/app/src/server/models/openapi/tag.ts

@@ -0,0 +1,32 @@
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      Tags:
+ *        description: Tags
+ *        type: array
+ *        items:
+ *          $ref: '#/components/schemas/TagName'
+ *        example: ['daily', 'report', 'tips']
+ *
+ *      TagName:
+ *        description: Tag name
+ *        type: string
+ *        example: daily
+ *
+ *      Tag:
+ *        description: Tag
+ *        type: object
+ *        properties:
+ *          _id:
+ *            type: string
+ *            description: tag ID
+ *            example: 5e2d6aede35da4004ef7e0b7
+ *          name:
+ *            $ref: '#/components/schemas/TagName'
+ *          count:
+ *            type: number
+ *            description: Count of tagged pages
+ *            example: 3
+ */

+ 0 - 19
apps/app/src/server/models/openapi/v1-response.js

@@ -1,19 +0,0 @@
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      V1Response:
- *        description: Response v1
- *        type: object
- *        properties:
- *          ok:
- *            type: boolean
- *            description: API is succeeded
- *            example: true
- *    responses:
- *      403:
- *        description: 'Forbidden'
- *      500:
- *        description: 'Internal Server Error'
- */

+ 131 - 0
apps/app/src/server/models/openapi/v1-response.ts

@@ -0,0 +1,131 @@
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *
+ *      # Common API Response Schemas (modern pattern)
+ *      ApiResponseBase:
+ *        type: object
+ *        required:
+ *          - ok
+ *        properties:
+ *          ok:
+ *            type: boolean
+ *            description: Indicates if the request was successful
+ *
+ *      ApiResponseSuccess:
+ *        description: Successful API response
+ *        allOf:
+ *          - $ref: '#/components/schemas/ApiResponseBase'
+ *          - type: object
+ *            properties:
+ *              ok:
+ *                type: boolean
+ *                enum: [true]
+ *                example: true
+ *                description: Success indicator (always true for successful responses)
+ *
+ *      ApiResponseError:
+ *        description: Error API response
+ *        allOf:
+ *          - $ref: '#/components/schemas/ApiResponseBase'
+ *          - type: object
+ *            properties:
+ *              ok:
+ *                type: boolean
+ *                enum: [false]
+ *                example: false
+ *                description: Success indicator (always false for error responses)
+ *              error:
+ *                oneOf:
+ *                  - type: string
+ *                    description: Simple error message
+ *                    example: "Invalid parameter"
+ *                  - type: object
+ *                    description: Detailed error object
+ *                    example: { "code": "VALIDATION_ERROR", "message": "Field validation failed" }
+ *                description: Error message or error object containing details about the failure
+ *
+ *    responses:
+ *      # Common error responses
+ *      BadRequest:
+ *        description: Bad request
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            examples:
+ *              missingParameter:
+ *                summary: Missing required parameter
+ *                value:
+ *                  ok: false
+ *                  error: "Invalid parameter"
+ *              validationError:
+ *                summary: Validation error
+ *                value:
+ *                  ok: false
+ *                  error: "Validation failed"
+ *
+ *      Forbidden:
+ *        description: Forbidden - insufficient permissions
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            example:
+ *              ok: false
+ *              error: "Access denied"
+ *
+ *      NotFound:
+ *        description: Resource not found
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            examples:
+ *              resourceNotFound:
+ *                summary: Resource not found
+ *                value:
+ *                  ok: false
+ *                  error: "Resource not found"
+ *              notFoundOrForbidden:
+ *                summary: Resource not found or forbidden
+ *                value:
+ *                  ok: false
+ *                  error: "notfound_or_forbidden"
+ *
+ *      Conflict:
+ *        description: Conflict
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            examples:
+ *              resourceConflict:
+ *                summary: Resource conflict
+ *                value:
+ *                  ok: false
+ *                  error: "Resource conflict"
+ *              outdated:
+ *                summary: Resource was updated by someone else
+ *                value:
+ *                  ok: false
+ *                  error: "outdated"
+ *              alreadyExists:
+ *                summary: Resource already exists
+ *                value:
+ *                  ok: false
+ *                  error: "already_exists"
+ *
+ *      InternalServerError:
+ *        description: Internal server error
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            example:
+ *              ok: false
+ *              error: "Internal server error"
+ *
+ */

+ 0 - 1
apps/app/src/server/routes/apiv3/admin-home.ts

@@ -68,7 +68,6 @@ module.exports = (crowi) => {
    *    /admin-home/:
    *    /admin-home/:
    *      get:
    *      get:
    *        tags: [AdminHome]
    *        tags: [AdminHome]
-   *        operationId: getAdminHome
    *        summary: /admin-home
    *        summary: /admin-home
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []

+ 79 - 41
apps/app/src/server/routes/apiv3/app-settings.js

@@ -1,4 +1,6 @@
-import { ConfigSource } from '@growi/core/dist/interfaces';
+import {
+  ConfigSource, toNonBlankString, toNonBlankStringOrUndefined,
+} from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
 
 
@@ -367,6 +369,7 @@ module.exports = (crowi) => {
       body('gcsBucket').trim(),
       body('gcsBucket').trim(),
       body('gcsUploadNamespace').trim(),
       body('gcsUploadNamespace').trim(),
       body('gcsReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
       body('gcsReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
+      body('s3Bucket').trim(),
       body('s3Region')
       body('s3Region')
         .trim()
         .trim()
         .if(value => value !== '')
         .if(value => value !== '')
@@ -387,7 +390,6 @@ module.exports = (crowi) => {
           }
           }
           return true;
           return true;
         }),
         }),
-      body('s3Bucket').trim(),
       body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('s3AccessKeyId').trim().if(value => value !== '').matches(/^[\da-zA-Z]+$/),
       body('s3SecretAccessKey').trim(),
       body('s3SecretAccessKey').trim(),
       body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
       body('s3ReferenceFileWithRelayMode').if(value => value != null).isBoolean(),
@@ -418,7 +420,6 @@ module.exports = (crowi) => {
    *    /app-settings:
    *    /app-settings:
    *      get:
    *      get:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: getAppSettings
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []
@@ -515,7 +516,6 @@ module.exports = (crowi) => {
    *    /app-settings/app-setting:
    *    /app-settings/app-setting:
    *      put:
    *      put:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettings
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: /app-settings/app-setting
    *        summary: /app-settings/app-setting
@@ -576,7 +576,6 @@ module.exports = (crowi) => {
    *    /app-settings/site-url-setting:
    *    /app-settings/site-url-setting:
    *      put:
    *      put:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingSiteUrlSetting
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: /app-settings/site-url-setting
    *        summary: /app-settings/site-url-setting
@@ -727,7 +726,6 @@ module.exports = (crowi) => {
    *    /app-settings/smtp-setting:
    *    /app-settings/smtp-setting:
    *      put:
    *      put:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingSmtpSetting
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: /app-settings/smtp-setting
    *        summary: /app-settings/smtp-setting
@@ -779,7 +777,6 @@ module.exports = (crowi) => {
    *    /app-settings/smtp-test:
    *    /app-settings/smtp-test:
    *      post:
    *      post:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: postSmtpTest
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: /app-settings/smtp-setting
    *        summary: /app-settings/smtp-setting
@@ -816,7 +813,6 @@ module.exports = (crowi) => {
    *    /app-settings/ses-setting:
    *    /app-settings/ses-setting:
    *      put:
    *      put:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingSesSetting
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: /app-settings/ses-setting
    *        summary: /app-settings/ses-setting
@@ -868,7 +864,6 @@ module.exports = (crowi) => {
    *    /app-settings/file-upload-settings:
    *    /app-settings/file-upload-settings:
    *      put:
    *      put:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingFileUploadSetting
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: /app-settings/file-upload-setting
    *        summary: /app-settings/file-upload-setting
@@ -895,42 +890,88 @@ module.exports = (crowi) => {
   router.put('/file-upload-setting', loginRequiredStrictly, adminRequired, addActivity, validator.fileUploadSetting, apiV3FormValidator, async(req, res) => {
   router.put('/file-upload-setting', loginRequiredStrictly, adminRequired, addActivity, validator.fileUploadSetting, apiV3FormValidator, async(req, res) => {
     const { fileUploadType } = req.body;
     const { fileUploadType } = req.body;
 
 
-    const requestParams = {
-      'app:fileUploadType': fileUploadType,
-    };
-
-    if (fileUploadType === 'gcs') {
-      requestParams['gcs:apiKeyJsonPath'] = req.body.gcsApiKeyJsonPath;
-      requestParams['gcs:bucket'] = req.body.gcsBucket;
-      requestParams['gcs:uploadNamespace'] = req.body.gcsUploadNamespace;
-      requestParams['gcs:referenceFileWithRelayMode'] = req.body.gcsReferenceFileWithRelayMode;
+    if (fileUploadType === 'aws') {
+      try {
+        try {
+          toNonBlankString(req.body.s3Bucket);
+        }
+        catch (err) {
+          throw new Error('S3 Bucket name is required');
+        }
+        try {
+          toNonBlankString(req.body.s3Region);
+        }
+        catch (err) {
+          throw new Error('S3 Region is required');
+        }
+        await configManager.updateConfigs({
+          'app:fileUploadType': fileUploadType,
+          'aws:s3Region': toNonBlankString(req.body.s3Region),
+          'aws:s3Bucket': toNonBlankString(req.body.s3Bucket),
+          'aws:referenceFileWithRelayMode': req.body.s3ReferenceFileWithRelayMode,
+        },
+        { skipPubsub: true });
+        await configManager.updateConfigs({
+          'aws:s3CustomEndpoint': toNonBlankStringOrUndefined(req.body.s3CustomEndpoint),
+          'aws:s3AccessKeyId': toNonBlankStringOrUndefined(req.body.s3AccessKeyId),
+          'aws:s3SecretAccessKey': toNonBlankStringOrUndefined(req.body.s3SecretAccessKey),
+        },
+        {
+          skipPubsub: true,
+          removeIfUndefined: true,
+        });
+      }
+      catch (err) {
+        const msg = `Error occurred in updating AWS S3 settings: ${err.message}`;
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
+      }
     }
     }
 
 
-    if (fileUploadType === 'aws') {
-      requestParams['aws:s3Region'] = req.body.s3Region;
-      requestParams['aws:s3CustomEndpoint'] = req.body.s3CustomEndpoint;
-      requestParams['aws:s3Bucket'] = req.body.s3Bucket;
-      requestParams['aws:s3AccessKeyId'] = req.body.s3AccessKeyId;
-      requestParams['aws:referenceFileWithRelayMode'] = req.body.s3ReferenceFileWithRelayMode;
+    if (fileUploadType === 'gcs') {
+      try {
+        await configManager.updateConfigs({
+          'app:fileUploadType': fileUploadType,
+          'gcs:referenceFileWithRelayMode': req.body.gcsReferenceFileWithRelayMode,
+        },
+        { skipPubsub: true });
+        await configManager.updateConfigs({
+          'gcs:apiKeyJsonPath': toNonBlankStringOrUndefined(req.body.gcsApiKeyJsonPath),
+          'gcs:bucket': toNonBlankStringOrUndefined(req.body.gcsBucket),
+          'gcs:uploadNamespace': toNonBlankStringOrUndefined(req.body.gcsUploadNamespace),
+        },
+        { skipPubsub: true, removeIfUndefined: true });
+      }
+      catch (err) {
+        const msg = `Error occurred in updating GCS settings: ${err.message}`;
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
+      }
     }
     }
 
 
     if (fileUploadType === 'azure') {
     if (fileUploadType === 'azure') {
-      requestParams['azure:tenantId'] = req.body.azureTenantId;
-      requestParams['azure:clientId'] = req.body.azureClientId;
-      requestParams['azure:clientSecret'] = req.body.azureClientSecret;
-      requestParams['azure:storageAccountName'] = req.body.azureStorageAccountName;
-      requestParams['azure:storageContainerName'] = req.body.azureStorageContainerName;
-      requestParams['azure:referenceFileWithRelayMode'] = req.body.azureReferenceFileWithRelayMode;
+      try {
+        await configManager.updateConfigs({
+          'app:fileUploadType': fileUploadType,
+          'azure:referenceFileWithRelayMode': req.body.azureReferenceFileWithRelayMode,
+        },
+        { skipPubsub: true });
+        await configManager.updateConfigs({
+          'azure:tenantId': toNonBlankStringOrUndefined(req.body.azureTenantId),
+          'azure:clientId': toNonBlankStringOrUndefined(req.body.azureClientId),
+          'azure:clientSecret': toNonBlankStringOrUndefined(req.body.azureClientSecret),
+          'azure:storageAccountName': toNonBlankStringOrUndefined(req.body.azureStorageAccountName),
+          'azure:storageContainerName': toNonBlankStringOrUndefined(req.body.azureStorageContainerName),
+        }, { skipPubsub: true, removeIfUndefined: true });
+      }
+      catch (err) {
+        const msg = `Error occurred in updating Azure settings: ${err.message}`;
+        logger.error('Error', err);
+        return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
+      }
     }
     }
 
 
     try {
     try {
-      await configManager.updateConfigs(requestParams, { skipPubsub: true });
-
-      const s3SecretAccessKey = req.body.s3SecretAccessKey;
-      if (fileUploadType === 'aws' && s3SecretAccessKey != null && s3SecretAccessKey.trim() !== '') {
-        await configManager.updateConfigs({ 'aws:s3SecretAccessKey': s3SecretAccessKey }, { skipPubsub: true });
-      }
-
       await crowi.setUpFileUpload(true);
       await crowi.setUpFileUpload(true);
       crowi.fileUploaderSwitchService.publishUpdatedMessage();
       crowi.fileUploaderSwitchService.publishUpdatedMessage();
 
 
@@ -966,7 +1007,7 @@ module.exports = (crowi) => {
       return res.apiv3({ responseParams });
       return res.apiv3({ responseParams });
     }
     }
     catch (err) {
     catch (err) {
-      const msg = 'Error occurred in updating fileUploadType';
+      const msg = 'Error occurred in retrieving file upload configurations';
       logger.error('Error', err);
       logger.error('Error', err);
       return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
       return res.apiv3Err(new ErrorV3(msg, 'update-fileUploadType-failed'));
     }
     }
@@ -979,7 +1020,6 @@ module.exports = (crowi) => {
    *    /app-settings/questionnaire-settings:
    *    /app-settings/questionnaire-settings:
    *      put:
    *      put:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingQuestionnaireSettings
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: /app-settings/questionnaire-settings
    *        summary: /app-settings/questionnaire-settings
@@ -1064,7 +1104,6 @@ module.exports = (crowi) => {
    *    /app-settings/v5-schema-migration:
    *    /app-settings/v5-schema-migration:
    *      post:
    *      post:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingV5SchemaMigration
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []
@@ -1110,7 +1149,6 @@ module.exports = (crowi) => {
    *    /app-settings/maintenance-mode:
    *    /app-settings/maintenance-mode:
    *      post:
    *      post:
    *        tags: [AppSettings]
    *        tags: [AppSettings]
-   *        operationId: updateAppSettingMaintenanceMode
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []

+ 4 - 6
apps/app/src/server/routes/apiv3/attachment.js

@@ -245,7 +245,6 @@ module.exports = (crowi) => {
    *    /attachment/limit:
    *    /attachment/limit:
    *      get:
    *      get:
    *        tags: [Attachment]
    *        tags: [Attachment]
-   *        operationId: getAttachmentLimit
    *        summary: /attachment/limit
    *        summary: /attachment/limit
    *        description: Get available capacity of uploaded file with GridFS
    *        description: Get available capacity of uploaded file with GridFS
    *        parameters:
    *        parameters:
@@ -268,9 +267,9 @@ module.exports = (crowi) => {
    *                      description: uploadable
    *                      description: uploadable
    *                      example: true
    *                      example: true
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   router.get('/limit', accessTokenParser, loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator, async(req, res) => {
   router.get('/limit', accessTokenParser, loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator, async(req, res) => {
     const { fileUploadService } = crowi;
     const { fileUploadService } = crowi;
@@ -290,7 +289,6 @@ module.exports = (crowi) => {
    *    /attachment:
    *    /attachment:
    *      post:
    *      post:
    *        tags: [Attachment]
    *        tags: [Attachment]
-   *        operationId: addAttachment
    *        summary: /attachment
    *        summary: /attachment
    *        description: Add attachment to the page
    *        description: Add attachment to the page
    *        requestBody:
    *        requestBody:
@@ -335,9 +333,9 @@ module.exports = (crowi) => {
    *                    revision:
    *                    revision:
    *                      type: string
    *                      type: string
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, uploads.single('file'),
   router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, uploads.single('file'),
     validator.retrieveAddAttachment, apiV3FormValidator, addActivity,
     validator.retrieveAddAttachment, apiV3FormValidator, addActivity,

+ 0 - 6
apps/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -128,7 +128,6 @@ module.exports = (crowi) => {
    *    /bookmark-folder:
    *    /bookmark-folder:
    *      post:
    *      post:
    *        tags: [BookmarkFolders]
    *        tags: [BookmarkFolders]
-   *        operationId: createBookmarkFolder
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []
@@ -184,7 +183,6 @@ module.exports = (crowi) => {
    *    /bookmark-folder/list/{userId}:
    *    /bookmark-folder/list/{userId}:
    *      get:
    *      get:
    *        tags: [BookmarkFolders]
    *        tags: [BookmarkFolders]
-   *        operationId: listBookmarkFolders
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []
@@ -274,7 +272,6 @@ module.exports = (crowi) => {
    *    /bookmark-folder/{id}:
    *    /bookmark-folder/{id}:
    *      delete:
    *      delete:
    *        tags: [BookmarkFolders]
    *        tags: [BookmarkFolders]
-   *        operationId: deleteBookmarkFolder
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []
@@ -318,7 +315,6 @@ module.exports = (crowi) => {
    *    /bookmark-folder:
    *    /bookmark-folder:
    *      put:
    *      put:
    *        tags: [BookmarkFolders]
    *        tags: [BookmarkFolders]
-   *        operationId: updateBookmarkFolder
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []
@@ -376,7 +372,6 @@ module.exports = (crowi) => {
    *    /bookmark-folder/add-bookmark-to-folder:
    *    /bookmark-folder/add-bookmark-to-folder:
    *      post:
    *      post:
    *        tags: [BookmarkFolders]
    *        tags: [BookmarkFolders]
-   *        operationId: addBookmarkToFolder
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []
@@ -427,7 +422,6 @@ module.exports = (crowi) => {
    *    /bookmark-folder/update-bookmark:
    *    /bookmark-folder/update-bookmark:
    *      put:
    *      put:
    *        tags: [BookmarkFolders]
    *        tags: [BookmarkFolders]
-   *        operationId: updateBookmarkInFolder
    *        security:
    *        security:
    *          - bearer: []
    *          - bearer: []
    *          - accessTokenInQuery: []
    *          - accessTokenInQuery: []

+ 1 - 4
apps/app/src/server/routes/apiv3/bookmarks.js

@@ -42,7 +42,7 @@ const router = express.Router();
  *          page:
  *          page:
  *            $ref: '#/components/schemas/Page'
  *            $ref: '#/components/schemas/Page'
  *          user:
  *          user:
- *            $ref: '#/components/schemas/User/properties/_id'
+ *            $ref: '#/components/schemas/ObjectId'
  *      Bookmarks:
  *      Bookmarks:
  *        description: User Root Bookmarks
  *        description: User Root Bookmarks
  *        type: object
  *        type: object
@@ -110,7 +110,6 @@ module.exports = (crowi) => {
    *        tags: [Bookmarks]
    *        tags: [Bookmarks]
    *        summary: /bookmarks/info
    *        summary: /bookmarks/info
    *        description: Get bookmarked info
    *        description: Get bookmarked info
-   *        operationId: getBookmarkedInfo
    *        parameters:
    *        parameters:
    *          - name: pageId
    *          - name: pageId
    *            in: query
    *            in: query
@@ -172,7 +171,6 @@ module.exports = (crowi) => {
    *        tags: [Bookmarks]
    *        tags: [Bookmarks]
    *        summary: /bookmarks/{userId}
    *        summary: /bookmarks/{userId}
    *        description: Get my bookmarked status
    *        description: Get my bookmarked status
-   *        operationId: getMyBookmarkedStatus
    *        parameters:
    *        parameters:
    *          - name: userId
    *          - name: userId
    *            in: path
    *            in: path
@@ -232,7 +230,6 @@ module.exports = (crowi) => {
    *        tags: [Bookmarks]
    *        tags: [Bookmarks]
    *        summary: /bookmarks
    *        summary: /bookmarks
    *        description: Update bookmarked status
    *        description: Update bookmarked status
-   *        operationId: updateBookmarkedStatus
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
    *            application/json:
    *            application/json:

+ 1 - 17
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -259,7 +259,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getCustomizeSetting
    *        summary: /customize-setting
    *        summary: /customize-setting
    *        description: Get customize parameters
    *        description: Get customize parameters
    *        responses:
    *        responses:
@@ -306,7 +305,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getLayoutCustomizeSetting
    *        summary: /customize-setting/layout
    *        summary: /customize-setting/layout
    *        description: Get layout
    *        description: Get layout
    *        responses:
    *        responses:
@@ -335,7 +333,6 @@ module.exports = (crowi) => {
    *    /customize-setting/layout:
    *    /customize-setting/layout:
    *      put:
    *      put:
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
-   *        operationId: updateLayoutCustomizeSetting
    *        summary: /customize-setting/layout
    *        summary: /customize-setting/layout
    *        description: Update layout
    *        description: Update layout
    *        requestBody:
    *        requestBody:
@@ -388,7 +385,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getThemeCustomizeSetting
    *        summary: /customize-setting/theme
    *        summary: /customize-setting/theme
    *        description: Get theme
    *        description: Get theme
    *        responses:
    *        responses:
@@ -437,7 +433,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateThemeCustomizeSetting
    *        summary: /customize-setting/theme
    *        summary: /customize-setting/theme
    *        description: Update theme
    *        description: Update theme
    *        requestBody:
    *        requestBody:
@@ -487,7 +482,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getCustomeSettingSidebar
    *        summary: /customize-setting/sidebar
    *        summary: /customize-setting/sidebar
    *        description: Get sidebar
    *        description: Get sidebar
    *        responses:
    *        responses:
@@ -520,7 +514,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateCustomizeSettingSidebar
    *        summary: /customize-setting/sidebar
    *        summary: /customize-setting/sidebar
    *        description: Update sidebar
    *        description: Update sidebar
    *        requestBody:
    *        requestBody:
@@ -572,7 +565,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *         - cookieAuth: []
    *         - cookieAuth: []
-   *        operationId: updateFunctionCustomizeSetting
    *        summary: /customize-setting/function
    *        summary: /customize-setting/function
    *        description: Update function
    *        description: Update function
    *        requestBody:
    *        requestBody:
@@ -640,7 +632,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *         - cookieAuth: []
    *         - cookieAuth: []
-   *        operationId: updatePresentationCustomizeSetting
    *        summary: /customize-setting/presentation
    *        summary: /customize-setting/presentation
    *        description: Update presentation
    *        description: Update presentation
    *        requestBody:
    *        requestBody:
@@ -689,7 +680,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateHighlightCustomizeSetting
    *        summary: /customize-setting/highlight
    *        summary: /customize-setting/highlight
    *        description: Update highlight
    *        description: Update highlight
    *        requestBody:
    *        requestBody:
@@ -740,7 +730,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateCustomizeTitleCustomizeSetting
    *        summary: /customize-setting/customizeTitle
    *        summary: /customize-setting/customizeTitle
    *        description: Update title
    *        description: Update title
    *        requestBody:
    *        requestBody:
@@ -792,7 +781,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateCustomizeNoscriptCustomizeSetting
    *        summary: /customize-setting/customize-noscript
    *        summary: /customize-setting/customize-noscript
    *        description: Update noscript
    *        description: Update noscript
    *        requestBody:
    *        requestBody:
@@ -840,7 +828,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateCustomizeCssCustomizeSetting
    *        summary: /customize-setting/customize-css
    *        summary: /customize-setting/customize-css
    *        description: Update customize css
    *        description: Update customize css
    *        requestBody:
    *        requestBody:
@@ -891,7 +878,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateCustomizeScriptCustomizeSetting
    *        summary: /customize-setting/customize-script
    *        summary: /customize-setting/customize-script
    *        description: Update customize script
    *        description: Update customize script
    *        requestBody:
    *        requestBody:
@@ -939,7 +925,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateCustomizeLogoCustomizeSetting
    *        summary: /customize-setting/customize-logo
    *        summary: /customize-setting/customize-logo
    *        description: Update customize logo
    *        description: Update customize logo
    *        requestBody:
    *        requestBody:
@@ -990,7 +975,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: uploadBrandLogoCustomizeSetting
    *        summary: /customize-setting/upload-brand-logo
    *        summary: /customize-setting/upload-brand-logo
    *        description: Upload brand logo
    *        description: Upload brand logo
    *        requestBody:
    *        requestBody:
@@ -1001,6 +985,7 @@ module.exports = (crowi) => {
    *               type: object
    *               type: object
    *               properties:
    *               properties:
    *                 file:
    *                 file:
+   *                   type: string
    *                   format: binary
    *                   format: binary
    *        responses:
    *        responses:
    *          200:
    *          200:
@@ -1066,7 +1051,6 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: deleteBrandLogoCustomizeSetting
    *        summary: /customize-setting/delete-brand-logo
    *        summary: /customize-setting/delete-brand-logo
    *        description: Delete brand logo
    *        description: Delete brand logo
    *        responses:
    *        responses:

+ 0 - 3
apps/app/src/server/routes/apiv3/export.js

@@ -156,7 +156,6 @@ module.exports = (crowi) => {
    *  /export/status:
    *  /export/status:
    *    get:
    *    get:
    *      tags: [Export]
    *      tags: [Export]
-   *      operationId: getExportStatus
    *      summary: /export/status
    *      summary: /export/status
    *      description: get properties of stored zip files for export
    *      description: get properties of stored zip files for export
    *      responses:
    *      responses:
@@ -188,7 +187,6 @@ module.exports = (crowi) => {
    *  /export:
    *  /export:
    *    post:
    *    post:
    *      tags: [Export]
    *      tags: [Export]
-   *      operationId: createExport
    *      summary: /export
    *      summary: /export
    *      description: generate zipped jsons for collections
    *      description: generate zipped jsons for collections
    *      requestBody:
    *      requestBody:
@@ -241,7 +239,6 @@ module.exports = (crowi) => {
    *  /export/{fileName}:
    *  /export/{fileName}:
    *    delete:
    *    delete:
    *      tags: [Export]
    *      tags: [Export]
-   *      operationId: deleteExport
    *      summary: /export/{fileName}
    *      summary: /export/{fileName}
    *      description: delete the file
    *      description: delete the file
    *      parameters:
    *      parameters:

+ 2 - 0
apps/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -220,6 +220,7 @@ module.exports = (crowi: Crowi): Router => {
    *              type: object
    *              type: object
    *              properties:
    *              properties:
    *                file:
    *                file:
+   *                  type: string
    *                  format: binary
    *                  format: binary
    *                  description: The zip file of the data to be transferred
    *                  description: The zip file of the data to be transferred
    *                collections:
    *                collections:
@@ -349,6 +350,7 @@ module.exports = (crowi: Crowi): Router => {
    *              type: object
    *              type: object
    *              properties:
    *              properties:
    *                file:
    *                file:
+   *                  type: string
    *                  format: binary
    *                  format: binary
    *                  description: The zip file of the data to be transferred
    *                  description: The zip file of the data to be transferred
    *                attachmentMetadata:
    *                attachmentMetadata:

+ 0 - 1
apps/app/src/server/routes/apiv3/healthcheck.ts

@@ -111,7 +111,6 @@ module.exports = (crowi) => {
    *    get:
    *    get:
    *      tags: [Healthcheck]
    *      tags: [Healthcheck]
    *      security: []
    *      security: []
-   *      operationId: getHealthcheck
    *      summary: /healthcheck
    *      summary: /healthcheck
    *      description: Check whether the server is healthy or not
    *      description: Check whether the server is healthy or not
    *      parameters:
    *      parameters:

+ 1 - 5
apps/app/src/server/routes/apiv3/import.js

@@ -171,7 +171,6 @@ export default function route(crowi) {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: getImportSettingsParams
    *      summary: /import
    *      summary: /import
    *      description: Get import settings params
    *      description: Get import settings params
    *      responses:
    *      responses:
@@ -224,7 +223,6 @@ export default function route(crowi) {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: getImportStatus
    *      summary: /import/status
    *      summary: /import/status
    *      description: Get properties of stored zip files for import
    *      description: Get properties of stored zip files for import
    *      responses:
    *      responses:
@@ -256,7 +254,6 @@ export default function route(crowi) {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: executeImport
    *      summary: /import
    *      summary: /import
    *      description: import a collection from a zipped json
    *      description: import a collection from a zipped json
    *      requestBody:
    *      requestBody:
@@ -389,7 +386,6 @@ export default function route(crowi) {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: uploadImport
    *      summary: /import/upload
    *      summary: /import/upload
    *      description: upload a zip file
    *      description: upload a zip file
    *      requestBody:
    *      requestBody:
@@ -399,6 +395,7 @@ export default function route(crowi) {
    *              type: object
    *              type: object
    *              properties:
    *              properties:
    *                file:
    *                file:
+   *                  type: string
    *                  format: binary
    *                  format: binary
    *      responses:
    *      responses:
    *        200:
    *        200:
@@ -446,7 +443,6 @@ export default function route(crowi) {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: deleteImportAll
    *      summary: /import/all
    *      summary: /import/all
    *      description: Delete all zip files
    *      description: Delete all zip files
    *      responses:
    *      responses:

+ 0 - 4
apps/app/src/server/routes/apiv3/in-app-notification.ts

@@ -105,7 +105,6 @@ module.exports = (crowi) => {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: getInAppNotificationList
    *      summary: /in-app-notification/list
    *      summary: /in-app-notification/list
    *      description: Get the list of in-app notifications
    *      description: Get the list of in-app notifications
    *      parameters:
    *      parameters:
@@ -196,7 +195,6 @@ module.exports = (crowi) => {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: getInAppNotificationStatus
    *      summary: /in-app-notification/status
    *      summary: /in-app-notification/status
    *      description: Get the status of in-app notifications
    *      description: Get the status of in-app notifications
    *      responses:
    *      responses:
@@ -233,7 +231,6 @@ module.exports = (crowi) => {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: openInAppNotification
    *      summary: /in-app-notification/open
    *      summary: /in-app-notification/open
    *      description: Open the in-app notification
    *      description: Open the in-app notification
    *      requestBody:
    *      requestBody:
@@ -280,7 +277,6 @@ module.exports = (crowi) => {
    *      security:
    *      security:
    *        - bearer: []
    *        - bearer: []
    *        - accessTokenInQuery: []
    *        - accessTokenInQuery: []
-   *      operationId: openAllInAppNotification
    *      summary: /in-app-notification/all-statuses-open
    *      summary: /in-app-notification/all-statuses-open
    *      description: Open all in-app notifications
    *      description: Open all in-app notifications
    *      responses:
    *      responses:

+ 0 - 1
apps/app/src/server/routes/apiv3/installer.ts

@@ -43,7 +43,6 @@ module.exports = (crowi: Crowi): Router => {
    *    post:
    *    post:
    *      tags: [Install]
    *      tags: [Install]
    *      security: []
    *      security: []
-   *      operationId: Install
    *      summary: /installer
    *      summary: /installer
    *      description: Install GROWI
    *      description: Install GROWI
    *      requestBody:
    *      requestBody:

+ 0 - 1
apps/app/src/server/routes/apiv3/invited.ts

@@ -26,7 +26,6 @@ module.exports = (crowi: Crowi): Router => {
    *      tags: [Users]
    *      tags: [Users]
    *      security:
    *      security:
    *        - cookieAuth: []
    *        - cookieAuth: []
-   *      operationId: activateInvitedUser
    *      summary: /invited
    *      summary: /invited
    *      description: Activate invited user
    *      description: Activate invited user
    *      requestBody:
    *      requestBody:

+ 0 - 4
apps/app/src/server/routes/apiv3/markdown-setting.js

@@ -136,7 +136,6 @@ module.exports = (crowi) => {
    *        tags: [MarkDownSetting]
    *        tags: [MarkDownSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getMarkdownSetting
    *        summary: Get markdown parameters
    *        summary: Get markdown parameters
    *        responses:
    *        responses:
    *          200:
    *          200:
@@ -173,7 +172,6 @@ module.exports = (crowi) => {
    *        tags: [MarkDownSetting]
    *        tags: [MarkDownSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateLineBreakMarkdownSetting
    *        summary: Update lineBreak setting
    *        summary: Update lineBreak setting
    *        requestBody:
    *        requestBody:
    *          required: true
    *          required: true
@@ -228,7 +226,6 @@ module.exports = (crowi) => {
    *        tags: [MarkDownSetting]
    *        tags: [MarkDownSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateIndentMarkdownSetting
    *        summary: Update indent setting
    *        summary: Update indent setting
    *        requestBody:
    *        requestBody:
    *          required: true
    *          required: true
@@ -284,7 +281,6 @@ module.exports = (crowi) => {
    *        tags: [MarkDownSetting]
    *        tags: [MarkDownSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateXssMarkdownSetting
    *        summary: Update XSS setting
    *        summary: Update XSS setting
    *        description: Update xss
    *        description: Update xss
    *        requestBody:
    *        requestBody:

+ 0 - 1
apps/app/src/server/routes/apiv3/mongo.js

@@ -18,7 +18,6 @@ module.exports = (crowi) => {
    *  /mongo/collections:
    *  /mongo/collections:
    *    get:
    *    get:
    *      tags: [MongoDB]
    *      tags: [MongoDB]
-   *      operationId: getMongoCollections
    *      summary: /mongo/collections
    *      summary: /mongo/collections
    *      description: get mongodb collections names
    *      description: get mongodb collections names
    *      responses:
    *      responses:

+ 12 - 20
apps/app/src/server/routes/apiv3/page/index.ts

@@ -190,7 +190,6 @@ module.exports = (crowi) => {
    *    /page:
    *    /page:
    *      get:
    *      get:
    *        tags: [Page]
    *        tags: [Page]
-   *        operationId: getPage
    *        summary: Get page
    *        summary: Get page
    *        description: get page by pagePath or pageId
    *        description: get page by pagePath or pageId
    *        parameters:
    *        parameters:
@@ -198,12 +197,12 @@ module.exports = (crowi) => {
    *            in: query
    *            in: query
    *            description: page id
    *            description: page id
    *            schema:
    *            schema:
-   *              $ref: '#/components/schemas/Page/properties/_id'
+   *              $ref: '#/components/schemas/ObjectId'
    *          - name: path
    *          - name: path
    *            in: query
    *            in: query
    *            description: page path
    *            description: page path
    *            schema:
    *            schema:
-   *              $ref: '#/components/schemas/Page/properties/path'
+   *              $ref: '#/components/schemas/PagePath'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Page data
    *            description: Page data
@@ -304,7 +303,6 @@ module.exports = (crowi) => {
    *      post:
    *      post:
    *        tags: [Page]
    *        tags: [Page]
    *        summary: Create page
    *        summary: Create page
-   *        operationId: createPage
    *        description: Create page
    *        description: Create page
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
@@ -315,9 +313,9 @@ module.exports = (crowi) => {
    *                    type: string
    *                    type: string
    *                    description: Text of page
    *                    description: Text of page
    *                  path:
    *                  path:
-   *                    $ref: '#/components/schemas/Page/properties/path'
+   *                    $ref: '#/components/schemas/PagePath'
    *                  grant:
    *                  grant:
-   *                    $ref: '#/components/schemas/Page/properties/grant'
+   *                    $ref: '#/components/schemas/PageGrant'
    *                  grantUserGroupIds:
    *                  grantUserGroupIds:
    *                    type: string
    *                    type: string
    *                    description: UserGroup ID
    *                    description: UserGroup ID
@@ -358,7 +356,6 @@ module.exports = (crowi) => {
    *    /page:
    *    /page:
    *      put:
    *      put:
    *        tags: [Page]
    *        tags: [Page]
-   *        operationId: updatePage
    *        description: Update page
    *        description: Update page
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
@@ -366,13 +363,13 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                properties:
    *                properties:
    *                  body:
    *                  body:
-   *                    $ref: '#/components/schemas/Revision/properties/body'
+   *                    $ref: '#/components/schemas/RevisionBody'
    *                  pageId:
    *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                  revisionId:
    *                  revisionId:
-   *                    $ref: '#/components/schemas/Revision/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                  grant:
    *                  grant:
-   *                    $ref: '#/components/schemas/Page/properties/grant'
+   *                    $ref: '#/components/schemas/PageGrant'
    *                  userRelatedGrantUserGroupIds:
    *                  userRelatedGrantUserGroupIds:
    *                    type: array
    *                    type: array
    *                    items:
    *                    items:
@@ -412,9 +409,9 @@ module.exports = (crowi) => {
    *                        revision:
    *                        revision:
    *                          $ref: '#/components/schemas/Revision'
    *                          $ref: '#/components/schemas/Revision'
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   router.put('/', updatePageHandlersFactory(crowi));
   router.put('/', updatePageHandlersFactory(crowi));
 
 
@@ -426,7 +423,6 @@ module.exports = (crowi) => {
    *        tags: [Page]
    *        tags: [Page]
    *        summary: Get page likes
    *        summary: Get page likes
    *        description: Update liked status
    *        description: Update liked status
-   *        operationId: updateLikedStatus
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
    *            application/json:
    *            application/json:
@@ -494,7 +490,6 @@ module.exports = (crowi) => {
    *        tags: [Page]
    *        tags: [Page]
    *        summary: Get page info
    *        summary: Get page info
    *        description: Retrieve current page info
    *        description: Retrieve current page info
-   *        operationId: getPageInfo
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
    *            application/json:
    *            application/json:
@@ -538,13 +533,12 @@ module.exports = (crowi) => {
    *        tags: [Page]
    *        tags: [Page]
    *        summary: Get page grant data
    *        summary: Get page grant data
    *        description: Retrieve current page's grant data
    *        description: Retrieve current page's grant data
-   *        operationId: getPageGrantData
    *        parameters:
    *        parameters:
    *          - name: pageId
    *          - name: pageId
    *            in: query
    *            in: query
    *            description: page id
    *            description: page id
    *            schema:
    *            schema:
-   *              $ref: '#/components/schemas/Page/properties/_id'
+   *              $ref: '#/components/schemas/ObjectId'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Successfully retrieved current grant data.
    *            description: Successfully retrieved current grant data.
@@ -951,7 +945,6 @@ module.exports = (crowi) => {
    *          - cookieAuth: []
    *          - cookieAuth: []
    *        summary: Get already exist paths
    *        summary: Get already exist paths
    *        description: Get already exist paths
    *        description: Get already exist paths
-   *        operationId: getAlreadyExistPaths
    *        parameters:
    *        parameters:
    *          - name: fromPath
    *          - name: fromPath
    *            in: query
    *            in: query
@@ -1012,14 +1005,13 @@ module.exports = (crowi) => {
    *        tags: [Page]
    *        tags: [Page]
    *        summary: Update subscription status
    *        summary: Update subscription status
    *        description: Update subscription status
    *        description: Update subscription status
-   *        operationId: updateSubscriptionStatus
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
    *            application/json:
    *            application/json:
    *              schema:
    *              schema:
    *                properties:
    *                properties:
    *                  pageId:
    *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Succeeded to update subscription status.
    *            description: Succeeded to update subscription status.

+ 5 - 42
apps/app/src/server/routes/apiv3/pages/index.js

@@ -29,35 +29,6 @@ const router = express.Router();
 const LIMIT_FOR_LIST = 10;
 const LIMIT_FOR_LIST = 10;
 const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
 const LIMIT_FOR_MULTIPLE_PAGE_OP = 20;
 
 
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      Tags:
- *        description: Tags
- *        type: array
- *        items:
- *          $ref: '#/components/schemas/Tag/properties/name'
- *        example: ['daily', 'report', 'tips']
- *
- *      Tag:
- *        description: Tag
- *        type: object
- *        properties:
- *          _id:
- *            type: string
- *            description: tag ID
- *            example: 5e2d6aede35da4004ef7e0b7
- *          name:
- *            type: string
- *            description: tag name
- *            example: daily
- *          count:
- *            type: number
- *            description: Count of tagged pages
- *            example: 3
- */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
   const loginRequired = require('../../../middlewares/login-required')(crowi, true);
@@ -227,7 +198,6 @@ module.exports = (crowi) => {
    *    /pages/rename:
    *    /pages/rename:
    *      post:
    *      post:
    *        tags: [Pages]
    *        tags: [Pages]
-   *        operationId: renamePage
    *        description: Rename page
    *        description: Rename page
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
@@ -235,9 +205,9 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                properties:
    *                properties:
    *                  pageId:
    *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                  path:
    *                  path:
-   *                    $ref: '#/components/schemas/Page/properties/path'
+   *                    $ref: '#/components/schemas/PagePath'
    *                  revisionId:
    *                  revisionId:
    *                    type: string
    *                    type: string
    *                    description: revision ID
    *                    description: revision ID
@@ -354,7 +324,6 @@ module.exports = (crowi) => {
     *    /pages/resume-rename:
     *    /pages/resume-rename:
     *      post:
     *      post:
     *        tags: [Pages]
     *        tags: [Pages]
-    *        operationId: resumeRenamePage
     *        description: Resume rename page operation
     *        description: Resume rename page operation
     *        requestBody:
     *        requestBody:
     *          content:
     *          content:
@@ -362,7 +331,7 @@ module.exports = (crowi) => {
     *              schema:
     *              schema:
     *                properties:
     *                properties:
     *                  pageId:
     *                  pageId:
-    *                    $ref: '#/components/schemas/Page/properties/_id'
+    *                    $ref: '#/components/schemas/ObjectId'
     *                required:
     *                required:
     *                  - pageId
     *                  - pageId
     *        responses:
     *        responses:
@@ -482,7 +451,6 @@ module.exports = (crowi) => {
     *    /pages/list:
     *    /pages/list:
     *      get:
     *      get:
     *        tags: [Pages]
     *        tags: [Pages]
-    *        operationId: getList
     *        description: Get list of pages
     *        description: Get list of pages
     *        parameters:
     *        parameters:
     *          - name: path
     *          - name: path
@@ -572,7 +540,6 @@ module.exports = (crowi) => {
    *    /pages/duplicate:
    *    /pages/duplicate:
    *      post:
    *      post:
    *        tags: [Pages]
    *        tags: [Pages]
-   *        operationId: duplicatePage
    *        description: Duplicate page
    *        description: Duplicate page
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
@@ -580,9 +547,9 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                properties:
    *                properties:
    *                  pageId:
    *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                  pageNameInput:
    *                  pageNameInput:
-   *                    $ref: '#/components/schemas/Page/properties/path'
+   *                    $ref: '#/components/schemas/PagePath'
    *                  isRecursively:
    *                  isRecursively:
    *                    type: boolean
    *                    type: boolean
    *                    description: whether duplicate page with descendants
    *                    description: whether duplicate page with descendants
@@ -678,7 +645,6 @@ module.exports = (crowi) => {
    *    /pages/subordinated-list:
    *    /pages/subordinated-list:
    *      get:
    *      get:
    *        tags: [Pages]
    *        tags: [Pages]
-   *        operationId: subordinatedList
    *        description: Get subordinated pages
    *        description: Get subordinated pages
    *        parameters:
    *        parameters:
    *          - name: path
    *          - name: path
@@ -724,7 +690,6 @@ module.exports = (crowi) => {
     *    /pages/delete:
     *    /pages/delete:
     *      post:
     *      post:
     *        tags: [Pages]
     *        tags: [Pages]
-    *        operationId: deletePages
     *        description: Delete pages
     *        description: Delete pages
     *        requestBody:
     *        requestBody:
     *          content:
     *          content:
@@ -823,7 +788,6 @@ module.exports = (crowi) => {
    *    /pages/convert-pages-by-path:
    *    /pages/convert-pages-by-path:
    *      post:
    *      post:
    *        tags: [Pages]
    *        tags: [Pages]
-   *        operationId: convertPagesByPath
    *        description: Convert pages by path
    *        description: Convert pages by path
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:
@@ -871,7 +835,6 @@ module.exports = (crowi) => {
    *    /pages/legacy-pages-migration:
    *    /pages/legacy-pages-migration:
    *      post:
    *      post:
    *        tags: [Pages]
    *        tags: [Pages]
-   *        operationId: legacyPagesMigration
    *        description: Migrate legacy pages
    *        description: Migrate legacy pages
    *        requestBody:
    *        requestBody:
    *          content:
    *          content:

+ 0 - 14
apps/app/src/server/routes/apiv3/personal-setting.js

@@ -134,7 +134,6 @@ module.exports = (crowi) => {
    *    /personal-setting:
    *    /personal-setting:
    *      get:
    *      get:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: getPersonalSetting
    *        summary: /personal-setting
    *        summary: /personal-setting
    *        description: Get personal parameters
    *        description: Get personal parameters
    *        responses:
    *        responses:
@@ -173,7 +172,6 @@ module.exports = (crowi) => {
    *    /personal-setting/is-password-set:
    *    /personal-setting/is-password-set:
    *      get:
    *      get:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: getIsPasswordSet
    *        summary: /personal-setting
    *        summary: /personal-setting
    *        description: Get whether a password has been set
    *        description: Get whether a password has been set
    *        responses:
    *        responses:
@@ -212,7 +210,6 @@ module.exports = (crowi) => {
    *    /personal-setting:
    *    /personal-setting:
    *      put:
    *      put:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: updatePersonalSetting
    *        summary: /personal-setting
    *        summary: /personal-setting
    *        description: Update personal setting
    *        description: Update personal setting
    *        requestBody:
    *        requestBody:
@@ -269,7 +266,6 @@ module.exports = (crowi) => {
    *    /personal-setting/image-type:
    *    /personal-setting/image-type:
    *      put:
    *      put:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: putUserImageType
    *        summary: /personal-setting/image-type
    *        summary: /personal-setting/image-type
    *        description: Update user image type
    *        description: Update user image type
    *        requestBody:
    *        requestBody:
@@ -315,7 +311,6 @@ module.exports = (crowi) => {
    *    /personal-setting/external-accounts:
    *    /personal-setting/external-accounts:
    *      get:
    *      get:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: getExternalAccounts
    *        summary: /personal-setting/external-accounts
    *        summary: /personal-setting/external-accounts
    *        description: Get external accounts that linked current user
    *        description: Get external accounts that linked current user
    *        responses:
    *        responses:
@@ -349,7 +344,6 @@ module.exports = (crowi) => {
    *    /personal-setting/password:
    *    /personal-setting/password:
    *      put:
    *      put:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: putUserPassword
    *        summary: /personal-setting/password
    *        summary: /personal-setting/password
    *        description: Update user password
    *        description: Update user password
    *        requestBody:
    *        requestBody:
@@ -404,7 +398,6 @@ module.exports = (crowi) => {
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: putUserApiToken
    *        summary: /personal-setting/api-token
    *        summary: /personal-setting/api-token
    *        description: Update user api token
    *        description: Update user api token
    *        responses:
    *        responses:
@@ -442,7 +435,6 @@ module.exports = (crowi) => {
    *    /personal-setting/associate-ldap:
    *    /personal-setting/associate-ldap:
    *      put:
    *      put:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: associateLdapAccount
    *        summary: /personal-setting/associate-ldap
    *        summary: /personal-setting/associate-ldap
    *        description: associate Ldap account
    *        description: associate Ldap account
    *        requestBody:
    *        requestBody:
@@ -497,7 +489,6 @@ module.exports = (crowi) => {
    *    /personal-setting/disassociate-ldap:
    *    /personal-setting/disassociate-ldap:
    *      put:
    *      put:
    *        tags: [GeneralSetting]
    *        tags: [GeneralSetting]
-   *        operationId: disassociateLdapAccount
    *        summary: /personal-setting/disassociate-ldap
    *        summary: /personal-setting/disassociate-ldap
    *        description: disassociate Ldap account
    *        description: disassociate Ldap account
    *        requestBody:
    *        requestBody:
@@ -552,7 +543,6 @@ module.exports = (crowi) => {
    *    /personal-setting/editor-settings:
    *    /personal-setting/editor-settings:
    *      put:
    *      put:
    *        tags: [EditorSetting]
    *        tags: [EditorSetting]
-   *        operationId: putEditorSettings
    *        summary: /personal-setting/editor-settings
    *        summary: /personal-setting/editor-settings
    *        description: Put editor preferences
    *        description: Put editor preferences
    *        requestBody:
    *        requestBody:
@@ -614,7 +604,6 @@ module.exports = (crowi) => {
    *    /personal-setting/editor-settings:
    *    /personal-setting/editor-settings:
    *      get:
    *      get:
    *        tags: [EditorSetting]
    *        tags: [EditorSetting]
-   *        operationId: getEditorSettings
    *        summary: /personal-setting/editor-settings
    *        summary: /personal-setting/editor-settings
    *        description: Get editor preferences
    *        description: Get editor preferences
    *        responses:
    *        responses:
@@ -644,7 +633,6 @@ module.exports = (crowi) => {
    *    /personal-setting/in-app-notification-settings:
    *    /personal-setting/in-app-notification-settings:
    *      put:
    *      put:
    *        tags: [InAppNotificationSettings]
    *        tags: [InAppNotificationSettings]
-   *        operationId: putInAppNotificationSettings
    *        summary: /personal-setting/in-app-notification-settings
    *        summary: /personal-setting/in-app-notification-settings
    *        description: Put InAppNotificationSettings
    *        description: Put InAppNotificationSettings
    *        requestBody:
    *        requestBody:
@@ -700,7 +688,6 @@ module.exports = (crowi) => {
    *    /personal-setting/in-app-notification-settings:
    *    /personal-setting/in-app-notification-settings:
    *      get:
    *      get:
    *        tags: [InAppNotificationSettings]
    *        tags: [InAppNotificationSettings]
-   *        operationId: getInAppNotificationSettings
    *        summary: personal-setting/in-app-notification-settings
    *        summary: personal-setting/in-app-notification-settings
    *        description: Get InAppNotificationSettings
    *        description: Get InAppNotificationSettings
    *        responses:
    *        responses:
@@ -731,7 +718,6 @@ module.exports = (crowi) => {
    *   /personal-setting/questionnaire-settings:
    *   /personal-setting/questionnaire-settings:
    *     put:
    *     put:
    *       tags: [QuestionnaireSetting]
    *       tags: [QuestionnaireSetting]
-   *       operationId: putQuestionnaireSetting
    *       summary: /personal-setting/questionnaire-settings
    *       summary: /personal-setting/questionnaire-settings
    *       description: Update the questionnaire settings for the current user
    *       description: Update the questionnaire settings for the current user
    *       requestBody:
    *       requestBody:

+ 10 - 10
apps/app/src/server/routes/apiv3/security-settings/index.js

@@ -1,4 +1,4 @@
-import { ConfigSource } from '@growi/core/dist/interfaces';
+import { ConfigSource, toNonBlankStringOrUndefined } from '@growi/core/dist/interfaces';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import xss from 'xss';
 import xss from 'xss';
 
 
@@ -407,11 +407,11 @@ module.exports = (crowi) => {
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
-  async function updateAndReloadStrategySettings(authId, params) {
+  async function updateAndReloadStrategySettings(authId, params, opts = { removeIfUndefined: false }) {
     const { passportService } = crowi;
     const { passportService } = crowi;
 
 
     // update config without publishing S2sMessage
     // update config without publishing S2sMessage
-    await configManager.updateConfigs(params, { skipPubsub: true });
+    await configManager.updateConfigs(params, { skipPubsub: true, removeIfUndefined: opts.removeIfUndefined });
 
 
     await passportService.setupStrategyById(authId);
     await passportService.setupStrategyById(authId);
     passportService.publishUpdatedMessage(authId);
     passportService.publishUpdatedMessage(authId);
@@ -1262,15 +1262,15 @@ module.exports = (crowi) => {
    *                      $ref: '#/components/schemas/GoogleOAuthSetting'
    *                      $ref: '#/components/schemas/GoogleOAuthSetting'
    */
    */
   router.put('/google-oauth', loginRequiredStrictly, adminRequired, addActivity, validator.googleOAuth, apiV3FormValidator, async(req, res) => {
   router.put('/google-oauth', loginRequiredStrictly, adminRequired, addActivity, validator.googleOAuth, apiV3FormValidator, async(req, res) => {
-    const requestParams = {
-      'security:passport-google:clientId': req.body.googleClientId,
-      'security:passport-google:clientSecret': req.body.googleClientSecret,
-      'security:passport-google:isSameEmailTreatedAsIdenticalUser': req.body.isSameEmailTreatedAsIdenticalUser,
-    };
-
 
 
     try {
     try {
-      await updateAndReloadStrategySettings('google', requestParams);
+      await updateAndReloadStrategySettings('google', {
+        'security:passport-google:isSameEmailTreatedAsIdenticalUser': req.body.isSameEmailTreatedAsIdenticalUser,
+      });
+      await updateAndReloadStrategySettings('google', {
+        'security:passport-google:clientId': toNonBlankStringOrUndefined(req.body.googleClientId),
+        'security:passport-google:clientSecret': toNonBlankStringOrUndefined(req.body.googleClientSecret),
+      }, { removeIfUndefined: true });
 
 
       const securitySettingParams = {
       const securitySettingParams = {
         googleClientId: await configManager.getConfig('security:passport-google:clientId'),
         googleClientId: await configManager.getConfig('security:passport-google:clientId'),

+ 0 - 13
apps/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -162,7 +162,6 @@ module.exports = (crowi) => {
    *    /slack-integration-settings/:
    *    /slack-integration-settings/:
    *      get:
    *      get:
    *        tags: [SlackIntegrationSettings]
    *        tags: [SlackIntegrationSettings]
-   *        operationId: getSlackBotSettingParams
    *        summary: /slack-integration-settings
    *        summary: /slack-integration-settings
    *        description: Get current settings and connection statuses.
    *        description: Get current settings and connection statuses.
    *        responses:
    *        responses:
@@ -321,7 +320,6 @@ module.exports = (crowi) => {
    *    /slack-integration-settings/bot-type/:
    *    /slack-integration-settings/bot-type/:
    *      put:
    *      put:
    *        tags: [SlackIntegrationSettings]
    *        tags: [SlackIntegrationSettings]
-   *        operationId: putBotType
    *        summary: /slack-integration/bot-type
    *        summary: /slack-integration/bot-type
    *        description: Put botType setting.
    *        description: Put botType setting.
    *        requestBody:
    *        requestBody:
@@ -360,7 +358,6 @@ module.exports = (crowi) => {
    *    /slack-integration/bot-type/:
    *    /slack-integration/bot-type/:
    *      delete:
    *      delete:
    *        tags: [SlackIntegrationSettings]
    *        tags: [SlackIntegrationSettings]
-   *        operationId: deleteBotType
    *        summary: /slack-integration/bot-type
    *        summary: /slack-integration/bot-type
    *        description: Delete botType setting.
    *        description: Delete botType setting.
    *        requestBody:
    *        requestBody:
@@ -393,7 +390,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (without proxy)]
    *        tags: [SlackIntegrationSettings (without proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: putWithoutProxySettings
    *        summary: /slack-integration-settings/without-proxy/update-settings
    *        summary: /slack-integration-settings/without-proxy/update-settings
    *        description: Update customBotWithoutProxy setting.
    *        description: Update customBotWithoutProxy setting.
    *        requestBody:
    *        requestBody:
@@ -446,7 +442,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (without proxy)]
    *        tags: [SlackIntegrationSettings (without proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: putWithoutProxyPermissions
    *        summary: /slack-integration-settings/without-proxy/update-permissions
    *        summary: /slack-integration-settings/without-proxy/update-permissions
    *        description: Update customBotWithoutProxy permissions.
    *        description: Update customBotWithoutProxy permissions.
    *        requestBody:
    *        requestBody:
@@ -502,7 +497,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: putSlackAppIntegrations
    *        summary: /slack-integration-settings/slack-app-integrations
    *        summary: /slack-integration-settings/slack-app-integrations
    *        description: Generate SlackAppIntegrations
    *        description: Generate SlackAppIntegrations
    *        responses:
    *        responses:
@@ -571,7 +565,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: deleteAccessTokens
    *        summary: /slack-integration-settings/slack-app-integrations/:id
    *        summary: /slack-integration-settings/slack-app-integrations/:id
    *        description: Delete accessTokens
    *        description: Delete accessTokens
    *        parameters:
    *        parameters:
@@ -622,7 +615,6 @@ module.exports = (crowi) => {
    *       tags: [SlackIntegrationSettings (with proxy)]
    *       tags: [SlackIntegrationSettings (with proxy)]
    *       security:
    *       security:
    *         - cookieAuth: []
    *         - cookieAuth: []
-   *       operationId: putProxyUri
    *       summary: /slack-integration-settings/proxy-uri
    *       summary: /slack-integration-settings/proxy-uri
    *       description: Update proxy uri
    *       description: Update proxy uri
    *       requestBody:
    *       requestBody:
@@ -670,7 +662,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: makePrimary
    *        summary: /slack-integration-settings/slack-app-integrations/:id/makeprimary
    *        summary: /slack-integration-settings/slack-app-integrations/:id/makeprimary
    *        description: Make SlackAppTokens primary
    *        description: Make SlackAppTokens primary
    *        parameters:
    *        parameters:
@@ -725,7 +716,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: putRegenerateTokens
    *        summary: /slack-integration-settings/slack-app-integrations/:id/regenerate-tokens
    *        summary: /slack-integration-settings/slack-app-integrations/:id/regenerate-tokens
    *        description: Regenerate SlackAppTokens
    *        description: Regenerate SlackAppTokens
    *        parameters:
    *        parameters:
@@ -770,7 +760,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: putSupportedCommands
    *        summary: /slack-integration-settings/slack-app-integrations/:id/permissions
    *        summary: /slack-integration-settings/slack-app-integrations/:id/permissions
    *        description: update supported commands
    *        description: update supported commands
    *        parameters:
    *        parameters:
@@ -853,7 +842,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        tags: [SlackIntegrationSettings (with proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: postRelationTest
    *        summary: /slack-integration-settings/slack-app-integrations/:id/relation-test
    *        summary: /slack-integration-settings/slack-app-integrations/:id/relation-test
    *        description: Delete botType setting.
    *        description: Delete botType setting.
    *        parameters:
    *        parameters:
@@ -940,7 +928,6 @@ module.exports = (crowi) => {
    *        tags: [SlackIntegrationSettings (without proxy)]
    *        tags: [SlackIntegrationSettings (without proxy)]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: postTest
    *        summary: /slack-integration-settings/without-proxy/test
    *        summary: /slack-integration-settings/without-proxy/test
    *        description: Test the connection with slack work space.
    *        description: Test the connection with slack work space.
    *        requestBody:
    *        requestBody:

+ 0 - 1
apps/app/src/server/routes/apiv3/statistics.js

@@ -121,7 +121,6 @@ module.exports = (crowi) => {
    *    get:
    *    get:
    *      tags: [Statistics]
    *      tags: [Statistics]
    *      security: []
    *      security: []
-   *      operationId: getStatisticsUser
    *      summary: /statistics/user
    *      summary: /statistics/user
    *      description: Get statistics for user
    *      description: Get statistics for user
    *      responses:
    *      responses:

+ 0 - 1
apps/app/src/server/routes/apiv3/user-activation.ts

@@ -78,7 +78,6 @@ async function sendEmailToAllAdmins(userData, admins, appTitle, mailService, tem
  *     summary: /complete-registration
  *     summary: /complete-registration
  *     tags: [Users]
  *     tags: [Users]
  *     security: []
  *     security: []
- *     operationId: completeRegistration
  *     requestBody:
  *     requestBody:
  *       required: true
  *       required: true
  *       content:
  *       content:

+ 0 - 1
apps/app/src/server/routes/apiv3/user-group-relation.js

@@ -31,7 +31,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroupRelations]
    *        tags: [UserGroupRelations]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: listUserGroupRelations
    *        summary: /user-group-relations
    *        summary: /user-group-relations
    *        description: Gets the user group relations
    *        description: Gets the user group relations
    *        responses:
    *        responses:

+ 0 - 15
apps/app/src/server/routes/apiv3/user-group.js

@@ -91,7 +91,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getUserGroup
    *        summary: /user-groups
    *        summary: /user-groups
    *        description: Get usergroups
    *        description: Get usergroups
    *        parameters:
    *        parameters:
@@ -167,7 +166,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getAncestorUserGroups
    *        summary: /user-groups/ancestors
    *        summary: /user-groups/ancestors
    *        description: Get ancestor user groups.
    *        description: Get ancestor user groups.
    *        parameters:
    *        parameters:
@@ -213,7 +211,6 @@ module.exports = (crowi) => {
    *          tags: [UserGroups]
    *          tags: [UserGroups]
    *          security:
    *          security:
    *            - cookieAuth: []
    *            - cookieAuth: []
-   *          operationId: getUserGroupChildren
    *          summary: /user-groups/children
    *          summary: /user-groups/children
    *          description: Get child user groups
    *          description: Get child user groups
    *          parameters:
    *          parameters:
@@ -276,7 +273,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: createUserGroup
    *        summary: /user-groups
    *        summary: /user-groups
    *        description: Adds userGroup
    *        description: Adds userGroup
    *        requestBody:
    *        requestBody:
@@ -334,7 +330,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getSelectableParentGroups
    *        summary: /selectable-parent-groups
    *        summary: /selectable-parent-groups
    *        description: Get selectable parent UserGroups
    *        description: Get selectable parent UserGroups
    *        parameters:
    *        parameters:
@@ -385,7 +380,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getSelectableChildGroups
    *        summary: /selectable-child-groups
    *        summary: /selectable-child-groups
    *        description: Get selectable child UserGroups
    *        description: Get selectable child UserGroups
    *        parameters:
    *        parameters:
@@ -439,7 +433,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getUserGroupFromGroupId
    *        summary: /user-groups/{id}
    *        summary: /user-groups/{id}
    *        description: Get UserGroup from Group ID
    *        description: Get UserGroup from Group ID
    *        parameters:
    *        parameters:
@@ -483,7 +476,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: deleteUserGroup
    *        summary: /user-groups/{id}
    *        summary: /user-groups/{id}
    *        description: Deletes userGroup
    *        description: Deletes userGroup
    *        parameters:
    *        parameters:
@@ -554,7 +546,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: updateUserGroups
    *        summary: /user-groups/{id}
    *        summary: /user-groups/{id}
    *        description: Update userGroup
    *        description: Update userGroup
    *        parameters:
    *        parameters:
@@ -624,7 +615,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getUsersUserGroups
    *        summary: /user-groups/{id}/users
    *        summary: /user-groups/{id}/users
    *        description: Get users related to the userGroup
    *        description: Get users related to the userGroup
    *        parameters:
    *        parameters:
@@ -677,7 +667,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getUnrelatedUsersUserGroups
    *        summary: /user-groups/{id}/unrelated-users
    *        summary: /user-groups/{id}/unrelated-users
    *        description: Get users unrelated to the userGroup
    *        description: Get users unrelated to the userGroup
    *        parameters:
    *        parameters:
@@ -760,7 +749,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: addUserUserGroups
    *        summary: /user-groups/{id}/users/{username}
    *        summary: /user-groups/{id}/users/{username}
    *        description: Add a user to the userGroup
    *        description: Add a user to the userGroup
    *        parameters:
    *        parameters:
@@ -832,7 +820,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: deleteUsersUserGroups
    *        summary: /user-groups/{id}/users/{username}
    *        summary: /user-groups/{id}/users/{username}
    *        description: remove a user from the userGroup
    *        description: remove a user from the userGroup
    *        parameters:
    *        parameters:
@@ -890,7 +877,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getUserGroupRelationsUserGroups
    *        summary: /user-groups/{id}/user-group-relations
    *        summary: /user-groups/{id}/user-group-relations
    *        description: Get the user group relations for the userGroup
    *        description: Get the user group relations for the userGroup
    *        parameters:
    *        parameters:
@@ -939,7 +925,6 @@ module.exports = (crowi) => {
    *        tags: [UserGroups]
    *        tags: [UserGroups]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: getPagesUserGroups
    *        summary: /user-groups/{id}/pages
    *        summary: /user-groups/{id}/pages
    *        description: Get closed pages for the userGroup
    *        description: Get closed pages for the userGroup
    *        parameters:
    *        parameters:

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

@@ -241,7 +241,6 @@ module.exports = (crowi) => {
    *    /users:
    *    /users:
    *      get:
    *      get:
    *        tags: [Users]
    *        tags: [Users]
-   *        operationId: listUsers
    *        summary: /users
    *        summary: /users
    *        description: Select selected columns from users order by asc or desc
    *        description: Select selected columns from users order by asc or desc
    *        parameters:
    *        parameters:
@@ -377,7 +376,6 @@ module.exports = (crowi) => {
    *    /{id}/recent:
    *    /{id}/recent:
    *      get:
    *      get:
    *        tags: [Users]
    *        tags: [Users]
-   *        operationId: recent created page of user id
    *        summary: /usersIdReacent
    *        summary: /usersIdReacent
    *        parameters:
    *        parameters:
    *          - name: id
    *          - name: id
@@ -453,7 +451,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: inviteUser
    *        summary: /users/invite
    *        summary: /users/invite
    *        description: Create new users and send Emails
    *        description: Create new users and send Emails
    *        parameters:
    *        parameters:
@@ -532,7 +529,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: grantAdminUser
    *        summary: /users/{id}/grant-admin
    *        summary: /users/{id}/grant-admin
    *        description: Grant user admin
    *        description: Grant user admin
    *        parameters:
    *        parameters:
@@ -581,7 +577,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: revokeAdminUser
    *        summary: /users/{id}/revoke-admin
    *        summary: /users/{id}/revoke-admin
    *        description: Revoke user admin
    *        description: Revoke user admin
    *        parameters:
    *        parameters:
@@ -630,7 +625,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: ReadOnly
    *        summary: /users/{id}/grant-read-only
    *        summary: /users/{id}/grant-read-only
    *        description: Grant user read only access
    *        description: Grant user read only access
    *        parameters:
    *        parameters:
@@ -684,7 +678,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: revokeReadOnly
    *        summary: /users/{id}/revoke-read-only
    *        summary: /users/{id}/revoke-read-only
    *        description: Revoke user read only access
    *        description: Revoke user read only access
    *        parameters:
    *        parameters:
@@ -738,7 +731,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: activateUser
    *        summary: /users/{id}/activate
    *        summary: /users/{id}/activate
    *        description: Activate user
    *        description: Activate user
    *        parameters:
    *        parameters:
@@ -794,7 +786,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: deactivateUser
    *        summary: /users/{id}/deactivate
    *        summary: /users/{id}/deactivate
    *        description: Deactivate user
    *        description: Deactivate user
    *        parameters:
    *        parameters:
@@ -843,7 +834,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: removeUser
    *        summary: /users/{id}/remove
    *        summary: /users/{id}/remove
    *        description: Delete user
    *        description: Delete user
    *        parameters:
    *        parameters:
@@ -907,7 +897,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: listExternalAccountsUsers
    *        summary: /users/external-accounts
    *        summary: /users/external-accounts
    *        description: Get external-account
    *        description: Get external-account
    *        parameters:
    *        parameters:
@@ -948,7 +937,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: removeExternalAccountUser
    *        summary: /users/external-accounts/{id}/remove
    *        summary: /users/external-accounts/{id}/remove
    *        description: Delete ExternalAccount
    *        description: Delete ExternalAccount
    *        parameters:
    *        parameters:
@@ -993,7 +981,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: update.imageUrlCache
    *        summary: /users/update.imageUrlCache
    *        summary: /users/update.imageUrlCache
    *        description: update imageUrlCache
    *        description: update imageUrlCache
    *        requestBody:
    *        requestBody:
@@ -1049,7 +1036,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: resetPassword
    *        summary: /users/reset-password
    *        summary: /users/reset-password
    *        description: update imageUrlCache
    *        description: update imageUrlCache
    *        requestBody:
    *        requestBody:
@@ -1099,7 +1085,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: resetPasswordEmail
    *        summary: /users/reset-password-email
    *        summary: /users/reset-password-email
    *        description: send new password email
    *        description: send new password email
    *        requestBody:
    *        requestBody:
@@ -1148,7 +1133,6 @@ module.exports = (crowi) => {
    *        tags: [Users Management]
    *        tags: [Users Management]
    *        security:
    *        security:
    *          - cookieAuth: []
    *          - cookieAuth: []
-   *        operationId: sendInvitationEmail
    *        summary: /users/send-invitation-email
    *        summary: /users/send-invitation-email
    *        description: send invitation email
    *        description: send invitation email
    *        requestBody:
    *        requestBody:
@@ -1207,7 +1191,6 @@ module.exports = (crowi) => {
    *        get:
    *        get:
    *          tags: [Users]
    *          tags: [Users]
    *          summary: /users/list
    *          summary: /users/list
-   *          operationId: getUsersList
    *          description: Get list of users
    *          description: Get list of users
    *          parameters:
    *          parameters:
    *            - in: query
    *            - in: query
@@ -1229,9 +1212,9 @@ module.exports = (crowi) => {
    *                          $ref: '#/components/schemas/User'
    *                          $ref: '#/components/schemas/User'
    *                        description: user list
    *                        description: user list
    *            403:
    *            403:
-   *              $ref: '#/components/responses/403'
+   *              $ref: '#/components/responses/Forbidden'
    *            500:
    *            500:
-   *              $ref: '#/components/responses/500'
+   *              $ref: '#/components/responses/InternalServerError'
    */
    */
   router.get('/list', accessTokenParser, loginRequired, async(req, res) => {
   router.get('/list', accessTokenParser, loginRequired, async(req, res) => {
     const userIds = req.query.userIds ?? null;
     const userIds = req.query.userIds ?? null;
@@ -1270,7 +1253,6 @@ module.exports = (crowi) => {
     *        get:
     *        get:
     *          tags: [Users]
     *          tags: [Users]
     *          summary: /users/usernames
     *          summary: /users/usernames
-    *          operationId: getUsernames
     *          description: Get list of usernames
     *          description: Get list of usernames
     *          parameters:
     *          parameters:
     *            - in: query
     *            - in: query

+ 17 - 20
apps/app/src/server/routes/attachment/api.js

@@ -1,4 +1,3 @@
-
 import { SupportedAction } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -110,7 +109,7 @@ const ApiResponse = require('../../util/apiResponse');
  *            description: original file name
  *            description: original file name
  *            example: profile.png
  *            example: profile.png
  *          creator:
  *          creator:
- *            $ref: '#/components/schemas/User/properties/_id'
+ *            $ref: '#/components/schemas/ObjectId'
  *          page:
  *          page:
  *            type: string
  *            type: string
  *            description: page ID attached at
  *            description: page ID attached at
@@ -220,15 +219,17 @@ export const routesFactory = (crowi) => {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    attachment:
-   *                      $ref: '#/components/schemas/AttachmentProfile'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        attachment:
+   *                          $ref: '#/components/schemas/AttachmentProfile'
+   *                          description: The uploaded profile image attachment
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {post} /attachments.uploadProfileImage Add attachment for profile image
    * @api {post} /attachments.uploadProfileImage Add attachment for profile image
@@ -289,7 +290,7 @@ export const routesFactory = (crowi) => {
    *              schema:
    *              schema:
    *                properties:
    *                properties:
    *                  attachment_id:
    *                  attachment_id:
-   *                    $ref: '#/components/schemas/Attachment/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                required:
    *                required:
    *                  - attachment_id
    *                  - attachment_id
    *        responses:
    *        responses:
@@ -298,13 +299,11 @@ export const routesFactory = (crowi) => {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
+   *                  $ref: '#/components/schemas/ApiResponseSuccess'
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {post} /attachments.remove Remove attachments
    * @api {post} /attachments.remove Remove attachments
@@ -363,13 +362,11 @@ export const routesFactory = (crowi) => {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
+   *                  $ref: '#/components/schemas/ApiResponseSuccess'
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {post} /attachments.removeProfileImage Remove profile image attachments
    * @api {post} /attachments.removeProfileImage Remove profile image attachments

+ 57 - 54
apps/app/src/server/routes/comment.js

@@ -1,4 +1,3 @@
-
 import { getIdStringForRef } from '@growi/core';
 import { getIdStringForRef } from '@growi/core';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 
@@ -21,32 +20,34 @@ import { preNotifyService } from '../service/pre-notify';
  *
  *
  *  components:
  *  components:
  *    schemas:
  *    schemas:
+ *      CommentBody:
+ *        description: The type for Comment.comment
+ *        type: string
+ *        example: good
+ *      CommentPosition:
+ *        description: comment position
+ *        type: number
+ *        example: 0
  *      Comment:
  *      Comment:
  *        description: Comment
  *        description: Comment
  *        type: object
  *        type: object
  *        properties:
  *        properties:
  *          _id:
  *          _id:
- *            type: string
- *            description: revision ID
- *            example: 5e079a0a0afa6700170a75fb
+ *            $ref: '#/components/schemas/ObjectId'
  *          __v:
  *          __v:
  *            type: number
  *            type: number
  *            description: DB record version
  *            description: DB record version
  *            example: 0
  *            example: 0
  *          page:
  *          page:
- *            $ref: '#/components/schemas/Page/properties/_id'
+ *            $ref: '#/components/schemas/ObjectId'
  *          creator:
  *          creator:
- *            $ref: '#/components/schemas/User/properties/_id'
+ *            $ref: '#/components/schemas/ObjectId'
  *          revision:
  *          revision:
- *            $ref: '#/components/schemas/Revision/properties/_id'
+ *            $ref: '#/components/schemas/ObjectId'
  *          comment:
  *          comment:
- *            type: string
- *            description: comment
- *            example: good
+ *            $ref: '#/components/schemas/CommentBody'
  *          commentPosition:
  *          commentPosition:
- *            type: number
- *            description: comment position
- *            example: 0
+ *            $ref: '#/components/schemas/CommentPosition'
  *          createdAt:
  *          createdAt:
  *            type: string
  *            type: string
  *            description: date created at
  *            description: date created at
@@ -88,28 +89,30 @@ module.exports = function(crowi, app) {
    *          - in: query
    *          - in: query
    *            name: page_id
    *            name: page_id
    *            schema:
    *            schema:
-   *              $ref: '#/components/schemas/Page/properties/_id'
+   *              $ref: '#/components/schemas/ObjectId'
    *          - in: query
    *          - in: query
    *            name: revision_id
    *            name: revision_id
    *            schema:
    *            schema:
-   *              $ref: '#/components/schemas/Revision/properties/_id'
+   *              $ref: '#/components/schemas/ObjectId'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Succeeded to get comments of the page of the revision.
    *            description: Succeeded to get comments of the page of the revision.
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    comments:
-   *                      type: array
-   *                      items:
-   *                        $ref: '#/components/schemas/Comment'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        comments:
+   *                          type: array
+   *                          items:
+   *                            $ref: '#/components/schemas/Comment'
+   *                          description: List of comments for the page revision
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {get} /comments.get Get comments of the page of the revision
    * @api {get} /comments.get Get comments of the page of the revision
@@ -190,13 +193,13 @@ module.exports = function(crowi, app) {
    *                    type: object
    *                    type: object
    *                    properties:
    *                    properties:
    *                      page_id:
    *                      page_id:
-   *                        $ref: '#/components/schemas/Page/properties/_id'
+   *                        $ref: '#/components/schemas/ObjectId'
    *                      revision_id:
    *                      revision_id:
-   *                        $ref: '#/components/schemas/Revision/properties/_id'
+   *                        $ref: '#/components/schemas/ObjectId'
    *                      comment:
    *                      comment:
-   *                        $ref: '#/components/schemas/Comment/properties/comment'
+   *                        $ref: '#/components/schemas/CommentBody'
    *                      comment_position:
    *                      comment_position:
-   *                        $ref: '#/components/schemas/Comment/properties/commentPosition'
+   *                        $ref: '#/components/schemas/CommentPosition'
    *                required:
    *                required:
    *                  - commentForm
    *                  - commentForm
    *        responses:
    *        responses:
@@ -205,15 +208,17 @@ module.exports = function(crowi, app) {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    comment:
-   *                      $ref: '#/components/schemas/Comment'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        comment:
+   *                          $ref: '#/components/schemas/Comment'
+   *                          description: The newly created comment
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {post} /comments.add Post comment for the page
    * @api {post} /comments.add Post comment for the page
@@ -336,13 +341,13 @@ module.exports = function(crowi, app) {
    *                        type: object
    *                        type: object
    *                        properties:
    *                        properties:
    *                          page_id:
    *                          page_id:
-   *                            $ref: '#/components/schemas/Page/properties/_id'
+   *                            $ref: '#/components/schemas/ObjectId'
    *                          revision_id:
    *                          revision_id:
-   *                            $ref: '#/components/schemas/Revision/properties/_id'
+   *                            $ref: '#/components/schemas/ObjectId'
    *                          comment_id:
    *                          comment_id:
-   *                            $ref: '#/components/schemas/Comment/properties/_id'
+   *                            $ref: '#/components/schemas/ObjectId'
    *                          comment:
    *                          comment:
-   *                            $ref: '#/components/schemas/Comment/properties/comment'
+   *                            $ref: '#/components/schemas/CommentBody'
    *                required:
    *                required:
    *                  - form
    *                  - form
    *        responses:
    *        responses:
@@ -351,15 +356,17 @@ module.exports = function(crowi, app) {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    comment:
-   *                      $ref: '#/components/schemas/Comment'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        comment:
+   *                          $ref: '#/components/schemas/Comment'
+   *                          description: The updated comment
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {post} /comments.update Update comment dody
    * @api {post} /comments.update Update comment dody
@@ -433,7 +440,7 @@ module.exports = function(crowi, app) {
    *              schema:
    *              schema:
    *                properties:
    *                properties:
    *                  comment_id:
    *                  comment_id:
-   *                    $ref: '#/components/schemas/Comment/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                required:
    *                required:
    *                  - comment_id
    *                  - comment_id
    *        responses:
    *        responses:
@@ -442,15 +449,11 @@ module.exports = function(crowi, app) {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    comment:
-   *                      $ref: '#/components/schemas/Comment'
+   *                  $ref: '#/components/schemas/ApiResponseSuccess'
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {post} /comments.remove Remove specified comment
    * @api {post} /comments.remove Remove specified comment

+ 331 - 117
apps/app/src/server/routes/page.js

@@ -14,54 +14,6 @@ import UpdatePost from '../models/update-post';
  *    name: Pages
  *    name: Pages
  */
  */
 
 
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *
- *      UpdatePost:
- *        description: UpdatePost
- *        type: object
- *        properties:
- *          _id:
- *            type: string
- *            description: update post ID
- *            example: 5e0734e472560e001761fa68
- *          __v:
- *            type: number
- *            description: DB record version
- *            example: 0
- *          pathPattern:
- *            type: string
- *            description: path pattern
- *            example: /test
- *          patternPrefix:
- *            type: string
- *            description: patternPrefix prefix
- *            example: /
- *          patternPrefix2:
- *            type: string
- *            description: path
- *            example: test
- *          channel:
- *            type: string
- *            description: channel
- *            example: general
- *          provider:
- *            type: string
- *            description: provider
- *            enum:
- *              - slack
- *            example: slack
- *          creator:
- *            $ref: '#/components/schemas/User'
- *          createdAt:
- *            type: string
- *            description: date created at
- *            example: 2010-01-01T00:00:00.000Z
- */
-
 /* eslint-disable no-use-before-define */
 /* eslint-disable no-use-before-define */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
@@ -134,37 +86,57 @@ module.exports = function(crowi, app) {
   const validator = {};
   const validator = {};
 
 
   actions.api = api;
   actions.api = api;
-  actions.validator = validator;
-
-  /**
+  actions.validator = validator; /**
    * @swagger
    * @swagger
    *
    *
-   *    /pages.getPageTag:
-   *      get:
-   *        tags: [Pages]
-   *        operationId: getPageTag
-   *        summary: /pages.getPageTag
-   *        description: Get page tag
-   *        parameters:
-   *          - in: query
-   *            name: pageId
-   *            schema:
-   *              $ref: '#/components/schemas/Page/properties/_id'
-   *        responses:
-   *          200:
-   *            description: Succeeded to get page tags.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    tags:
-   *                      $ref: '#/components/schemas/Tags'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
+   * components:
+   *   schemas:
+   *     PageTagsData:
+   *       type: object
+   *       properties:
+   *         tags:
+   *           type: array
+   *           items:
+   *             type: string
+   *           description: Array of tag names associated with the page
+   *           example: ["javascript", "tutorial", "backend"]
+   *
+   *   responses:
+   *     PageTagsSuccess:
+   *       description: Successfully retrieved page tags
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageTagsData'
+   *
+   * /pages.getPageTag:
+   *   get:
+   *     tags: [Pages]
+   *     operationId: getPageTag
+   *     summary: Get page tags
+   *     description: Retrieve all tags associated with a specific page
+   *     parameters:
+   *       - in: query
+   *         name: pageId
+   *         required: true
+   *         description: Unique identifier of the page
+   *         schema:
+   *           type: string
+   *           format: ObjectId
+   *           example: "507f1f77bcf86cd799439011"
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageTagsSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       404:
+   *         $ref: '#/components/responses/NotFound'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {get} /pages.getPageTag get page tags
    * @api {get} /pages.getPageTag get page tags
@@ -187,32 +159,58 @@ module.exports = function(crowi, app) {
   /**
   /**
    * @swagger
    * @swagger
    *
    *
-   *    /pages.updatePost:
-   *      get:
-   *        tags: [Pages]
-   *        operationId: getUpdatePostPage
-   *        summary: /pages.updatePost
-   *        description: Get UpdatePost setting list
-   *        parameters:
-   *          - in: query
-   *            name: path
-   *            schema:
-   *              $ref: '#/components/schemas/Page/properties/path'
-   *        responses:
-   *          200:
-   *            description: Succeeded to get UpdatePost setting list.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    updatePost:
-   *                      $ref: '#/components/schemas/UpdatePost'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
+   * components:
+   *   schemas:
+   *     UpdatePostData:
+   *       type: object
+   *       properties:
+   *         updatePost:
+   *           type: array
+   *           items:
+   *             type: string
+   *           description: Array of channel names for notifications
+   *           example: ["general", "development", "notifications"]
+   *
+   *   responses:
+   *     UpdatePostSuccess:
+   *       description: Successfully retrieved UpdatePost settings
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/UpdatePostData'
+   *
+   * /pages.updatePost:
+   *   get:
+   *     tags: [Pages]
+   *     operationId: getUpdatePost
+   *     summary: Get UpdatePost settings
+   *     description: Retrieve UpdatePost notification settings for a specific path
+   *     parameters:
+   *       - in: query
+   *         name: path
+   *         required: true
+   *         description: Page path to get UpdatePost settings for
+   *         schema:
+   *           type: string
+   *           example: "/user/example"
+   *         examples:
+   *           userPage:
+   *             value: "/user/john"
+   *             description: User page path
+   *           projectPage:
+   *             value: "/project/myproject"
+   *             description: Project page path
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/UpdatePostSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {get} /pages.updatePost
    * @api {get} /pages.updatePost
@@ -254,12 +252,101 @@ module.exports = function(crowi, app) {
   ];
   ];
 
 
   /**
   /**
-   * @api {post} /pages.remove Remove page
-   * @apiName RemovePage
-   * @apiGroup Page
+   * @swagger
+   *
+   * components:
+   *   schemas:
+   *     PageRemoveData:
+   *       type: object
+   *       properties:
+   *         path:
+   *           type: string
+   *           required: true
+   *           description: Path of the deleted page
+   *           example: "/user/example"
+   *         isRecursively:
+   *           type: boolean
+   *           description: Whether deletion was recursive
+   *           example: true
+   *         isCompletely:
+   *           type: boolean
+   *           description: Whether deletion was complete
+   *           example: false
+   *
+   *   responses:
+   *     PageRemoveSuccess:
+   *       description: Page successfully deleted
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageRemoveData'
    *
    *
-   * @apiParam {String} page_id Page Id.
-   * @apiParam {String} revision_id
+   * /pages.remove:
+   *   post:
+   *     tags: [Pages]
+   *     operationId: removePage
+   *     summary: Remove page
+   *     description: Delete a page either softly or completely, with optional recursive deletion
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             required:
+   *               - page_id
+   *             properties:
+   *               page_id:
+   *                 type: string
+   *                 format: ObjectId
+   *                 description: Unique identifier of the page to delete
+   *                 example: "507f1f77bcf86cd799439011"
+   *               revision_id:
+   *                 type: string
+   *                 format: ObjectId
+   *                 description: Revision ID for conflict detection
+   *                 example: "507f1f77bcf86cd799439012"
+   *               completely:
+   *                 type: boolean
+   *                 description: Whether to delete the page completely (true) or soft delete (false)
+   *                 default: false
+   *                 example: false
+   *               recursively:
+   *                 type: boolean
+   *                 description: Whether to delete child pages recursively
+   *                 default: false
+   *                 example: true
+   *           examples:
+   *             softDelete:
+   *               summary: Soft delete single page
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 revision_id: "507f1f77bcf86cd799439012"
+   *             recursiveDelete:
+   *               summary: Recursive soft delete
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 recursively: true
+   *             completeDelete:
+   *               summary: Complete deletion
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 completely: true
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageRemoveSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       404:
+   *         $ref: '#/components/responses/NotFound'
+   *       409:
+   *         $ref: '#/components/responses/Conflict'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
    */
   api.remove = async function(req, res) {
   api.remove = async function(req, res) {
     const pageId = req.body.page_id;
     const pageId = req.body.page_id;
@@ -365,11 +452,89 @@ module.exports = function(crowi, app) {
   ];
   ];
 
 
   /**
   /**
-   * @api {post} /pages.revertRemove Revert removed page
-   * @apiName RevertRemovePage
-   * @apiGroup Page
+   * @swagger
+   *
+   * components:
+   *   schemas:
+   *     PageRevertData:
+   *       type: object
+   *       properties:
+   *         page:
+   *           type: object
+   *           description: Restored page object
+   *           properties:
+   *             _id:
+   *               type: string
+   *               format: ObjectId
+   *               example: "507f1f77bcf86cd799439011"
+   *             path:
+   *               type: string
+   *               example: "/user/example"
+   *             title:
+   *               type: string
+   *               example: "Example Page"
+   *             status:
+   *               type: string
+   *               example: "published"
    *
    *
-   * @apiParam {String} page_id Page Id.
+   *   responses:
+   *     PageRevertSuccess:
+   *       description: Page successfully restored
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageRevertData'
+   *
+   * /pages.revertRemove:
+   *   post:
+   *     tags: [Pages]
+   *     operationId: revertRemovePage
+   *     summary: Revert removed page
+   *     description: Restore a previously deleted (soft-deleted) page
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             required:
+   *               - page_id
+   *             properties:
+   *               page_id:
+   *                 type: string
+   *                 format: ObjectId
+   *                 description: Unique identifier of the page to restore
+   *                 example: "507f1f77bcf86cd799439011"
+   *               recursively:
+   *                 type: boolean
+   *                 description: Whether to restore child pages recursively
+   *                 default: false
+   *                 example: true
+   *           examples:
+   *             singleRevert:
+   *               summary: Revert single page
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *             recursiveRevert:
+   *               summary: Revert page and children
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 recursively: true
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageRevertSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       404:
+   *         $ref: '#/components/responses/NotFound'
+   *       409:
+   *         $ref: '#/components/responses/Conflict'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
    */
   api.revertRemove = async function(req, res, options) {
   api.revertRemove = async function(req, res, options) {
     const pageId = req.body.page_id;
     const pageId = req.body.page_id;
@@ -406,12 +571,61 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   /**
   /**
-   * @api {post} /pages.unlink Remove the redirecting page
-   * @apiName UnlinkPage
-   * @apiGroup Page
+   * @swagger
+   *
+   * components:
+   *   schemas:
+   *     PageUnlinkData:
+   *       type: object
+   *       properties:
+   *         path:
+   *           type: string
+   *           description: Path for which redirects were removed
+   *           example: "/user/example"
+   *
+   *   responses:
+   *     PageUnlinkSuccess:
+   *       description: Successfully removed page redirects
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageUnlinkData'
    *
    *
-   * @apiParam {String} page_id Page Id.
-   * @apiParam {String} revision_id
+   * /pages.unlink:
+   *   post:
+   *     tags: [Pages]
+   *     operationId: unlinkPage
+   *     summary: Remove page redirects
+   *     description: Remove all redirect entries that point to the specified page path
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             required:
+   *               - path
+   *             properties:
+   *               path:
+   *                 type: string
+   *                 description: Target path to remove redirects for
+   *                 example: "/user/example"
+   *           examples:
+   *             unlinkPage:
+   *               summary: Remove redirects to a page
+   *               value:
+   *                 path: "/user/example"
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageUnlinkSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
    */
   api.unlink = async function(req, res) {
   api.unlink = async function(req, res) {
     const path = req.body.path;
     const path = req.body.path;

+ 38 - 34
apps/app/src/server/routes/search.ts

@@ -14,26 +14,28 @@ const logger = loggerFactory('growi:routes:search');
  *
  *
  *   components:
  *   components:
  *     schemas:
  *     schemas:
+ *       ElasticsearchResultMeta:
+ *         type: object
+ *         properties:
+ *           took:
+ *             type: number
+ *             description: Time Elasticsearch took to execute a search(milliseconds)
+ *             example: 34
+ *           total:
+ *             type: number
+ *             description: Number of documents matching search criteria
+ *             example: 2
+ *           results:
+ *             type: number
+ *             description: Actual array length of search results
+ *             example: 2
+ *
  *       ElasticsearchResult:
  *       ElasticsearchResult:
  *         description: Elasticsearch result v1
  *         description: Elasticsearch result v1
  *         type: object
  *         type: object
  *         properties:
  *         properties:
  *           meta:
  *           meta:
- *             type: object
- *             properties:
- *               took:
- *                 type: number
- *                 description: Time Elasticsearch took to execute a search(milliseconds)
- *                 example: 34
- *               total:
- *                 type: number
- *                 description: Number of documents matching search criteria
- *                 example: 2
- *               results:
- *                 type: number
- *                 description: Actual array length of search results
- *                 example: 2
- *
+ *             $ref: '#/components/schemas/ElasticsearchResultMeta'
  */
  */
 module.exports = function(crowi: Crowi, app) {
 module.exports = function(crowi: Crowi, app) {
   const ApiResponse = require('../util/apiResponse');
   const ApiResponse = require('../util/apiResponse');
@@ -62,39 +64,41 @@ module.exports = function(crowi: Crowi, app) {
    *         - in: query
    *         - in: query
    *           name: path
    *           name: path
    *           schema:
    *           schema:
-   *             $ref: '#/components/schemas/Page/properties/path'
+   *             $ref: '#/components/schemas/PagePath'
    *         - in: query
    *         - in: query
    *           name: offset
    *           name: offset
    *           schema:
    *           schema:
-   *             $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/offset'
+   *             $ref: '#/components/schemas/Offset'
    *         - in: query
    *         - in: query
    *           name: limit
    *           name: limit
    *           schema:
    *           schema:
-   *             $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/limit'
+   *             $ref: '#/components/schemas/Limit'
    *       responses:
    *       responses:
    *         200:
    *         200:
    *           description: Succeeded to get list of pages.
    *           description: Succeeded to get list of pages.
    *           content:
    *           content:
    *             application/json:
    *             application/json:
    *               schema:
    *               schema:
-   *                 properties:
-   *                   ok:
-   *                     $ref: '#/components/schemas/V1Response/properties/ok'
-   *                   meta:
-   *                     $ref: '#/components/schemas/ElasticsearchResult/properties/meta'
-   *                   totalCount:
-   *                     type: integer
-   *                     description: total count of pages
-   *                     example: 35
-   *                   data:
-   *                     type: array
-   *                     items:
-   *                       $ref: '#/components/schemas/Page'
-   *                     description: page list
+   *                 allOf:
+   *                   - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                   - type: object
+   *                     properties:
+   *                       meta:
+   *                         $ref: '#/components/schemas/ElasticsearchResultMeta'
+   *                         description: Elasticsearch metadata
+   *                       totalCount:
+   *                         type: integer
+   *                         description: total count of pages
+   *                         example: 35
+   *                       data:
+   *                         type: array
+   *                         items:
+   *                           $ref: '#/components/schemas/Page'
+   *                         description: page list
    *         403:
    *         403:
-   *           $ref: '#/components/responses/403'
+   *           $ref: '#/components/responses/Forbidden'
    *         500:
    *         500:
-   *           $ref: '#/components/responses/500'
+   *           $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {get} /search search page
    * @api {get} /search search page

+ 33 - 56
apps/app/src/server/routes/tag.js

@@ -5,35 +5,6 @@ import PageTagRelation from '../models/page-tag-relation';
 import { Revision } from '../models/revision';
 import { Revision } from '../models/revision';
 import ApiResponse from '../util/apiResponse';
 import ApiResponse from '../util/apiResponse';
 
 
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      Tags:
- *        description: Tags
- *        type: array
- *        items:
- *          $ref: '#/components/schemas/Tag/properties/name'
- *        example: ['daily', 'report', 'tips']
- *
- *      Tag:
- *        description: Tag
- *        type: object
- *        properties:
- *          _id:
- *            type: string
- *            description: tag ID
- *            example: 5e2d6aede35da4004ef7e0b7
- *          name:
- *            type: string
- *            description: tag name
- *            example: daily
- *          count:
- *            type: number
- *            description: Count of tagged pages
- *            example: 3
- */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
 
 
@@ -66,15 +37,17 @@ module.exports = function(crowi, app) {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    tags:
-   *                      $ref: '#/components/schemas/Tags'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        tags:
+   *                          $ref: '#/components/schemas/Tags'
+   *                          description: List of matching tags
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {get} /tags.search search tags
    * @api {get} /tags.search search tags
@@ -109,9 +82,9 @@ module.exports = function(crowi, app) {
    *              schema:
    *              schema:
    *                properties:
    *                properties:
    *                  pageId:
    *                  pageId:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                  revisionId:
    *                  revisionId:
-   *                    $ref: '#/components/schemas/Revision/properties/_id'
+   *                    $ref: '#/components/schemas/ObjectId'
    *                  tags:
    *                  tags:
    *                    $ref: '#/components/schemas/Tags'
    *                    $ref: '#/components/schemas/Tags'
    *        responses:
    *        responses:
@@ -120,15 +93,17 @@ module.exports = function(crowi, app) {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    tags:
-   *                      $ref: '#/components/schemas/Tags'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        tags:
+   *                          $ref: '#/components/schemas/Tags'
+   *                          description: Updated tags for the page
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {post} /tags.update update tags on view-mode (not edit-mode)
    * @api {post} /tags.update update tags on view-mode (not edit-mode)
@@ -186,28 +161,30 @@ module.exports = function(crowi, app) {
    *          - in: query
    *          - in: query
    *            name: limit
    *            name: limit
    *            schema:
    *            schema:
-   *              $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/limit'
+   *              $ref: '#/components/schemas/Limit'
    *          - in: query
    *          - in: query
    *            name: offset
    *            name: offset
    *            schema:
    *            schema:
-   *              $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/offset'
+   *              $ref: '#/components/schemas/Offset'
    *        responses:
    *        responses:
    *          200:
    *          200:
    *            description: Succeeded to tag list.
    *            description: Succeeded to tag list.
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    data:
-   *                      type: array
-   *                      items:
-   *                        $ref: '#/components/schemas/Tag'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        data:
+   *                          type: array
+   *                          items:
+   *                            $ref: '#/components/schemas/Tag'
+   *                          description: List of tags with count information
    *          403:
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
    */
   /**
   /**
    * @api {get} /tags.list get tagnames and count pages relate each tag
    * @api {get} /tags.list get tagnames and count pages relate each tag

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

@@ -1,6 +1,9 @@
 import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
 import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
-import type { ConfigDefinition, Lang } from '@growi/core/dist/interfaces';
-import { defineConfig } from '@growi/core/dist/interfaces';
+import type { ConfigDefinition, Lang, NonBlankString } from '@growi/core/dist/interfaces';
+import {
+  toNonBlankString,
+  defineConfig,
+} from '@growi/core/dist/interfaces';
 import type OpenAI from 'openai';
 import type OpenAI from 'openai';
 
 
 import { ActionGroupSize } from '~/interfaces/activity';
 import { ActionGroupSize } from '~/interfaces/activity';
@@ -726,10 +729,10 @@ export const CONFIG_DEFINITIONS = {
   'security:passport-google:isEnabled': defineConfig<boolean>({
   'security:passport-google:isEnabled': defineConfig<boolean>({
     defaultValue: false,
     defaultValue: false,
   }),
   }),
-  'security:passport-google:clientId': defineConfig<string | undefined>({
+  'security:passport-google:clientId': defineConfig<NonBlankString | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
-  'security:passport-google:clientSecret': defineConfig<string | undefined>({
+  'security:passport-google:clientSecret': defineConfig<NonBlankString | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
   'security:passport-google:isSameUsernameTreatedAsIdenticalUser': defineConfig<boolean>({
   'security:passport-google:isSameUsernameTreatedAsIdenticalUser': defineConfig<boolean>({
@@ -819,28 +822,29 @@ export const CONFIG_DEFINITIONS = {
     envVarName: 'S3_OBJECT_ACL',
     envVarName: 'S3_OBJECT_ACL',
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
-  'aws:s3Bucket': defineConfig<string>({
-    defaultValue: 'growi',
+  'aws:s3Bucket': defineConfig<NonBlankString>({
+    defaultValue: toNonBlankString('growi'),
   }),
   }),
-  'aws:s3Region': defineConfig<string>({
-    defaultValue: 'ap-northeast-1',
+  'aws:s3Region': defineConfig<NonBlankString>({
+    defaultValue: toNonBlankString('ap-northeast-1'),
   }),
   }),
-  'aws:s3AccessKeyId': defineConfig<string | undefined>({
+  'aws:s3AccessKeyId': defineConfig<NonBlankString | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
-  'aws:s3SecretAccessKey': defineConfig<string | undefined>({
+  'aws:s3SecretAccessKey': defineConfig<NonBlankString | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
+    isSecret: true,
   }),
   }),
-  'aws:s3CustomEndpoint': defineConfig<string | undefined>({
+  'aws:s3CustomEndpoint': defineConfig<NonBlankString | undefined>({
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
 
 
   // GCS Settings
   // GCS Settings
-  'gcs:apiKeyJsonPath': defineConfig<string | undefined>({
+  'gcs:apiKeyJsonPath': defineConfig<NonBlankString | undefined>({
     envVarName: 'GCS_API_KEY_JSON_PATH',
     envVarName: 'GCS_API_KEY_JSON_PATH',
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
-  'gcs:bucket': defineConfig<string | undefined>({
+  'gcs:bucket': defineConfig<NonBlankString | undefined>({
     envVarName: 'GCS_BUCKET',
     envVarName: 'GCS_BUCKET',
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
@@ -866,15 +870,15 @@ export const CONFIG_DEFINITIONS = {
     envVarName: 'AZURE_REFERENCE_FILE_WITH_RELAY_MODE',
     envVarName: 'AZURE_REFERENCE_FILE_WITH_RELAY_MODE',
     defaultValue: false,
     defaultValue: false,
   }),
   }),
-  'azure:tenantId': defineConfig<string | undefined>({
+  'azure:tenantId': defineConfig<NonBlankString | undefined>({
     envVarName: 'AZURE_TENANT_ID',
     envVarName: 'AZURE_TENANT_ID',
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
-  'azure:clientId': defineConfig<string | undefined>({
+  'azure:clientId': defineConfig<NonBlankString | undefined>({
     envVarName: 'AZURE_CLIENT_ID',
     envVarName: 'AZURE_CLIENT_ID',
     defaultValue: undefined,
     defaultValue: undefined,
   }),
   }),
-  'azure:clientSecret': defineConfig<string | undefined>({
+  'azure:clientSecret': defineConfig<NonBlankString | undefined>({
     envVarName: 'AZURE_CLIENT_SECRET',
     envVarName: 'AZURE_CLIENT_SECRET',
     defaultValue: undefined,
     defaultValue: undefined,
     isSecret: true,
     isSecret: true,

+ 87 - 0
apps/app/src/server/service/config-manager/config-manager.integ.ts

@@ -107,6 +107,55 @@ describe('ConfigManager', () => {
     });
     });
   });
   });
 
 
+  describe('updateConfig', () => {
+    beforeEach(async() => {
+      await Config.deleteMany({ key: /app.*/ }).exec();
+      await Config.create({ key: 'app:siteUrl', value: JSON.stringify('initial value') });
+    });
+
+    test('updates a single config', async() => {
+      // arrange
+      await configManager.loadConfigs();
+      const config = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(config?.value).toEqual(JSON.stringify('initial value'));
+
+      // act
+      await configManager.updateConfig('app:siteUrl', 'updated value');
+
+      // assert
+      const updatedConfig = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(updatedConfig?.value).toEqual(JSON.stringify('updated value'));
+    });
+
+    test('removes config when value is undefined and removeIfUndefined is true', async() => {
+      // arrange
+      await configManager.loadConfigs();
+      const config = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(config?.value).toEqual(JSON.stringify('initial value'));
+
+      // act
+      await configManager.updateConfig('app:siteUrl', undefined, { removeIfUndefined: true });
+
+      // assert
+      const updatedConfig = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(updatedConfig).toBeNull(); // should be removed
+    });
+
+    test('does not update config when value is undefined and removeIfUndefined is false', async() => {
+      // arrange
+      await configManager.loadConfigs();
+      const config = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(config?.value).toEqual(JSON.stringify('initial value'));
+
+      // act
+      await configManager.updateConfig('app:siteUrl', undefined);
+
+      // assert
+      const updatedConfig = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(updatedConfig?.value).toEqual(JSON.stringify('initial value')); // should remain unchanged
+    });
+  });
+
   describe('updateConfigs', () => {
   describe('updateConfigs', () => {
     beforeEach(async() => {
     beforeEach(async() => {
       await Config.deleteMany({ key: /app.*/ }).exec();
       await Config.deleteMany({ key: /app.*/ }).exec();
@@ -133,6 +182,44 @@ describe('ConfigManager', () => {
       expect(updatedConfig1?.value).toEqual(JSON.stringify('new value1'));
       expect(updatedConfig1?.value).toEqual(JSON.stringify('new value1'));
       expect(updatedConfig2?.value).toEqual(JSON.stringify('aws'));
       expect(updatedConfig2?.value).toEqual(JSON.stringify('aws'));
     });
     });
+
+    test('removes config when value is undefined and removeIfUndefined is true', async() => {
+      // arrange
+      await configManager.loadConfigs();
+      const config1 = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(config1?.value).toEqual(JSON.stringify('value1'));
+
+      // act
+      await configManager.updateConfigs({
+        'app:siteUrl': undefined,
+        'app:fileUploadType': 'aws',
+      }, { removeIfUndefined: true });
+
+      // assert
+      const updatedConfig1 = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      const updatedConfig2 = await Config.findOne({ key: 'app:fileUploadType' }).exec();
+      expect(updatedConfig1).toBeNull(); // should be removed
+      expect(updatedConfig2?.value).toEqual(JSON.stringify('aws'));
+    });
+
+    test('does not update config when value is undefined and removeIfUndefined is false', async() => {
+      // arrange
+      await configManager.loadConfigs();
+      const config1 = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      expect(config1?.value).toEqual(JSON.stringify('value1'));
+
+      // act
+      await configManager.updateConfigs({
+        'app:siteUrl': undefined,
+        'app:fileUploadType': 'aws',
+      });
+
+      // assert
+      const updatedConfig1 = await Config.findOne({ key: 'app:siteUrl' }).exec();
+      const updatedConfig2 = await Config.findOne({ key: 'app:fileUploadType' }).exec();
+      expect(updatedConfig1?.value).toEqual(JSON.stringify('value1')); // should remain unchanged
+      expect(updatedConfig2?.value).toEqual(JSON.stringify('aws'));
+    });
   });
   });
 
 
   describe('removeConfigs', () => {
   describe('removeConfigs', () => {

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

@@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({
   ConfigMock: {
   ConfigMock: {
     updateOne: vi.fn(),
     updateOne: vi.fn(),
     bulkWrite: vi.fn(),
     bulkWrite: vi.fn(),
+    deleteOne: vi.fn(),
   },
   },
 }));
 }));
 vi.mock('../../models/config', () => ({
 vi.mock('../../models/config', () => ({
@@ -40,6 +41,9 @@ describe('ConfigManager test', () => {
     let loadConfigsSpy;
     let loadConfigsSpy;
     beforeEach(async() => {
     beforeEach(async() => {
       loadConfigsSpy = vi.spyOn(configManager, 'loadConfigs');
       loadConfigsSpy = vi.spyOn(configManager, 'loadConfigs');
+      // Reset mocks
+      mocks.ConfigMock.updateOne.mockClear();
+      mocks.ConfigMock.deleteOne.mockClear();
     });
     });
 
 
     test('invoke publishUpdateMessage()', async() => {
     test('invoke publishUpdateMessage()', async() => {
@@ -70,6 +74,42 @@ describe('ConfigManager test', () => {
       expect(configManager.publishUpdateMessage).not.toHaveBeenCalled();
       expect(configManager.publishUpdateMessage).not.toHaveBeenCalled();
     });
     });
 
 
+    test('remove config when value is undefined and removeIfUndefined is true', async() => {
+      // arrange
+      configManager.publishUpdateMessage = vi.fn();
+      vi.spyOn((configManager as unknown as ConfigManagerToGetLoader).configLoader, 'loadFromDB').mockImplementation(vi.fn());
+
+      // act
+      await configManager.updateConfig('app:siteUrl', undefined, { removeIfUndefined: true });
+
+      // assert
+      expect(mocks.ConfigMock.deleteOne).toHaveBeenCalledTimes(1);
+      expect(mocks.ConfigMock.deleteOne).toHaveBeenCalledWith({ key: 'app:siteUrl' });
+      expect(mocks.ConfigMock.updateOne).not.toHaveBeenCalled();
+      expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
+      expect(configManager.publishUpdateMessage).toHaveBeenCalledTimes(1);
+    });
+
+    test('update config with undefined value when removeIfUndefined is false', async() => {
+      // arrange
+      configManager.publishUpdateMessage = vi.fn();
+      vi.spyOn((configManager as unknown as ConfigManagerToGetLoader).configLoader, 'loadFromDB').mockImplementation(vi.fn());
+
+      // act
+      await configManager.updateConfig('app:siteUrl', undefined);
+
+      // assert
+      expect(mocks.ConfigMock.updateOne).toHaveBeenCalledTimes(1);
+      expect(mocks.ConfigMock.updateOne).toHaveBeenCalledWith(
+        { key: 'app:siteUrl' },
+        { value: JSON.stringify(undefined) },
+        { upsert: true },
+      );
+      expect(mocks.ConfigMock.deleteOne).not.toHaveBeenCalled();
+      expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
+      expect(configManager.publishUpdateMessage).toHaveBeenCalledTimes(1);
+    });
+
   });
   });
 
 
   describe('updateConfigs()', () => {
   describe('updateConfigs()', () => {
@@ -77,18 +117,20 @@ describe('ConfigManager test', () => {
     let loadConfigsSpy;
     let loadConfigsSpy;
     beforeEach(async() => {
     beforeEach(async() => {
       loadConfigsSpy = vi.spyOn(configManager, 'loadConfigs');
       loadConfigsSpy = vi.spyOn(configManager, 'loadConfigs');
+      // Reset mocks
+      mocks.ConfigMock.bulkWrite.mockClear();
     });
     });
 
 
     test('invoke publishUpdateMessage()', async() => {
     test('invoke publishUpdateMessage()', async() => {
-      // arrenge
+      // arrange
       configManager.publishUpdateMessage = vi.fn();
       configManager.publishUpdateMessage = vi.fn();
       vi.spyOn((configManager as unknown as ConfigManagerToGetLoader).configLoader, 'loadFromDB').mockImplementation(vi.fn());
       vi.spyOn((configManager as unknown as ConfigManagerToGetLoader).configLoader, 'loadFromDB').mockImplementation(vi.fn());
 
 
       // act
       // act
-      await configManager.updateConfig('app:siteUrl', '');
+      await configManager.updateConfigs({ 'app:siteUrl': 'https://example.com' });
 
 
       // assert
       // assert
-      // expect(Config.bulkWrite).toHaveBeenCalledTimes(1);
+      expect(mocks.ConfigMock.bulkWrite).toHaveBeenCalledTimes(1);
       expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
       expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
       expect(configManager.publishUpdateMessage).toHaveBeenCalledTimes(1);
       expect(configManager.publishUpdateMessage).toHaveBeenCalledTimes(1);
     });
     });
@@ -102,10 +144,68 @@ describe('ConfigManager test', () => {
       await configManager.updateConfigs({ 'app:siteUrl': '' }, { skipPubsub: true });
       await configManager.updateConfigs({ 'app:siteUrl': '' }, { skipPubsub: true });
 
 
       // assert
       // assert
-      // expect(Config.bulkWrite).toHaveBeenCalledTimes(1);
+      expect(mocks.ConfigMock.bulkWrite).toHaveBeenCalledTimes(1);
       expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
       expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
       expect(configManager.publishUpdateMessage).not.toHaveBeenCalled();
       expect(configManager.publishUpdateMessage).not.toHaveBeenCalled();
     });
     });
+
+    test('remove configs when values are undefined and removeIfUndefined is true', async() => {
+      // arrange
+      configManager.publishUpdateMessage = vi.fn();
+      vi.spyOn((configManager as unknown as ConfigManagerToGetLoader).configLoader, 'loadFromDB').mockImplementation(vi.fn());
+
+      // act
+      await configManager.updateConfigs(
+        { 'app:siteUrl': undefined, 'app:title': 'GROWI' },
+        { removeIfUndefined: true },
+      );
+
+      // assert
+      expect(mocks.ConfigMock.bulkWrite).toHaveBeenCalledTimes(1);
+      const operations = mocks.ConfigMock.bulkWrite.mock.calls[0][0];
+      expect(operations).toHaveLength(2);
+      expect(operations[0]).toEqual({ deleteOne: { filter: { key: 'app:siteUrl' } } });
+      expect(operations[1]).toEqual({
+        updateOne: {
+          filter: { key: 'app:title' },
+          update: { value: JSON.stringify('GROWI') },
+          upsert: true,
+        },
+      });
+      expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
+      expect(configManager.publishUpdateMessage).toHaveBeenCalledTimes(1);
+    });
+
+    test('update configs including undefined values when removeIfUndefined is false', async() => {
+      // arrange
+      configManager.publishUpdateMessage = vi.fn();
+      vi.spyOn((configManager as unknown as ConfigManagerToGetLoader).configLoader, 'loadFromDB').mockImplementation(vi.fn());
+
+      // act
+      await configManager.updateConfigs({ 'app:siteUrl': undefined, 'app:title': 'GROWI' });
+
+      // assert
+      expect(mocks.ConfigMock.bulkWrite).toHaveBeenCalledTimes(1);
+      const operations = mocks.ConfigMock.bulkWrite.mock.calls[0][0];
+      expect(operations).toHaveLength(2); // both operations should be included
+      expect(operations[0]).toEqual({
+        updateOne: {
+          filter: { key: 'app:siteUrl' },
+          update: { value: JSON.stringify(undefined) },
+          upsert: true,
+        },
+      });
+      expect(operations[1]).toEqual({
+        updateOne: {
+          filter: { key: 'app:title' },
+          update: { value: JSON.stringify('GROWI') },
+          upsert: true,
+        },
+      });
+      expect(loadConfigsSpy).toHaveBeenCalledTimes(1);
+      expect(configManager.publishUpdateMessage).toHaveBeenCalledTimes(1);
+    });
+
   });
   });
 
 
   describe('getManagedEnvVars()', () => {
   describe('getManagedEnvVars()', () => {

Some files were not shown because too many files changed in this diff