Browse Source

Merge branch 'master' into fix/doc-v3-questionnaire

Yuki Takei 1 year ago
parent
commit
95bbb1083e
37 changed files with 1859 additions and 307 deletions
  1. 6 1
      .devcontainer/app/initializeCommand.sh
  2. 6 1
      .devcontainer/pdf-converter/initializeCommand.sh
  3. 14 3
      .github/workflows/reusable-app-prod.yml
  4. 13 1
      apps/app/bin/swagger-jsdoc/definition-apiv3.js
  5. 3 0
      apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh
  6. 1 1
      apps/app/package.json
  7. 97 3
      apps/app/src/features/growi-plugin/server/routes/apiv3/admin/index.ts
  8. 102 0
      apps/app/src/features/templates/server/routes/apiv3/index.ts
  9. 12 0
      apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js
  10. 174 1
      apps/app/src/server/routes/apiv3/activity.ts
  11. 1 1
      apps/app/src/server/routes/apiv3/attachment.js
  12. 22 19
      apps/app/src/server/routes/apiv3/bookmarks.js
  13. 2 2
      apps/app/src/server/routes/apiv3/customize-setting.js
  14. 237 3
      apps/app/src/server/routes/apiv3/g2g-transfer.ts
  15. 1 2
      apps/app/src/server/routes/apiv3/healthcheck.ts
  16. 106 19
      apps/app/src/server/routes/apiv3/import.js
  17. 173 0
      apps/app/src/server/routes/apiv3/in-app-notification.ts
  18. 166 1
      apps/app/src/server/routes/apiv3/page-listing.ts
  19. 197 0
      apps/app/src/server/routes/apiv3/slack-integration.js
  20. 58 1
      apps/app/src/server/routes/apiv3/staffs.js
  21. 45 4
      apps/app/src/server/routes/apiv3/statistics.js
  22. 11 0
      apps/app/src/server/routes/apiv3/user-group-relation.js
  23. 45 0
      apps/app/src/server/routes/apiv3/user-ui-settings.ts
  24. 36 0
      apps/app/src/server/routes/apiv3/users.js
  25. 1 1
      apps/app/src/server/routes/index.js
  26. 68 0
      apps/app/src/server/routes/login.js
  27. 1 1
      apps/pdf-converter/package.json
  28. 5 3
      package.json
  29. 12 0
      packages/pluginkit/CHANGELOG.md
  30. 5 2
      packages/pluginkit/package.json
  31. 1 0
      packages/pluginkit/src/index.ts
  32. 1 0
      packages/pluginkit/src/v4/client/index.ts
  33. 42 0
      packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.spec.ts
  34. 34 0
      packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.ts
  35. 1 0
      packages/pluginkit/src/v4/client/utils/growi-facade/index.ts
  36. 1 0
      packages/pluginkit/src/v4/client/utils/index.ts
  37. 159 237
      pnpm-lock.yaml

+ 6 - 1
.devcontainer/app/initializeCommand.sh

@@ -1,4 +1,9 @@
 # prevent file not found error on docker compose up
 if [ ! -f ".devcontainer/compose.extend.yml" ]; then
-  touch .devcontainer/compose.extend.yml
+
+cat > ".devcontainer/compose.extend.yml" <<EOF
+services:
+  {}
+EOF
+
 fi

+ 6 - 1
.devcontainer/pdf-converter/initializeCommand.sh

@@ -1,4 +1,9 @@
 # prevent file not found error on docker compose up
 if [ ! -f ".devcontainer/compose.extend.yml" ]; then
-  touch .devcontainer/compose.extend.yml
+
+cat > ".devcontainer/compose.extend.yml" <<EOF
+services:
+  {}
+EOF
+
 fi

+ 14 - 3
.github/workflows/reusable-app-prod.yml

@@ -266,7 +266,7 @@ jobs:
       if: always()
       uses: actions/upload-artifact@v4
       with:
-        name: blob-report-${{ matrix.shard }}
+        name: blob-report-${{ matrix.browser }}-${{ matrix.shard }}
         path: blob-report
         retention-days: 30
 
@@ -302,10 +302,21 @@ jobs:
       run: |
         pnpm install --frozen-lockfile
 
+    - name: Download blob reports
+      uses: actions/download-artifact@v4
+      with:
+        pattern: blob-report-*
+        path: all-blob-reports
+        merge-multiple: true
+
     - name: Merge into HTML Report
       run: |
-        mkdir -p all-blob-reports
-        pnpm playwright merge-reports --reporter html ./all-blob-reports
+        mkdir -p playwright-report
+        if [ -z "$(ls all-blob-reports/*.zip all-blob-reports/*.blob 2>/dev/null || true)" ]; then
+          echo "<html><body><h1>No test results available</h1><p>This could be because tests were skipped or all artifacts were not available.</p></body></html>" > playwright-report/index.html
+        else
+          pnpm playwright merge-reports --reporter html all-blob-reports
+        fi
 
     - name: Upload HTML report
       uses: actions/upload-artifact@v4

+ 13 - 1
apps/app/bin/swagger-jsdoc/definition-apiv3.js

@@ -28,6 +28,11 @@ module.exports = {
         in: 'cookie',
         name: 'connect.sid',
       },
+      transferHeaderAuth: {
+        type: 'apiKey',
+        in: 'header',
+        name: 'x-growi-transfer-key',
+      },
     },
   },
   'x-tagGroups': [
@@ -39,10 +44,11 @@ module.exports = {
         'BookmarkFolders',
         'Page',
         'Pages',
+        'PageListing',
         'Revisions',
         'ShareLinks',
         'Users',
-        '',
+        'UserUISettings',
         '',
       ],
     },
@@ -63,6 +69,7 @@ module.exports = {
       name: 'System Management API',
       tags: [
         'Home',
+        'Activity',
         'AdminHome',
         'AppSettings',
         'ExternalUserGroups',
@@ -71,15 +78,20 @@ module.exports = {
         'CustomizeSetting',
         'Import',
         'Export',
+        'GROWI to GROWI Transfer',
         'MongoDB',
         'NotificationSetting',
+        'Plugins',
         'Questionnaire',
         'QuestionnaireSetting',
+        'SlackIntegration',
         'SlackIntegrationSettings',
         'SlackIntegrationSettings (with proxy)',
         'SlackIntegrationSettings (without proxy)',
         'SlackIntegrationLegacySetting',
         'ShareLink Management',
+        'Templates',
+        'Staff',
         'UserGroupRelations',
         'UserGroups',
         'Users Management',

+ 3 - 0
apps/app/bin/swagger-jsdoc/generate-spec-apiv3.sh

@@ -12,5 +12,8 @@ swagger-jsdoc \
   -d "${APP_PATH}/bin/swagger-jsdoc/definition-apiv3.js" \
   "${APP_PATH}/src/features/external-user-group/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/growi-plugin/server/routes/apiv3/**/*.ts" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \
+  "${APP_PATH}/src/server/routes/login.js" \
   "${APP_PATH}/src/server/models/openapi/**/*.{js,ts}"

+ 1 - 1
apps/app/package.json

@@ -167,7 +167,7 @@
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "mustache": "^4.2.0",
-    "next": "^14.2.21",
+    "next": "^14.2.25",
     "next-dynamic-loading-props": "^0.1.1",
     "next-i18next": "^15.3.1",
     "next-superjson": "^0.0.4",

+ 97 - 3
apps/app/src/features/growi-plugin/server/routes/apiv3/admin/index.ts

@@ -1,9 +1,10 @@
-import express, { Request, Router } from 'express';
+import type { Request, Router } from 'express';
+import express from 'express';
 import { body, query } from 'express-validator';
 import mongoose from 'mongoose';
 
-import Crowi from '~/server/crowi';
-import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
+import type Crowi from '~/server/crowi';
+import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 
 import { GrowiPlugin } from '../../../models';
 import { growiPluginService } from '../../../services';
@@ -39,6 +40,45 @@ module.exports = (crowi: Crowi): Router => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   * /plugins:
+   *   post:
+   *     tags: [Plugins]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /plugins
+   *     description: Install a plugin
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             properties:
+   *               pluginInstallerForm:
+   *                 type: object
+   *                 properties:
+   *                   url:
+   *                     type: string
+   *                   ghBranch:
+   *                     type: string
+   *                   ghTag:
+   *                     type: string
+   *     responses:
+   *       200:
+   *         description: OK
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 pluginName:
+   *                   type: string
+   *                   description: The name of the installed plugin
+   *
+   */
   router.post('/', loginRequiredStrictly, adminRequired, validator.pluginFormValueisRequired, async(req: Request, res: ApiV3Response) => {
     const { pluginInstallerForm: formValue } = req.body;
 
@@ -51,6 +91,33 @@ module.exports = (crowi: Crowi): Router => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   * /plugins/{id}/activate:
+   *   put:
+   *     tags: [Plugins]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /plugins/{id}/activate
+   *     description: Activate a plugin
+   *     parameters:
+   *       - name: id
+   *         in: path
+   *         required: true
+   *         type: string
+   *     responses:
+   *       200:
+   *         description: OK
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 pluginName:
+   *                   type: string
+   *                   description: The name of the activated plugin
+   */
   router.put('/:id/activate', loginRequiredStrictly, adminRequired, validator.pluginIdisRequired, async(req: Request, res: ApiV3Response) => {
     const { id } = req.params;
     const pluginId = new ObjectID(id);
@@ -77,6 +144,33 @@ module.exports = (crowi: Crowi): Router => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   * /plugins/{id}/remove:
+   *   delete:
+   *     tags: [Plugins]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /plugins/{id}/remove
+   *     description: Remove a plugin
+   *     parameters:
+   *       - name: id
+   *         in: path
+   *         required: true
+   *         type: string
+   *     responses:
+   *       200:
+   *         description: OK
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 pluginName:
+   *                   type: string
+   *                   description: The name of the removed plugin
+   */
   router.delete('/:id/remove', loginRequiredStrictly, adminRequired, validator.pluginIdisRequired, async(req: Request, res: ApiV3Response) => {
     const { id } = req.params;
     const pluginId = new ObjectID(id);

+ 102 - 0
apps/app/src/features/templates/server/routes/apiv3/index.ts

@@ -36,6 +36,46 @@ let presetTemplateSummaries: TemplateSummary[];
 module.exports = (crowi: Crowi) => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
 
+  /**
+   * @swagger
+   *
+   * /templates:
+   *   get:
+   *     summary: /templates
+   *     security:
+   *       - cookieAuth: []
+   *     description: Get all templates
+   *     tags: [Templates]
+   *     parameters:
+   *       - name: includeInvalidTemplates
+   *         in: query
+   *         description: Whether to include invalid templates
+   *         required: false
+   *         type: boolean
+   *     responses:
+   *       200:
+   *         description: OK
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 summaries:
+   *                   type: object
+   *                   additionalProperties:
+   *                     type: object
+   *                     properties:
+   *                       id:
+   *                         type: string
+   *                       isDefault:
+   *                         type: boolean
+   *                       isValid:
+   *                         type: boolean
+   *                       locale:
+   *                         type: string
+   *                       title:
+   *                         type: string
+   */
   router.get('/', loginRequiredStrictly, validator.list, apiV3FormValidator, async(req, res: ApiV3Response) => {
     const { includeInvalidTemplates } = req.query;
 
@@ -72,6 +112,34 @@ module.exports = (crowi: Crowi) => {
     });
   });
 
+  /**
+   * @swagger
+   *
+   * /templates/preset-templates/{templateId}/{locale}:
+   *   get:
+   *     tags: [Templates]
+   *     summary: /templates/preset-templates/{templateId}/{locale}
+   *     security:
+   *       - cookieAuth: []
+   *     description: Get a preset template
+   *     parameters:
+   *       - name: templateId
+   *         in: path
+   *         description: The template ID
+   *       - name: locale
+   *         in: path
+   *         description: The locale
+   *     responses:
+   *       200:
+   *         description: OK
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 markdown:
+   *                   type: string
+   */
   router.get('/preset-templates/:templateId/:locale', loginRequiredStrictly, validator.get, apiV3FormValidator, async(req, res: ApiV3Response) => {
     const {
       templateId, locale,
@@ -88,6 +156,40 @@ module.exports = (crowi: Crowi) => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   * /templates/plugin-templates/{organizationId}/{reposId}/{templateId}/{locale}:
+   *   get:
+   *     tags: [Templates]
+   *     summary: /templates/plugin-templates/{organizationId}/{reposId}/{templateId}/{locale}
+   *     security:
+   *       - cookieAuth: []
+   *     description: Get a plugin template
+   *     parameters:
+   *       - name: organizationId
+   *         in: path
+   *         description: The organization ID
+   *       - name: reposId
+   *         in: path
+   *         description: The repository ID
+   *       - name: templateId
+   *         in: path
+   *         description: The template ID
+   *       - name: locale
+   *         in: path
+   *         description: The locale
+   *     responses:
+   *       200:
+   *         description: OK
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 markdown:
+   *                   type: string
+   */
   router.get('/plugin-templates/:organizationId/:reposId/:templateId/:locale', loginRequiredStrictly, validator.get, apiV3FormValidator, async(
       req, res: ApiV3Response,
   ) => {

+ 12 - 0
apps/app/src/migrations/19700101000000-foremost-1000-20241123211930-remove-index-for-ns-from-configs.js

@@ -24,6 +24,18 @@ module.exports = {
     logger.info('Apply migration');
     await mongoose.connect(getMongoUri(), mongoOptions);
 
+    // remove unnecessary data
+    // see: https://redmine.weseek.co.jp/issues/163527
+    await db.collection('configs').deleteMany({
+      ns: 'crowi',
+      key: {
+        $in: [
+          'notification:owner-page:isEnabled',
+          'notification:group-page:isEnabled',
+        ],
+      },
+    });
+
     // drop index
     await dropIndexIfExists(db, 'configs', 'ns_1_key_1');
 

+ 174 - 1
apps/app/src/server/routes/apiv3/activity.ts

@@ -27,13 +27,186 @@ const validator = {
   ],
 };
 
+/**
+ * @swagger
+ *
+ * components:
+ *   schemas:
+ *     ActivityResponse:
+ *       type: object
+ *       properties:
+ *         serializedPaginationResult:
+ *           type: object
+ *           properties:
+ *             docs:
+ *               type: array
+ *               items:
+ *                 type: object
+ *                 properties:
+ *                   _id:
+ *                     type: string
+ *                     example: "67e33da5d97e8d3b53e99f95"
+ *                   id:
+ *                     type: string
+ *                     example: "67e33da5d97e8d3b53e99f95"
+ *                   ip:
+ *                     type: string
+ *                     example: "::ffff:172.18.0.1"
+ *                   endpoint:
+ *                     type: string
+ *                     example: "/_api/pages.remove"
+ *                   targetModel:
+ *                     type: string
+ *                     example: "Page"
+ *                   target:
+ *                     type: string
+ *                     example: "675547e97f208f8050a361d4"
+ *                   action:
+ *                     type: string
+ *                     example: "PAGE_DELETE_COMPLETELY"
+ *                   snapshot:
+ *                     type: object
+ *                     properties:
+ *                       username:
+ *                         type: string
+ *                         example: "growi"
+ *                       _id:
+ *                         type: string
+ *                         example: "67e33da5d97e8d3b53e99f96"
+ *                   createdAt:
+ *                     type: string
+ *                     format: date-time
+ *                     example: "2025-03-25T23:35:01.584Z"
+ *                   __v:
+ *                     type: integer
+ *                     example: 0
+ *                   user:
+ *                     type: object
+ *                     properties:
+ *                       _id:
+ *                         type: string
+ *                         example: "669a5aa48d45e62b521d00e4"
+ *                       isGravatarEnabled:
+ *                         type: boolean
+ *                         example: false
+ *                       isEmailPublished:
+ *                         type: boolean
+ *                         example: true
+ *                       lang:
+ *                         type: string
+ *                         example: "ja_JP"
+ *                       status:
+ *                         type: integer
+ *                         example: 2
+ *                       admin:
+ *                         type: boolean
+ *                         example: true
+ *                       readOnly:
+ *                         type: boolean
+ *                         example: false
+ *                       isInvitationEmailSended:
+ *                         type: boolean
+ *                         example: false
+ *                       isQuestionnaireEnabled:
+ *                         type: boolean
+ *                         example: true
+ *                       name:
+ *                         type: string
+ *                         example: "Taro"
+ *                       username:
+ *                         type: string
+ *                         example: "grow"
+ *                       createdAt:
+ *                         type: string
+ *                         format: date-time
+ *                         example: "2024-07-19T12:23:00.806Z"
+ *                       updatedAt:
+ *                         type: string
+ *                         format: date-time
+ *                         example: "2025-03-25T23:34:04.362Z"
+ *                       __v:
+ *                         type: integer
+ *                         example: 0
+ *                       imageUrlCached:
+ *                         type: string
+ *                         example: "/images/icons/user.svg"
+ *                       lastLoginAt:
+ *                         type: string
+ *                         format: date-time
+ *                         example: "2025-03-25T23:34:04.355Z"
+ *                       email:
+ *                         type: string
+ *                         example: "test@example.com"
+ *             totalDocs:
+ *               type: integer
+ *               example: 3
+ *             offset:
+ *               type: integer
+ *               example: 0
+ *             limit:
+ *               type: integer
+ *               example: 10
+ *             totalPages:
+ *               type: integer
+ *               example: 1
+ *             page:
+ *               type: integer
+ *               example: 1
+ *             pagingCounter:
+ *               type: integer
+ *               example: 1
+ *             hasPrevPage:
+ *               type: boolean
+ *               example: false
+ *             hasNextPage:
+ *               type: boolean
+ *               example: false
+ *             prevPage:
+ *               type: integer
+ *               nullable: true
+ *               example: null
+ *             nextPage:
+ *               type: integer
+ *               nullable: true
+ *               example: null
+ */
+
 module.exports = (crowi: Crowi): Router => {
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
 
   const router = express.Router();
 
-  // eslint-disable-next-line max-len
+  /**
+   * @swagger
+   *
+   * /activity:
+   *   get:
+   *     summary: /activity
+   *     tags: [Activity]
+   *     security:
+   *       - api_key: []
+   *     parameters:
+   *       - name: limit
+   *         in: query
+   *         required: false
+   *         type: integer
+   *       - name: offset
+   *         in: query
+   *         required: false
+   *         type: integer
+   *       - name: searchFilter
+   *         in: query
+   *         required: false
+   *         type: string
+   *     responses:
+   *       200:
+   *         description: Activity fetched successfully
+   *         content:
+   *           application/json:
+   *             schema:
+   *               $ref: '#/components/schemas/ActivityResponse'
+   */
   router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, validator.list, apiV3FormValidator, async(req: Request, res: ApiV3Response) => {
     const auditLogEnabled = configManager.getConfig('app:auditLogEnabled');
     if (!auditLogEnabled) {

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

@@ -339,7 +339,7 @@ module.exports = (crowi) => {
    *          500:
    *            $ref: '#/components/responses/500'
    */
-  router.post('/', uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser,
+  router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, uploads.single('file'), autoReap,
     validator.retrieveAddAttachment, apiV3FormValidator, addActivity,
     async(req, res) => {
 

+ 22 - 19
apps/app/src/server/routes/apiv3/bookmarks.js

@@ -40,10 +40,17 @@ const router = express.Router();
  *            description: date created at
  *            example: 2010-01-01T00:00:00.000Z
  *          page:
- *            $ref: '#/components/schemas/Page/properties/_id'
+ *            $ref: '#/components/schemas/Page'
  *          user:
  *            $ref: '#/components/schemas/User/properties/_id'
- *
+ *      Bookmarks:
+ *        description: User Root Bookmarks
+ *        type: object
+ *        properties:
+ *          userRootBookmarks:
+ *            type: array
+ *            items:
+ *              $ref: '#/components/schemas/Bookmark'
  *      BookmarkParams:
  *        description: BookmarkParams
  *        type: object
@@ -66,6 +73,14 @@ const router = express.Router();
  *          isBookmarked:
  *            type: boolean
  *            description: Whether the request user bookmarked (will be returned if the user is included in the request)
+ *          pageId:
+ *            type: string
+ *            description: page ID
+ *            example: 5e07345972560e001761fa63
+ *          bookmarkedUsers:
+ *            type: array
+ *            items:
+ *              $ref: '#/components/schemas/User'
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
@@ -165,28 +180,13 @@ module.exports = (crowi) => {
    *            description: user id
    *            schema:
    *              type: string
-   *          - name: page
-   *            in: query
-   *            description: selected page number
-   *            schema:
-   *              type: number
-   *          - name: limit
-   *            in: query
-   *            description: page item limit
-   *            schema:
-   *              type: number
-   *          - name: offset
-   *            in: query
-   *            description: page item offset
-   *            schema:
-   *              type: number
    *        responses:
    *          200:
    *            description: Succeeded to get my bookmarked status.
    *            content:
    *              application/json:
    *                schema:
-   *                  $ref: '#/components/schemas/Bookmark'
+   *                  $ref: '#/components/schemas/Bookmarks'
    */
   validator.userBookmarkList = [
     param('userId').isMongoId().withMessage('userId is required'),
@@ -244,7 +244,10 @@ module.exports = (crowi) => {
    *            content:
    *              application/json:
    *                schema:
-   *                  $ref: '#/components/schemas/Bookmark'
+   *                  type: object
+   *                  properties:
+   *                    bookmark:
+   *                      $ref: '#/components/schemas/Bookmark'
    */
   router.put('/', accessTokenParser, loginRequiredStrictly, addActivity, validator.bookmarks, apiV3FormValidator, async(req, res) => {
     const { pageId, bool } = req.body;

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

@@ -1021,8 +1021,8 @@ module.exports = (crowi) => {
    *                            temporaryUrlExpiredAt: {}
    *                            temporaryUrlCached: {}
    */
-  router.post('/upload-brand-logo', uploads.single('file'), loginRequiredStrictly,
-    adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
+  router.post('/upload-brand-logo', loginRequiredStrictly, adminRequired,
+    uploads.single('file'), validator.logo, apiV3FormValidator, async(req, res) => {
 
       if (req.file == null) {
         return res.apiv3Err(new ErrorV3('File error.', 'upload-brand-logo-failed'));

+ 237 - 3
apps/app/src/server/routes/apiv3/g2g-transfer.ts

@@ -37,6 +37,43 @@ const validator = {
   ],
 };
 
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      GrowiInfo:
+ *        type: object
+ *        properties:
+ *           version:
+ *             type: string
+ *             description: The version of the GROWI
+ *           userUpperLimit:
+ *             type: number
+ *             description: The upper limit of the number of users
+ *           fileUploadDisabled:
+ *             type: boolean
+ *           fileUploadTotalLimit:
+ *             type: number
+ *             description: The total limit of the file upload size
+ *           attachmentInfo:
+ *             type: object
+ *             properties:
+ *               type:
+ *                 type: string
+ *               writable:
+ *                 type: boolean
+ *               bucket:
+ *                 type: string
+ *               customEndpoint:
+ *                 type: string
+ *               uploadNamespace:
+ *                 type: string
+ *               accountName:
+ *                 type: string
+ *               containerName:
+ *                 type: string
+*/
 /*
  * Routes
  */
@@ -131,15 +168,88 @@ module.exports = (crowi: Crowi): Router => {
   const receiveRouter = express.Router();
   const pushRouter = express.Router();
 
+  /**
+   * @swagger
+   *
+   *  /g2g-transfer/files:
+   *    get:
+   *      summary: /g2g-transfer/files
+   *      tags: [GROWI to GROWI Transfer]
+   *      security:
+   *        - transferHeaderAuth: []
+   *      responses:
+   *        '200':
+   *          description: Successfully got the list of files
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  files:
+   *                    type: array
+   *                    items:
+   *                      type: object
+   *                      properties:
+   *                        name:
+   *                          type: string
+   *                          description: The name of the file
+   *                        size:
+   *                          type: number
+   *                          description: The size of the file
+   */
   // eslint-disable-next-line max-len
   receiveRouter.get('/files', validateTransferKey, async(req: Request, res: ApiV3Response) => {
     const files = await crowi.fileUploadService.listFiles();
     return res.apiv3({ files });
   });
 
-  // Auto import
+  /**
+   * @swagger
+   *
+   *  /g2g-transfer:
+   *    post:
+   *      summary: /g2g-transfer
+   *      tags: [GROWI to GROWI Transfer]
+   *      security:
+   *        - transferHeaderAuth: []
+   *      requestBody:
+   *        required: true
+   *        content:
+   *          multipart/form-data:
+   *            schema:
+   *              type: object
+   *              properties:
+   *                file:
+   *                  format: binary
+   *                  description: The zip file of the data to be transferred
+   *                collections:
+   *                  type: array
+   *                  description: The list of MongoDB collections to be transferred
+   *                  items:
+   *                    type: string
+   *                optionsMap:
+   *                  type: object
+   *                  description: The map of options for each collection
+   *                operatorUserId:
+   *                  type: string
+   *                  description: The ID of the operator user
+   *                uploadConfigs:
+   *                  type: object
+   *                  description: The map of upload configurations
+   *      responses:
+   *        '200':
+   *          description: Successfully started to receive transfer data
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  message:
+   *                    type: string
+   *                    description: The message of the result
+   */
   // eslint-disable-next-line max-len
-  receiveRouter.post('/', uploads.single('transferDataZipFile'), validateTransferKey, async(req: Request & { file: any; }, res: ApiV3Response) => {
+  receiveRouter.post('/', validateTransferKey, uploads.single('transferDataZipFile'), async(req: Request & { file: any; }, res: ApiV3Response) => {
     const { file } = req;
     const {
       collections: strCollections,
@@ -222,8 +332,42 @@ module.exports = (crowi: Crowi): Router => {
     return res.apiv3({ message: 'Successfully started to receive transfer data.' });
   });
 
+  /**
+   * @swagger
+   *
+   *  /g2g-transfer/attachment:
+   *    post:
+   *      summary: /g2g-transfer/attachment
+   *      tags: [GROWI to GROWI Transfer]
+   *      security:
+   *        - transferHeaderAuth: []
+   *      requestBody:
+   *        required: true
+   *        content:
+   *          multipart/form-data:
+   *            schema:
+   *              type: object
+   *              properties:
+   *                file:
+   *                  format: binary
+   *                  description: The zip file of the data to be transferred
+   *                attachmentMetadata:
+   *                  type: object
+   *                  description: Metadata of the attachment
+   *      responses:
+   *        '200':
+   *          description:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  message:
+   *                    type: string
+   *                    description: The message of the result
+   */
   // This endpoint uses multer's MemoryStorage since the received data should be persisted directly on attachment storage.
-  receiveRouter.post('/attachment', uploadsForAttachment.single('content'), validateTransferKey,
+  receiveRouter.post('/attachment', validateTransferKey, uploadsForAttachment.single('content'),
     async(req: Request & { file: any; }, res: ApiV3Response) => {
       const { file } = req;
       const { attachmentMetadata } = req.body;
@@ -251,6 +395,26 @@ module.exports = (crowi: Crowi): Router => {
       return res.apiv3({ message: 'Successfully imported attached file.' });
     });
 
+  /**
+   * @swagger
+   *
+   *  /g2g-transfer/growi-info:
+   *    get:
+   *      summary: /g2g-transfer/growi-info
+   *      tags: [GROWI to GROWI Transfer]
+   *      security:
+   *        - transferHeaderAuth: []
+   *      responses:
+   *        '200':
+   *          description:
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  growiInfo:
+   *                    $ref: '#/components/schemas/GrowiInfo'
+   */
   receiveRouter.get('/growi-info', validateTransferKey, async(req: Request, res: ApiV3Response) => {
     let growiInfo: IDataGROWIInfo;
     try {
@@ -269,6 +433,37 @@ module.exports = (crowi: Crowi): Router => {
     return res.apiv3({ growiInfo });
   });
 
+  /**
+   * @swagger
+   *
+   *  /g2g-transfer/generate-key:
+   *    post:
+   *      summary: /g2g-transfer/generate-key
+   *      tags: [GROWI to GROWI Transfer]
+   *      security:
+   *        - api_key: []
+   *      requestBody:
+   *        required: true
+   *        content:
+   *          application/json:
+   *            schema:
+   *              type: object
+   *              properties:
+   *                appSiteUrl:
+   *                  type: string
+   *                  description: The URL of the GROWI
+   *      responses:
+   *        '200':
+   *          description: Successfully generated transfer key
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  transferKey:
+   *                    type: string
+   *                    description: The transfer key
+   */
   // eslint-disable-next-line max-len
   receiveRouter.post('/generate-key', accessTokenParser, adminRequiredIfInstalled, appSiteUrlRequiredIfNotInstalled, async(req: Request, res: ApiV3Response) => {
     const appSiteUrl = req.body.appSiteUrl ?? configManager.getConfig('app:siteUrl');
@@ -295,6 +490,45 @@ module.exports = (crowi: Crowi): Router => {
     return res.apiv3({ transferKey: transferKeyString });
   });
 
+  /**
+   * @swagger
+   *
+   *  /g2g-transfer/transfer:
+   *    post:
+   *      summary: /g2g-transfer/transfer
+   *      tags: [GROWI to GROWI Transfer]
+   *      security:
+   *        - api_key: []
+   *      requestBody:
+   *        required: true
+   *        content:
+   *          application/json:
+   *            schema:
+   *              type: object
+   *              properties:
+   *                transferKey:
+   *                  type: string
+   *                  description: The transfer key
+   *                collections:
+   *                  type: array
+   *                  description: The list of MongoDB collections to be transferred
+   *                  items:
+   *                    type: string
+   *                optionsMap:
+   *                  type: object
+   *                  description: The map of options for each collection
+   *      responses:
+   *        '200':
+   *          description: Successfully requested auto transfer
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   *                properties:
+   *                  message:
+   *                    type: string
+   *                    description: The message of the result
+   */
   // eslint-disable-next-line max-len
   pushRouter.post('/transfer', accessTokenParser, loginRequiredStrictly, adminRequired, validator.transfer, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const { transferKey, collections, optionsMap } = req.body;

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

@@ -77,6 +77,7 @@ module.exports = (crowi) => {
    *  /healthcheck:
    *    get:
    *      tags: [Healthcheck]
+   *      security: []
    *      operationId: getHealthcheck
    *      summary: /healthcheck
    *      description: Check whether the server is healthy or not
@@ -116,8 +117,6 @@ module.exports = (crowi) => {
    *                    description: Errors
    *                    items:
    *                      $ref: '#/components/schemas/ErrorV3'
-   *                  info:
-   *                    $ref: '#/components/schemas/HealthcheckInfo'
    */
   router.get('/', nocache(), async(req, res: ApiV3Response) => {
     let checkServices = (() => {

+ 106 - 19
apps/app/src/server/routes/apiv3/import.js

@@ -31,17 +31,13 @@ const router = express.Router();
  *            description: Import mode
  *            type: string
  *            enum: [insert, upsert, flushAndInsert]
- */
-
-/**
- * @swagger
- *
- *  components:
- *    schemas:
  *      ImportStatus:
  *        description: ImportStatus
  *        type: object
  *        properties:
+ *          isTheSameVersion:
+ *            type: boolean
+ *            description: whether the version of the uploaded data is the same as the current GROWI version
  *          zipFileStat:
  *            type: object
  *            description: the property object
@@ -53,6 +49,77 @@ const router = express.Router();
  *          isImporting:
  *            type: boolean
  *            description: whether the current importing job exists or not
+ *      FileImportResponse:
+ *        type: object
+ *        properties:
+ *          meta:
+ *            type: object
+ *            properties:
+ *              version:
+ *                type: string
+ *              url:
+ *                type: string
+ *              passwordSeed:
+ *                type: string
+ *              exportedAt:
+ *                type: string
+ *                format: date-time
+ *              envVars:
+ *                type: object
+ *                properties:
+ *                  ELASTICSEARCH_URI:
+ *                    type: string
+ *          fileName:
+ *            type: string
+ *          zipFilePath:
+ *            type: string
+ *          fileStat:
+ *            type: object
+ *            properties:
+ *              dev:
+ *                type: integer
+ *              mode:
+ *                type: integer
+ *              nlink:
+ *                type: integer
+ *              uid:
+ *                type: integer
+ *              gid:
+ *                type: integer
+ *              rdev:
+ *                type: integer
+ *              blksize:
+ *                type: integer
+ *              ino:
+ *                type: integer
+ *              size:
+ *                type: integer
+ *              blocks:
+ *                type: integer
+ *              atime:
+ *                type: string
+ *                format: date-time
+ *              mtime:
+ *                type: string
+ *                format: date-time
+ *              ctime:
+ *                type: string
+ *                format: date-time
+ *              birthtime:
+ *                type: string
+ *                format: date-time
+ *          innerFileStats:
+ *            type: array
+ *            items:
+ *              type: object
+ *              properties:
+ *                fileName:
+ *                  type: string
+ *                collectionName:
+ *                  type: string
+ *                size:
+ *                  type: integer
+ *                  nullable: true
  */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 export default function route(crowi) {
@@ -101,6 +168,8 @@ export default function route(crowi) {
    *  /import:
    *    get:
    *      tags: [Import]
+   *      security:
+   *        - api_key: []
    *      operationId: getImportSettingsParams
    *      summary: /import
    *      description: Get import settings params
@@ -114,6 +183,19 @@ export default function route(crowi) {
    *                  importSettingsParams:
    *                    type: object
    *                    description: import settings params
+   *                    properties:
+   *                      esaTeamName:
+   *                        type: string
+   *                        description: the team name of esa.io
+   *                      esaAccessToken:
+   *                        type: string
+   *                        description: the access token of esa.io
+   *                      qiitaTeamName:
+   *                        type: string
+   *                        description: the team name of qiita.com
+   *                      qiitaAccessToken:
+   *                        type: string
+   *                        description: the access token of qiita.com
    */
   router.get('/', accessTokenParser, loginRequired, adminRequired, async(req, res) => {
     try {
@@ -138,6 +220,8 @@ export default function route(crowi) {
    *  /import/status:
    *    get:
    *      tags: [Import]
+   *      security:
+   *        - api_key: []
    *      operationId: getImportStatus
    *      summary: /import/status
    *      description: Get properties of stored zip files for import
@@ -167,6 +251,8 @@ export default function route(crowi) {
    *  /import:
    *    post:
    *      tags: [Import]
+   *      security:
+   *        - api_key: []
    *      operationId: executeImport
    *      summary: /import
    *      description: import a collection from a zipped json
@@ -297,27 +383,26 @@ export default function route(crowi) {
    *  /import/upload:
    *    post:
    *      tags: [Import]
+   *      security:
+   *        - api_key: []
    *      operationId: uploadImport
    *      summary: /import/upload
    *      description: upload a zip file
+   *      requestBody:
+   *        content:
+   *          multipart/form-data:
+   *            schema:
+   *              type: object
+   *              properties:
+   *                file:
+   *                  format: binary
    *      responses:
    *        200:
    *          description: the file is uploaded
    *          content:
    *            application/json:
    *              schema:
-   *                properties:
-   *                  meta:
-   *                    type: object
-   *                    description: the meta data of the uploaded file
-   *                  fileName:
-   *                    type: string
-   *                    description: the base name of the uploaded file
-   *                  fileStats:
-   *                    type: array
-   *                    items:
-   *                      type: object
-   *                      description: the property of each extracted file
+   *                $ref: '#/components/schemas/FileImportResponse'
    */
   router.post('/upload', accessTokenParser, loginRequired, adminRequired, uploads.single('file'), addActivity, async(req, res) => {
     const { file } = req;
@@ -354,6 +439,8 @@ export default function route(crowi) {
    *  /import/all:
    *    delete:
    *      tags: [Import]
+   *      security:
+   *        - api_key: []
    *      operationId: deleteImportAll
    *      summary: /import/all
    *      description: Delete all zip files

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

@@ -13,6 +13,78 @@ import type { ApiV3Response } from './interfaces/apiv3-response';
 
 const router = express.Router();
 
+/**
+ * @swagger
+ * components:
+ *   schemas:
+ *     InAppNotificationListResponse:
+ *       type: object
+ *       properties:
+ *         docs:
+ *           type: array
+ *           items:
+ *             $ref: '#/components/schemas/InAppNotificationDocument'
+ *         totalDocs:
+ *           type: integer
+ *           description: Total number of in app notification documents
+ *         offset:
+ *           type: integer
+ *           description: Offset value
+ *         limit:
+ *           type: integer
+ *           description: Limit per page
+ *         totalPages:
+ *           type: integer
+ *           description: Total pages available
+ *         page:
+ *           type: integer
+ *           description: Current page number
+ *         hasPrevPage:
+ *           type: boolean
+ *           description: Indicator for previous page
+ *         hasNextPage:
+ *           type: boolean
+ *           description: Indicator for next page
+ *         prevPage:
+ *           type: string
+ *           description: Previous page number or null
+ *         nextPage:
+ *           type: string
+ *           description: Next page number or null
+ *     InAppNotificationDocument:
+ *       type: object
+ *       properties:
+ *         _id:
+ *           type: string
+ *           description: In app notification document ID
+ *         action:
+ *           type: string
+ *           description: Action performed on the in app notification document
+ *         snapshot:
+ *           type: string
+ *           description: Snapshot details in JSON format
+ *         target:
+ *           $ref: '#/components/schemas/Page'
+ *         user:
+ *           $ref: '#/components/schemas/User'
+ *         createdAt:
+ *           type: string
+ *           format: date-time
+ *           description: Creation timestamp
+ *         status:
+ *           type: string
+ *           description: Status of the in app notification document
+ *         targetModel:
+ *           type: string
+ *           description: Model of the target
+ *         id:
+ *           type: string
+ *           description: In app notification document ID
+ *         actionUsers:
+ *           type: array
+ *           items:
+ *             $ref: '#/components/schemas/User'
+ */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
@@ -24,6 +96,41 @@ module.exports = (crowi) => {
 
   const activityEvent = crowi.event('activity');
 
+  /**
+   * @swagger
+   *
+   *  /in-app-notification/list:
+   *    get:
+   *      tags: [NotificationSetting]
+   *      security:
+   *        - api_key: []
+   *      operationId: getInAppNotificationList
+   *      summary: /in-app-notification/list
+   *      description: Get the list of in-app notifications
+   *      parameters:
+   *        - name: limit
+   *          in: query
+   *          description: The number of notifications to get
+   *          schema:
+   *            type: integer
+   *        - name: offset
+   *          in: query
+   *          description: The number of notifications to skip
+   *          schema:
+   *            type: integer
+   *        - name: status
+   *          in: query
+   *          description: The status to categorize. 'UNOPENED' or 'OPENED'.
+   *          schema:
+   *            type: string
+   *      responses:
+   *        200:
+   *          description: The list of in-app notifications
+   *          content:
+   *            application/json:
+   *              schema:
+   *                $ref: '#/components/schemas/InAppNotificationListResponse'
+   */
   router.get('/list', accessTokenParser, loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -79,6 +186,28 @@ module.exports = (crowi) => {
     return res.apiv3(serializedPaginationResult);
   });
 
+  /**
+   * @swagger
+   *
+   *  /in-app-notification/status:
+   *    get:
+   *      tags: [NotificationSetting]
+   *      security:
+   *        - api_key: []
+   *      operationId: getInAppNotificationStatus
+   *      summary: /in-app-notification/status
+   *      description: Get the status of in-app notifications
+   *      responses:
+   *        200:
+   *          description: Get count of unread notifications
+   *          content:
+   *            application/json:
+   *              schema:
+   *                properties:
+   *                  count:
+   *                    type: integer
+   *                    description: Count of unread notifications
+   */
   router.get('/status', accessTokenParser, loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -93,6 +222,35 @@ module.exports = (crowi) => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   *  /in-app-notification/open:
+   *    post:
+   *      tags: [NotificationSetting]
+   *      security:
+   *        - api_key: []
+   *      operationId: openInAppNotification
+   *      summary: /in-app-notification/open
+   *      description: Open the in-app notification
+   *      requestBody:
+   *        content:
+   *          application/json:
+   *            schema:
+   *              properties:
+   *                id:
+   *                  type: string
+   *                  description: Notification ID
+   *              required:
+   *                - id
+   *      responses:
+   *        200:
+   *          description: Notification opened successfully
+   *          content:
+   *            application/json:
+   *              schema:
+   *                type: object
+   */
   router.post('/open', accessTokenParser, loginRequiredStrictly, async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -110,6 +268,21 @@ module.exports = (crowi) => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   *  /in-app-notification/all-statuses-open:
+   *    put:
+   *      tags: [NotificationSetting]
+   *      security:
+   *        - api_key: []
+   *      operationId: openAllInAppNotification
+   *      summary: /in-app-notification/all-statuses-open
+   *      description: Open all in-app notifications
+   *      responses:
+   *        200:
+   *          description: All notifications opened successfully
+   */
   router.put('/all-statuses-open', accessTokenParser, loginRequiredStrictly, addActivity, async(req: CrowiRequest, res: ApiV3Response) => {
     // user must be set by loginRequiredStrictly
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

+ 166 - 1
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -65,7 +65,27 @@ const routerFactory = (crowi: Crowi): Router => {
 
   const router = express.Router();
 
-
+  /**
+   * @swagger
+   *
+   * /page-listing/root:
+   *   get:
+   *     tags: [PageListing]
+   *     security:
+   *       - api_key: []
+   *     summary: /page-listing/root
+   *     description: Get the root page
+   *     responses:
+   *       200:
+   *         description: Success
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 rootPage:
+   *                   $ref: '#/components/schemas/Page'
+   */
   router.get('/root', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const Page = mongoose.model<IPage, PageModel>('Page');
 
@@ -80,6 +100,54 @@ const routerFactory = (crowi: Crowi): Router => {
     return res.apiv3({ rootPage });
   });
 
+  /**
+   * @swagger
+   *
+   * /page-listing/ancestors-children:
+   *   get:
+   *     tags: [PageListing]
+   *     security:
+   *       - api_key: []
+   *     summary: /page-listing/ancestors-children
+   *     description: Get the ancestors and children of a page
+   *     parameters:
+   *       - name: path
+   *         in: query
+   *         required: true
+   *         type: string
+   *     responses:
+   *       200:
+   *         description: Get the ancestors and children of a page
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 ancestorsChildren:
+   *                   type: object
+   *                   additionalProperties:
+   *                     type: object
+   *                     properties:
+   *                       _id:
+   *                         type: string
+   *                         description: Document ID
+   *                       descendantCount:
+   *                         type: integer
+   *                         description: Number of descendants
+   *                       isEmpty:
+   *                         type: boolean
+   *                         description: Indicates if the node is empty
+   *                       grant:
+   *                         type: integer
+   *                         description: Access level
+   *                       path:
+   *                         type: string
+   *                         description: Path string
+   *                       revision:
+   *                         type: string
+   *                         nullable: true
+   *                         description: Revision ID (nullable)
+   */
   // eslint-disable-next-line max-len
   router.get('/ancestors-children', accessTokenParser, loginRequired, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
     const { path } = req.query;
@@ -96,6 +164,36 @@ const routerFactory = (crowi: Crowi): Router => {
 
   });
 
+  /**
+   * @swagger
+   *
+   * /page-listing/children:
+   *   get:
+   *     tags: [PageListing]
+   *     security:
+   *       - api_key: []
+   *     summary: /page-listing/children
+   *     description: Get the children of a page
+   *     parameters:
+   *       - name: id
+   *         in: query
+   *         type: string
+   *       - name: path
+   *         in: query
+   *         type: string
+   *     responses:
+   *       200:
+   *         description: Get the children of a page
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 children:
+   *                   type: array
+   *                   items:
+   *                     $ref: '#/components/schemas/Page'
+   */
   /*
    * In most cases, using id should be prioritized
    */
@@ -120,6 +218,73 @@ const routerFactory = (crowi: Crowi): Router => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   * /page-listing/info:
+   *   get:
+   *     tags: [PageListing]
+   *     security:
+   *       - api_key: []
+   *     summary: /page-listing/info
+   *     description: Get the information of a page
+   *     parameters:
+   *       - name: pageIds
+   *         in: query
+   *         type: array
+   *       - name: path
+   *         in: query
+   *         type: string
+   *       - name: attachBookmarkCount
+   *         in: query
+   *         type: boolean
+   *       - name: attachShortBody
+   *         in: query
+   *         type: boolean
+   *     responses:
+   *       200:
+   *         description: Get the information of a page
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 idToPageInfoMap:
+   *                   type: object
+   *                   additionalProperties:
+   *                     type: object
+   *                     properties:
+   *                       commentCount:
+   *                         type: integer
+   *                       contentAge:
+   *                         type: integer
+   *                       descendantCount:
+   *                         type: integer
+   *                       isAbleToDeleteCompletely:
+   *                         type: boolean
+   *                       isDeletable:
+   *                         type: boolean
+   *                       isEmpty:
+   *                         type: boolean
+   *                       isMovable:
+   *                         type: boolean
+   *                       isRevertible:
+   *                         type: boolean
+   *                       isV5Compatible:
+   *                         type: boolean
+   *                       likerIds:
+   *                         type: array
+   *                         items:
+   *                           type: string
+   *                       seenUserIds:
+   *                         type: array
+   *                         items:
+   *                           type: string
+   *                       sumOfLikers:
+   *                         type: integer
+   *                       sumOfSeenUsers:
+   *                         type: integer
+   */
   // eslint-disable-next-line max-len
   router.get('/info', accessTokenParser, loginRequired, validator.pageIdsOrPathRequired, validator.infoParams, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response) => {
     const {

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

@@ -301,6 +301,29 @@ module.exports = (crowi) => {
     return responseUrl;
   }
 
+  /**
+   * @swagger
+   *
+   * /slack-integration/commands:
+   *   post:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/commands
+   *     description: Handle Slack commands
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *     responses:
+   *       200:
+   *         description: OK
+   *         schema:
+   *           type: string
+   *           example: "No text."
+   */
   router.post('/commands', addSigningSecretToReq, verifySlackRequest, checkCommandsPermission, async(req, res) => {
     const { body } = req;
     const responseUrl = getResponseUrl(req);
@@ -318,6 +341,36 @@ module.exports = (crowi) => {
   });
 
   // when relation test
+  /**
+   * @swagger
+   *
+   * /slack-integration/proxied/verify:
+   *   post:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/proxied/verify
+   *     description: Verify the access token
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             properties:
+   *               type:
+   *                 type: string
+   *               challenge:
+   *                 type: string
+   *     responses:
+   *       200:
+   *         description: OK
+   *         schema:
+   *           type: object
+   *           properties:
+   *             challenge:
+   *               type: string
+   */
   router.post('/proxied/verify', verifyAccessTokenFromProxy, async(req, res) => {
     const { body } = req;
 
@@ -328,6 +381,29 @@ module.exports = (crowi) => {
     }
   });
 
+  /**
+   * @swagger
+   *
+   * /slack-integration/proxied/commands:
+   *   post:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/proxied/commands
+   *     description: Handle Slack commands
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *     responses:
+   *       200:
+   *         description: OK
+   *         schema:
+   *           type: string
+   *           example: "No text."
+   */
   router.post('/proxied/commands', verifyAccessTokenFromProxy, checkCommandsPermission, async(req, res) => {
     const { body } = req;
     const responseUrl = getResponseUrl(req);
@@ -370,17 +446,84 @@ module.exports = (crowi) => {
     }
   }
 
+  /**
+   * @swagger
+   *
+   * /slack-integration/interactions:
+   *   post:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/interactions
+   *     description: Handle Slack interactions
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *     responses:
+   *       200:
+   *         description: OK
+   */
   router.post('/interactions', addSigningSecretToReq, verifySlackRequest, parseSlackInteractionRequest, checkInteractionsPermission, async(req, res) => {
     const client = await slackIntegrationService.generateClientForCustomBotWithoutProxy();
     return handleInteractionsRequest(req, res, client);
   });
 
+  /**
+   * @swagger
+   *
+   * /slack-integration/proxied/interactions:
+   *   post:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/proxied/interactions
+   *     description: Handle Slack interactions
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *     responses:
+   *       200:
+   *         description: OK
+   */
   router.post('/proxied/interactions', verifyAccessTokenFromProxy, parseSlackInteractionRequest, checkInteractionsPermission, async(req, res) => {
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
     const client = await slackIntegrationService.generateClientByTokenPtoG(tokenPtoG);
     return handleInteractionsRequest(req, res, client);
   });
 
+  /**
+   * @swagger
+   *
+   * /slack-integration/supported-commands:
+   *   get:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/supported-commands
+   *     description: Get supported commands
+   *     responses:
+   *       200:
+   *         description: Supported commands
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 permissionsForBroadcastUseCommands:
+   *                   type: array
+   *                   items:
+   *                     type: object
+   *                 permissionsForSingleUseCommands:
+   *                   type: array
+   *                   items:
+   *                     type: object
+   */
   router.get('/supported-commands', verifyAccessTokenFromProxy, async(req, res) => {
     const tokenPtoG = req.headers['x-growi-ptog-tokens'];
     const slackAppIntegration = await SlackAppIntegration.findOne({ tokenPtoG });
@@ -389,6 +532,31 @@ module.exports = (crowi) => {
     return res.apiv3({ permissionsForBroadcastUseCommands, permissionsForSingleUseCommands });
   });
 
+  /**
+   * @swagger
+   *
+   * /slack-integration/events:
+   *   post:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/events
+   *     description: Handle Slack events
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             properties:
+   *               event:
+   *                 type: object
+   *     responses:
+   *       200:
+   *         description: OK
+   *         schema:
+   *           type: object
+   */
   router.post('/events', verifyUrlMiddleware, addSigningSecretToReq, verifySlackRequest, async(req, res) => {
     const { event } = req.body;
 
@@ -419,6 +587,35 @@ module.exports = (crowi) => {
     ],
   };
 
+  /**
+   * @swagger
+   *
+   * /slack-integration/proxied/events:
+   *   post:
+   *     tags: [SlackIntegration]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /slack-integration/proxied/events
+   *     description: Handle Slack events
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             properties:
+   *               growiBotEvent:
+   *                 type: object
+   *               data:
+   *                 type: object
+   *     responses:
+   *       200:
+   *         description: OK
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   */
   router.post('/proxied/events', verifyAccessTokenFromProxy, validator.validateEventRequest, async(req, res) => {
     const { growiBotEvent, data } = req.body;
 

+ 58 - 1
apps/app/src/server/routes/apiv3/staffs.js

@@ -21,9 +21,66 @@ const compareFunction = function(a, b) {
   return a.order - b.order;
 };
 
+/**
+ * @swagger
+ *
+ * components:
+ *   schemas:
+ *     Staff:
+ *       type: object
+ *       properties:
+ *         order:
+ *           type: integer
+ *           example: 1
+ *         sectionName:
+ *           type: string
+ *           example: GROWI VILLAGE
+ *         additionalClass:
+ *           type: string
+ *           example: ""
+ *         memberGroups:
+ *           type: array
+ *           items:
+ *             type: object
+ *             properties:
+ *               additionalClass:
+ *                 type: string
+ *                 example: col-md-12 my-4
+ *               members:
+ *                 type: array
+ *                 items:
+ *                   type: object
+ *                   properties:
+ *                     position:
+ *                       type: string
+ *                       example: Founder
+ *                     name:
+ *                       type: string
+ *                       example: yuki-takei
+ */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
-
+  /**
+   * @swagger
+   *
+   * /staffs:
+   *   get:
+   *     summary: Get staffs
+   *     security: []
+   *     tags: [Staff]
+   *     responses:
+   *       200:
+   *         description: Staffs fetched successfully
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 contributors:
+   *                   type: array
+   *                   items:
+   *                     $ref: '#/components/schemas/Staff'
+   */
   router.get('/', async(req, res) => {
     const now = new Date();
     const growiCloudUri = await crowi.configManager.getConfig('app:growiCloudUri');

+ 45 - 4
apps/app/src/server/routes/apiv3/statistics.js

@@ -16,6 +16,48 @@ const USER_STATUS_MASTER = {
   5: 'invited',
 };
 
+/**
+ * @swagger
+ *   components:
+ *     schemas:
+ *       StatisticsUserResponse:
+ *         type: object
+ *         properties:
+ *           data:
+*             type: object
+*             properties:
+*               total:
+*                 type: integer
+*                 example: 1
+*               active:
+*                 type: object
+*                 properties:
+*                   total:
+*                     type: integer
+*                     example: 1
+*                   admin:
+*                     type: integer
+*                     example: 1
+*               inactive:
+*                 type: object
+*                 properties:
+*                   total:
+*                     type: integer
+*                     example: 0
+*                   registered:
+*                     type: integer
+*                     example: 0
+*                   suspended:
+*                     type: integer
+*                     example: 0
+*                   deleted:
+*                     type: integer
+*                     example: 0
+*                   invited:
+*                     type: integer
+*                     example: 0
+*/
+
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = (crowi) => {
 
@@ -78,6 +120,7 @@ module.exports = (crowi) => {
    *  /statistics/user:
    *    get:
    *      tags: [Statistics]
+   *      security: []
    *      operationId: getStatisticsUser
    *      summary: /statistics/user
    *      description: Get statistics for user
@@ -87,10 +130,8 @@ module.exports = (crowi) => {
    *          content:
    *            application/json:
    *              schema:
-   *                properties:
-   *                  data:
-   *                    type: object
-   *                    description: Statistics for all user
+   *                description: Statistics for all user
+   *                $ref: '#/components/schemas/StatisticsUserResponse'
    */
   router.get('/user', noCache(), async(req, res) => {
     const data = req.user == null ? await getUserStatisticsForNotLoggedIn() : await getUserStatistics();

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

@@ -29,6 +29,8 @@ module.exports = (crowi) => {
    *    /user-group-relations:
    *      get:
    *        tags: [UserGroupRelations]
+   *        security:
+   *          - cookieAuth: []
    *        operationId: listUserGroupRelations
    *        summary: /user-group-relations
    *        description: Gets the user group relations
@@ -42,6 +44,15 @@ module.exports = (crowi) => {
    *                    userGroupRelations:
    *                      type: object
    *                      description: contains arrays user objects related
+   *                      properties:
+   *                        userGroupRelations:
+   *                          type: array
+   *                          items:
+   *                            type: object
+   *                        relationsOfChildGroups:
+   *                          type: array
+   *                          items:
+   *                            type: object
    */
   router.get('/', loginRequiredStrictly, adminRequired, validator.list, async(req, res) => {
     const { query } = req;

+ 45 - 0
apps/app/src/server/routes/apiv3/user-ui-settings.ts

@@ -21,6 +21,51 @@ module.exports = () => {
     body('settings.preferCollapsedModeByUser').optional().isBoolean(),
   ];
 
+  /**
+   * @swagger
+   *
+   * /user-ui-settings:
+   *   put:
+   *     tags: [UserUISettings]
+   *     security:
+   *       - cookieAuth: []
+   *     summary: /user-ui-settings
+   *     description: Update the user's UI settings
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             properties:
+   *               settings:
+   *                 type: object
+   *                 properties:
+   *                   currentSidebarContents:
+   *                     type: string
+   *                   currentProductNavWidth:
+   *                     type: number
+   *                   preferCollapsedModeByUser:
+   *                     type: boolean
+   *     responses:
+   *       200:
+   *         description: The user's UI settings
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 _id:
+   *                   type: string
+   *                 user:
+   *                   type: string
+   *                 __v:
+   *                   type: number
+   *                 currentSidebarContents:
+   *                   type: string
+   *                 preferCollapsedModeByUser:
+   *                   type: boolean
+   */
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   router.put('/', validatorForPut, apiV3FormValidator, async(req: any, res: any) => {
     const { user } = req;

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

@@ -74,6 +74,42 @@ const validator = {};
  *            type: string
  *            description: date created at
  *            example: 2010-01-01T00:00:00.000Z
+ *          imageUrlCached:
+ *            type: string
+ *            description: cached image URL
+ *            example: /images/user/5ae5fccfc5577b0004dbd8ab/profile.jpg
+ *          isEmailPublished:
+ *            type: boolean
+ *            description: whether the email is published
+ *            example: false
+ *          isGravatarEnabled:
+ *            type: boolean
+ *            description: whether the gravatar is enabled
+ *            example: false
+ *          isInvitationEmailSended:
+ *            type: boolean
+ *            description: whether the invitation email is sent
+ *            example: false
+ *          isQuestionnaireEnabled:
+ *            type: boolean
+ *            description: whether the questionnaire is enabled
+ *            example: false
+ *          lastLoginAt:
+ *            type: string
+ *            description: datetime last login at
+ *            example: 2010-01-01T00:00:00.000Z
+ *          readOnly:
+ *            type: boolean
+ *            description: whether the user is read only
+ *            example: false
+ *          updatedAt:
+ *            type: string
+ *            description: datetime updated at
+ *            example: 2010-01-01T00:00:00.000Z
+ *          __v:
+ *            type: integer
+ *            description: DB record version
+ *            example: 0
  */
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */

+ 1 - 1
apps/app/src/server/routes/index.js

@@ -137,7 +137,7 @@ module.exports = function(crowi, app) {
   apiV1Router.post('/comments.update'    , comment.api.validators.add(), accessTokenParser , loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.update);
   apiV1Router.post('/comments.remove'    , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUserIfCommentNotAllowed, addActivity, comment.api.remove);
 
-  apiV1Router.post('/attachments.uploadProfileImage'   , uploads.single('file'), autoReap, accessTokenParser, loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.uploadProfileImage);
+  apiV1Router.post('/attachments.uploadProfileImage'   , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, uploads.single('file'), autoReap, attachmentApi.uploadProfileImage);
   apiV1Router.post('/attachments.remove'               , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, addActivity ,attachmentApi.remove);
   apiV1Router.post('/attachments.removeProfileImage'   , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, attachmentApi.removeProfileImage);
 

+ 68 - 0
apps/app/src/server/routes/login.js

@@ -82,6 +82,38 @@ module.exports = function(crowi, app) {
       return res.apiv3({});
     }
 
+    /**
+     * @swagger
+     *
+     * /login:
+     *   post:
+     *     summary: /login
+     *     tags: [Users]
+     *     requestBody:
+     *       required: true
+     *       content:
+     *         application/json:
+     *           schema:
+     *             type: object
+     *             properties:
+     *               loginForm:
+     *                 type: object
+     *                 properties:
+     *                   username:
+     *                     type: string
+     *                   password:
+     *                     type: string
+     *     responses:
+     *       200:
+     *         description: Login successful
+     *         content:
+     *           application/json:
+     *             schema:
+     *               type: object
+     *               properties:
+     *                 redirectTo:
+     *                   type: string
+     */
     req.login(userData, (err) => {
       if (err) {
         logger.debug(err);
@@ -131,6 +163,42 @@ module.exports = function(crowi, app) {
     next();
   };
 
+  /**
+   * @swagger
+   *
+   * /register:
+   *   post:
+   *     summary: /register
+   *     tags: [Users]
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             properties:
+   *               registerForm:
+   *                 type: object
+   *                 properties:
+   *                   name:
+   *                     type: string
+   *                   username:
+   *                     type: string
+   *                   email:
+   *                     type: string
+   *                   password:
+   *                     type: string
+   *     responses:
+   *       200:
+   *         description: Register successful
+   *         content:
+   *           application/json:
+   *             schema:
+   *               type: object
+   *               properties:
+   *                 redirectTo:
+   *                   type: string
+   */
   actions.register = function(req, res) {
     if (req.user != null) {
       return res.apiv3Err('message.user_already_logged_in', 403);

+ 1 - 1
apps/pdf-converter/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/pdf-converter",
-  "version": "1.0.0-RC.0",
+  "version": "1.0.1-RC.0",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",
   "license": "MIT",

+ 5 - 3
package.json

@@ -96,7 +96,7 @@
     "turbo": "^2.1.3",
     "typescript": "~5.0.0",
     "typescript-transform-paths": "^3.4.7",
-    "vite": "^5.4.12",
+    "vite": "^5.4.15",
     "vite-plugin-dts": "^3.9.1",
     "vite-tsconfig-paths": "^5.0.1",
     "vitest": "^2.1.1",
@@ -104,13 +104,15 @@
     "vue-tsc": "^2.1.10"
   },
   "// comments for pnpm.overrides": {
+    "@lykmapipo/common>flat": "flat v6 is provided only by ESM, but @lykmapipo/common requires CommonJS version",
     "@lykmapipo/common>mime": "mime v4 is provided only by ESM, but @lykmapipo/common requires CommonJS version",
-    "@lykmapipo/common>flat": "flat v6 is provided only by ESM, but @lykmapipo/common requires CommonJS version"
+    "@lykmapipo/common>parse-json": "parse-json v6 is provided only by ESM, but @lykmapipo/common requires CommonJS version"
   },
   "pnpm": {
     "overrides": {
+      "@lykmapipo/common>flat": "5.0.2",
       "@lykmapipo/common>mime": "3.0.0",
-      "@lykmapipo/common>flat": "5.0.2"
+      "@lykmapipo/common>parse-json": "5.2.0"
     }
   },
   "engines": {

+ 12 - 0
packages/pluginkit/CHANGELOG.md

@@ -1,5 +1,17 @@
 # @growi/pluginkit
 
+## 1.1.1
+
+### Patch Changes
+
+- [#9812](https://github.com/weseek/growi/pull/9812) [`fdde5ad`](https://github.com/weseek/growi/commit/fdde5ad90f8324ae5fd6b3ca127b46f1dd8453e0) Thanks [@NaokiHigashi28](https://github.com/NaokiHigashi28)! - Fix growifacade typo
+
+## 1.1.0
+
+### Minor Changes
+
+- [#9775](https://github.com/weseek/growi/pull/9775) [`6b9781d`](https://github.com/weseek/growi/commit/6b9781d76b7037ae1f7cb69df3fa99b3b894c83e) Thanks [@NaokiHigashi28](https://github.com/NaokiHigashi28)! - feat: Add util function to get react instance of GROWI via GrowiFacade
+
 ## 1.0.1
 
 ### Patch Changes

+ 5 - 2
packages/pluginkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/pluginkit",
-  "version": "1.0.1",
+  "version": "1.1.1",
   "description": "Plugin kit for GROWI",
   "license": "MIT",
   "main": "dist/index.cjs",
@@ -21,7 +21,10 @@
     "test": "vitest run --coverage"
   },
   "dependencies": {
-    "@growi/core": "^1.4.0",
+    "@growi/core": "^1.5.0",
     "extensible-custom-error": "^0.0.7"
+  },
+  "devDependencies": {
+    "@types/react": "^18.2.14"
   }
 }

+ 1 - 0
packages/pluginkit/src/index.ts

@@ -1 +1,2 @@
 export * from './model';
+export * from './v4/client';

+ 1 - 0
packages/pluginkit/src/v4/client/index.ts

@@ -0,0 +1 @@
+export * from './utils';

+ 42 - 0
packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.spec.ts

@@ -0,0 +1,42 @@
+import type React from 'react';
+
+import { growiReact } from './growi-react';
+
+describe('growiReact()', () => {
+  const mockReact = { useState: () => {} } as unknown as typeof React;
+  const originalNodeEnv = process.env.NODE_ENV;
+
+  afterEach(() => {
+    process.env.NODE_ENV = originalNodeEnv;
+    delete (global as any).window.growiFacade;
+  });
+
+  it('returns window.growiFacade.react in production mode', () => {
+    // given
+    process.env.NODE_ENV = 'production';
+    const mockProductionReact = { useEffect: () => {} } as unknown as typeof React;
+
+    (global as any).window = {
+      growiFacade: {
+        react: mockProductionReact,
+      },
+    };
+
+    // when
+    const result = growiReact(mockReact);
+
+    // then
+    expect(result).toBe(mockProductionReact);
+  });
+
+  it('returns the given react instance in development mode', () => {
+    // given
+    process.env.NODE_ENV = 'development';
+
+    // when
+    const result = growiReact(mockReact);
+
+    // then
+    expect(result).toBe(mockReact);
+  });
+});

+ 34 - 0
packages/pluginkit/src/v4/client/utils/growi-facade/growi-react.ts

@@ -0,0 +1,34 @@
+import type React from 'react';
+
+import type { GrowiFacade } from '@growi/core';
+
+
+declare global {
+  interface Window {
+    growiFacade: GrowiFacade
+  }
+}
+
+/**
+ * Retrieves the React instance that this package should use.
+ *
+ * - **Production Mode**: Returns the React instance from `window.growiFacade.react`
+ *   to ensure a single shared React instance across the app.
+ * - **Development Mode**: Returns the React instance passed as an argument,
+ *   which allows local development and hot reload without issues.
+ *
+ * @param react - The React instance to use during development
+ * @returns A React instance to be used in the current environment
+ *
+ * @remarks
+ * Using multiple React instances in a single app can cause serious issues,
+ * especially with features like Hooks, which rely on a consistent internal state.
+ * This function ensures that only one React instance is used in production
+ * to avoid such problems.
+ */
+export const growiReact = (react: typeof React): typeof React => {
+  if (process.env.NODE_ENV === 'production') {
+    return window.growiFacade.react as typeof React;
+  }
+  return react as typeof React;
+};

+ 1 - 0
packages/pluginkit/src/v4/client/utils/growi-facade/index.ts

@@ -0,0 +1 @@
+export * from './growi-react';

+ 1 - 0
packages/pluginkit/src/v4/client/utils/index.ts

@@ -0,0 +1 @@
+export * from './growi-facade';

File diff suppressed because it is too large
+ 159 - 237
pnpm-lock.yaml


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