Browse Source

Merge branch 'master' into support/156162-172790-openai-feature-client-dir-biome

Futa Arai 5 months ago
parent
commit
1200001ce4
79 changed files with 2503 additions and 1547 deletions
  1. 7 0
      apps/app/.eslintrc.js
  2. 6 4
      apps/app/src/server/app.ts
  3. 22 11
      apps/app/src/server/crowi/dev.js
  4. 42 23
      apps/app/src/server/crowi/express-init.js
  5. 122 99
      apps/app/src/server/crowi/index.js
  6. 27 10
      apps/app/src/server/crowi/setup-models.ts
  7. 2 2
      apps/app/src/server/events/bookmark.js
  8. 4 4
      apps/app/src/server/events/page.js
  9. 1 1
      apps/app/src/server/events/tag.js
  10. 19 11
      apps/app/src/server/events/user.ts
  11. 8 8
      apps/app/src/server/interfaces/attachment.ts
  12. 54 26
      apps/app/src/server/interfaces/search.ts
  13. 1 1
      apps/app/src/server/interfaces/slack-integration/events.ts
  14. 20 20
      apps/app/src/server/interfaces/slack-integration/link-shared-unfurl.ts
  15. 6 10
      apps/app/src/server/repl.ts
  16. 6 4
      apps/app/src/server/util/apiPaginate.js
  17. 4 6
      apps/app/src/server/util/apiResponse.js
  18. 0 1
      apps/app/src/server/util/batch-stream.js
  19. 4 2
      apps/app/src/server/util/collect-ancestor-paths.ts
  20. 1 2
      apps/app/src/server/util/compare-objectId.spec.ts
  21. 25 14
      apps/app/src/server/util/compare-objectId.ts
  22. 2 3
      apps/app/src/server/util/createApiRouter.ts
  23. 7 6
      apps/app/src/server/util/createGrowiPagesFromImports.js
  24. 4 2
      apps/app/src/server/util/createRedirectToForUnauthenticated.ts
  25. 1 5
      apps/app/src/server/util/formUtil.js
  26. 3 3
      apps/app/src/server/util/getToday.js
  27. 5 4
      apps/app/src/server/util/granted-group.ts
  28. 34 30
      apps/app/src/server/util/importer.js
  29. 13 19
      apps/app/src/server/util/is-simple-request.spec.ts
  30. 9 5
      apps/app/src/server/util/is-simple-request.ts
  31. 12 9
      apps/app/src/server/util/locale-utils.ts
  32. 30 14
      apps/app/src/server/util/mongoose-utils.ts
  33. 0 1
      apps/app/src/server/util/project-dir-utils.ts
  34. 6 6
      apps/app/src/server/util/runtime-versions.ts
  35. 14 6
      apps/app/src/server/util/scope-util.spec.ts
  36. 11 7
      apps/app/src/server/util/scope-utils.ts
  37. 4 2
      apps/app/src/server/util/slack-integration.ts
  38. 15 12
      apps/app/src/server/util/slack-legacy.ts
  39. 55 29
      apps/app/src/server/util/slack.js
  40. 13 4
      apps/app/src/server/util/stream.spec.ts
  41. 5 5
      apps/app/src/server/util/stream.ts
  42. 5 1
      apps/app/src/services/renderer/rehype-plugins/add-class.ts
  43. 3 3
      apps/app/src/services/renderer/rehype-plugins/keyword-highlighter.ts
  44. 0 1
      apps/app/src/services/renderer/rehype-plugins/relative-links-by-pukiwiki-like-linker.spec.ts
  45. 0 1
      apps/app/src/services/renderer/rehype-plugins/relative-links.ts
  46. 14 4
      apps/app/src/stores/activity.ts
  47. 2 3
      apps/app/src/stores/admin/app-settings.tsx
  48. 20 22
      apps/app/src/stores/admin/customize.tsx
  49. 37 28
      apps/app/src/stores/admin/sidebar-config.tsx
  50. 30 20
      apps/app/src/stores/alert.tsx
  51. 47 38
      apps/app/src/stores/attachment.tsx
  52. 7 5
      apps/app/src/stores/bookmark-folder.ts
  53. 25 15
      apps/app/src/stores/bookmark.ts
  54. 51 38
      apps/app/src/stores/comment.tsx
  55. 48 30
      apps/app/src/stores/editor.tsx
  56. 18 13
      apps/app/src/stores/global-notification.ts
  57. 30 25
      apps/app/src/stores/in-app-notification.ts
  58. 14 9
      apps/app/src/stores/maintenanceMode.tsx
  59. 3 2
      apps/app/src/stores/middlewares/user.ts
  60. 436 277
      apps/app/src/stores/modal.tsx
  61. 119 79
      apps/app/src/stores/page-listing.tsx
  62. 3 2
      apps/app/src/stores/page-redirect.tsx
  63. 14 9
      apps/app/src/stores/page-timeline.tsx
  64. 184 107
      apps/app/src/stores/page.tsx
  65. 70 46
      apps/app/src/stores/personal-settings.tsx
  66. 50 28
      apps/app/src/stores/remote-latest-page.ts
  67. 85 37
      apps/app/src/stores/renderer.tsx
  68. 42 35
      apps/app/src/stores/search.tsx
  69. 7 3
      apps/app/src/stores/share-link.tsx
  70. 0 1
      apps/app/src/stores/socket-io.ts
  71. 2 3
      apps/app/src/stores/staff.tsx
  72. 15 4
      apps/app/src/stores/tag.tsx
  73. 287 134
      apps/app/src/stores/ui.tsx
  74. 6 2
      apps/app/src/stores/use-editing-clients.ts
  75. 93 31
      apps/app/src/stores/user-group.tsx
  76. 45 28
      apps/app/src/stores/user.tsx
  77. 21 9
      apps/app/src/stores/websocket.tsx
  78. 42 20
      apps/app/src/stores/yjs.ts
  79. 4 3
      biome.json

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

@@ -48,7 +48,14 @@ module.exports = {
     'src/utils/**',
     'src/utils/**',
     'src/components/**',
     'src/components/**',
     'src/services/**',
     'src/services/**',
+    'src/stores/**',
     'src/pages/**',
     'src/pages/**',
+    'src/server/crowi/**',
+    'src/server/events/**',
+    'src/server/interfaces/**',
+    'src/server/util/**',
+    'src/server/app.ts',
+    'src/server/repl.ts',
   ],
   ],
   settings: {
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript
     // resolve path aliases by eslint-import-resolver-typescript

+ 6 - 4
apps/app/src/server/app.ts

@@ -1,12 +1,15 @@
 import type Logger from 'bunyan';
 import type Logger from 'bunyan';
 
 
-import { initInstrumentation, setupAdditionalResourceAttributes, startOpenTelemetry } from '~/features/opentelemetry/server';
+import {
+  initInstrumentation,
+  setupAdditionalResourceAttributes,
+  startOpenTelemetry,
+} from '~/features/opentelemetry/server';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { hasProcessFlag } from '~/utils/process-utils';
 import { hasProcessFlag } from '~/utils/process-utils';
 
 
 const logger: Logger = loggerFactory('growi');
 const logger: Logger = loggerFactory('growi');
 
 
-
 /** **********************************
 /** **********************************
  *          Main Process
  *          Main Process
  ********************************** */
  ********************************** */
@@ -37,8 +40,7 @@ async function main() {
         process.exit();
         process.exit();
       });
       });
     }
     }
-  }
-  catch (err) {
+  } catch (err) {
     logger.error('An error occurred, unable to start the server');
     logger.error('An error occurred, unable to start the server');
     logger.error(err);
     logger.error(err);
     process.exit(1);
     process.exit(1);

+ 22 - 11
apps/app/src/server/crowi/dev.js

@@ -1,6 +1,5 @@
-import path from 'path';
-
 import express from 'express';
 import express from 'express';
+import path from 'path';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -8,9 +7,7 @@ import nextFactory from '../routes/next';
 
 
 const logger = loggerFactory('growi:crowi:dev');
 const logger = loggerFactory('growi:crowi:dev');
 
 
-
 class CrowiDev {
 class CrowiDev {
-
   /**
   /**
    * @param {import('~/server/crowi').default} crowi Crowi instance
    * @param {import('~/server/crowi').default} crowi Crowi instance
    *
    *
@@ -43,7 +40,9 @@ class CrowiDev {
     let serverUrl = `http://localhost:${port}}`;
     let serverUrl = `http://localhost:${port}}`;
 
 
     if (this.crowi.env.DEV_HTTPS) {
     if (this.crowi.env.DEV_HTTPS) {
-      logger.info(`[${this.crowi.node_env}] Express server will start with HTTPS Self-Signed Certification`);
+      logger.info(
+        `[${this.crowi.node_env}] Express server will start with HTTPS Self-Signed Certification`,
+      );
 
 
       serverUrl = `https://localhost:${port}}`;
       serverUrl = `https://localhost:${port}}`;
 
 
@@ -51,8 +50,12 @@ class CrowiDev {
       const https = require('https');
       const https = require('https');
 
 
       const options = {
       const options = {
-        key: fs.readFileSync(path.join(this.crowi.rootDir, './resource/certs/localhost/key.pem')),
-        cert: fs.readFileSync(path.join(this.crowi.rootDir, './resource/certs/localhost/cert.pem')),
+        key: fs.readFileSync(
+          path.join(this.crowi.rootDir, './resource/certs/localhost/key.pem'),
+        ),
+        cert: fs.readFileSync(
+          path.join(this.crowi.rootDir, './resource/certs/localhost/cert.pem'),
+        ),
       };
       };
 
 
       server = https.createServer(options, app);
       server = https.createServer(options, app);
@@ -64,9 +67,15 @@ class CrowiDev {
     });
     });
 
 
     eazyLogger.info('{bold:Server URLs:}');
     eazyLogger.info('{bold:Server URLs:}');
-    eazyLogger.unprefixed('info', '{grey:=======================================}');
+    eazyLogger.unprefixed(
+      'info',
+      '{grey:=======================================}',
+    );
     eazyLogger.unprefixed('info', `         APP: {magenta:${serverUrl}}`);
     eazyLogger.unprefixed('info', `         APP: {magenta:${serverUrl}}`);
-    eazyLogger.unprefixed('info', '{grey:=======================================}');
+    eazyLogger.unprefixed(
+      'info',
+      '{grey:=======================================}',
+    );
 
 
     return server;
     return server;
   }
   }
@@ -81,14 +90,16 @@ class CrowiDev {
 
 
   setupNextBundleAnalyzer(app) {
   setupNextBundleAnalyzer(app) {
     const next = nextFactory(this.crowi);
     const next = nextFactory(this.crowi);
-    app.use('/analyze', express.static(path.resolve(__dirname, '../../../.next/analyze')));
+    app.use(
+      '/analyze',
+      express.static(path.resolve(__dirname, '../../../.next/analyze')),
+    );
   }
   }
 
 
   setupNextjsStackFrame(app) {
   setupNextjsStackFrame(app) {
     const next = nextFactory(this.crowi);
     const next = nextFactory(this.crowi);
     app.get('/__nextjs_original-stack-frame', next.delegateToNext);
     app.get('/__nextjs_original-stack-frame', next.delegateToNext);
   }
   }
-
 }
 }
 
 
 module.exports = CrowiDev;
 module.exports = CrowiDev;

+ 42 - 23
apps/app/src/server/crowi/express-init.js

@@ -2,18 +2,20 @@ import { themesRootPath as presetThemesRootPath } from '@growi/preset-themes';
 import csrf from 'csurf';
 import csrf from 'csurf';
 import qs from 'qs';
 import qs from 'qs';
 
 
+import { resolveFromRoot } from '~/server/util/project-dir-utils';
 
 
-import { PLUGIN_EXPRESS_STATIC_DIR, PLUGIN_STORING_PATH } from '../../features/growi-plugin/server/consts';
+import {
+  PLUGIN_EXPRESS_STATIC_DIR,
+  PLUGIN_STORING_PATH,
+} from '../../features/growi-plugin/server/consts';
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
-import { resolveFromRoot } from '~/server/util/project-dir-utils';
 import CertifyOrigin from '../middlewares/certify-origin';
 import CertifyOrigin from '../middlewares/certify-origin';
-
 import registerSafeRedirectFactory from '../middlewares/safe-redirect';
 import registerSafeRedirectFactory from '../middlewares/safe-redirect';
 
 
 const logger = loggerFactory('growi:crowi:express-init');
 const logger = loggerFactory('growi:crowi:express-init');
 
 
 /** @param {import('./index').default} crowi Crowi instance */
 /** @param {import('./index').default} crowi Crowi instance */
-module.exports = function(crowi, app) {
+module.exports = (crowi, app) => {
   const express = require('express');
   const express = require('express');
   const compression = require('compression');
   const compression = require('compression');
   const helmet = require('helmet');
   const helmet = require('helmet');
@@ -26,19 +28,20 @@ module.exports = function(crowi, app) {
   const mongoSanitize = require('express-mongo-sanitize');
   const mongoSanitize = require('express-mongo-sanitize');
 
 
   const registerSafeRedirect = registerSafeRedirectFactory();
   const registerSafeRedirect = registerSafeRedirectFactory();
-  const injectCurrentuserToLocalvars = require('../middlewares/inject-currentuser-to-localvars')();
-  const autoReconnectToS2sMsgServer = require('../middlewares/auto-reconnect-to-s2s-msg-server')(crowi);
+  const injectCurrentuserToLocalvars =
+    require('../middlewares/inject-currentuser-to-localvars')();
+  const autoReconnectToS2sMsgServer =
+    require('../middlewares/auto-reconnect-to-s2s-msg-server')(crowi);
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
 
 
   const env = crowi.node_env;
   const env = crowi.node_env;
 
 
   // see: https://qiita.com/nazomikan/items/9458d591a4831480098d
   // see: https://qiita.com/nazomikan/items/9458d591a4831480098d
   // Cannot set a custom query parser after app.use() has been called: https://github.com/expressjs/express/issues/3454
   // Cannot set a custom query parser after app.use() has been called: https://github.com/expressjs/express/issues/3454
-  app.set('query parser', str => qs.parse(str, { arrayLimit: Infinity }));
+  app.set('query parser', (str) => qs.parse(str, { arrayLimit: Infinity }));
 
 
   app.use(compression());
   app.use(compression());
 
 
-
   const { configManager } = crowi;
   const { configManager } = crowi;
 
 
   const trustProxyBool = configManager.getConfig('security:trustProxyBool');
   const trustProxyBool = configManager.getConfig('security:trustProxyBool');
@@ -49,24 +52,30 @@ module.exports = function(crowi, app) {
 
 
   try {
   try {
     if (trustProxy != null) {
     if (trustProxy != null) {
-      const isNotSpec = [trustProxyBool, trustProxyCsv, trustProxyHops].filter(trustProxy => trustProxy != null).length !== 1;
+      const isNotSpec =
+        [trustProxyBool, trustProxyCsv, trustProxyHops].filter(
+          (trustProxy) => trustProxy != null,
+        ).length !== 1;
       if (isNotSpec) {
       if (isNotSpec) {
         // eslint-disable-next-line max-len
         // eslint-disable-next-line max-len
-        logger.warn(`If more than one TRUST_PROXY_ ~ environment variable is set, the values are set in the following order of inequality size (BOOL > CSV > HOPS) first. Set value: ${trustProxy}`);
+        logger.warn(
+          `If more than one TRUST_PROXY_ ~ environment variable is set, the values are set in the following order of inequality size (BOOL > CSV > HOPS) first. Set value: ${trustProxy}`,
+        );
       }
       }
       app.set('trust proxy', trustProxy);
       app.set('trust proxy', trustProxy);
     }
     }
-  }
-  catch (err) {
+  } catch (err) {
     logger.error(err);
     logger.error(err);
   }
   }
 
 
-  app.use(helmet({
-    contentSecurityPolicy: false,
-    expectCt: false,
-    referrerPolicy: false,
-    permittedCrossDomainPolicies: false,
-  }));
+  app.use(
+    helmet({
+      contentSecurityPolicy: false,
+      expectCt: false,
+      referrerPolicy: false,
+      permittedCrossDomainPolicies: false,
+    }),
+  );
 
 
   app.use((req, res, next) => {
   app.use((req, res, next) => {
     const now = new Date();
     const now = new Date();
@@ -83,11 +92,16 @@ module.exports = function(crowi, app) {
 
 
   app.set('port', crowi.port);
   app.set('port', crowi.port);
 
 
-  const staticOption = (crowi.node_env === 'production') ? { maxAge: '30d' } : {};
+  const staticOption = crowi.node_env === 'production' ? { maxAge: '30d' } : {};
   app.use(express.static(crowi.publicDir, staticOption));
   app.use(express.static(crowi.publicDir, staticOption));
-  app.use('/static/preset-themes', express.static(
-    resolveFromRoot(`node_modules/@growi/preset-themes/${presetThemesRootPath}`),
-  ));
+  app.use(
+    '/static/preset-themes',
+    express.static(
+      resolveFromRoot(
+        `node_modules/@growi/preset-themes/${presetThemesRootPath}`,
+      ),
+    ),
+  );
   app.use(PLUGIN_EXPRESS_STATIC_DIR, express.static(PLUGIN_STORING_PATH));
   app.use(PLUGIN_EXPRESS_STATIC_DIR, express.static(PLUGIN_STORING_PATH));
 
 
   app.use(methodOverride());
   app.use(methodOverride());
@@ -122,7 +136,12 @@ module.exports = function(crowi, app) {
 
 
   // csurf should be initialized after express-session
   // csurf should be initialized after express-session
   // default methods + PUT. See: https://expressjs.com/en/resources/middleware/csurf.html#ignoremethods
   // default methods + PUT. See: https://expressjs.com/en/resources/middleware/csurf.html#ignoremethods
-  app.use(csrf({ ignoreMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'DELETE'], cookie: false }));
+  app.use(
+    csrf({
+      ignoreMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'DELETE'],
+      cookie: false,
+    }),
+  );
 
 
   app.use('/_api', CertifyOrigin);
   app.use('/_api', CertifyOrigin);
 
 

+ 122 - 99
apps/app/src/server/crowi/index.js

@@ -1,12 +1,12 @@
 /* eslint-disable @typescript-eslint/no-this-alias */
 /* eslint-disable @typescript-eslint/no-this-alias */
-import http from 'http';
-import path from 'path';
 
 
+import next from 'next';
 import { createTerminus } from '@godaddy/terminus';
 import { createTerminus } from '@godaddy/terminus';
 import attachmentRoutes from '@growi/remark-attachment-refs/dist/server';
 import attachmentRoutes from '@growi/remark-attachment-refs/dist/server';
 import lsxRoutes from '@growi/remark-lsx/dist/server/index.cjs';
 import lsxRoutes from '@growi/remark-lsx/dist/server/index.cjs';
+import http from 'http';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
-import next from 'next';
+import path from 'path';
 
 
 import { KeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
 import { KeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
 import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
@@ -31,7 +31,10 @@ import { configManager as configManagerSingletonInstance } from '../service/conf
 import instanciateExportService from '../service/export';
 import instanciateExportService from '../service/export';
 import instanciateExternalAccountService from '../service/external-account';
 import instanciateExternalAccountService from '../service/external-account';
 import { FileUploader, getUploader } from '../service/file-uploader'; // eslint-disable-line no-unused-vars
 import { FileUploader, getUploader } from '../service/file-uploader'; // eslint-disable-line no-unused-vars
-import { G2GTransferPusherService, G2GTransferReceiverService } from '../service/g2g-transfer';
+import {
+  G2GTransferPusherService,
+  G2GTransferReceiverService,
+} from '../service/g2g-transfer';
 import { GrowiBridgeService } from '../service/growi-bridge';
 import { GrowiBridgeService } from '../service/growi-bridge';
 import { initializeImportService } from '../service/import';
 import { initializeImportService } from '../service/import';
 import { InstallerService } from '../service/installer';
 import { InstallerService } from '../service/installer';
@@ -46,18 +49,19 @@ import { SocketIoService } from '../service/socket-io';
 import UserGroupService from '../service/user-group';
 import UserGroupService from '../service/user-group';
 import { UserNotificationService } from '../service/user-notification';
 import { UserNotificationService } from '../service/user-notification';
 import { initializeYjsService } from '../service/yjs';
 import { initializeYjsService } from '../service/yjs';
-import { getModelSafely, getMongoUri, mongoOptions } from '../util/mongoose-utils';
-
+import {
+  getModelSafely,
+  getMongoUri,
+  mongoOptions,
+} from '../util/mongoose-utils';
 import { setupModelsDependentOnCrowi } from './setup-models';
 import { setupModelsDependentOnCrowi } from './setup-models';
 
 
-
 const logger = loggerFactory('growi:crowi');
 const logger = loggerFactory('growi:crowi');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 
 
 const sep = path.sep;
 const sep = path.sep;
 
 
 class Crowi {
 class Crowi {
-
   /**
   /**
    * For retrieving other packages
    * For retrieving other packages
    * @type {import('~/server/middlewares/access-token-parser').AccessTokenParser}
    * @type {import('~/server/middlewares/access-token-parser').AccessTokenParser}
@@ -167,10 +171,9 @@ class Crowi {
       admin: new (require('../events/admin'))(this),
       admin: new (require('../events/admin'))(this),
     };
     };
   }
   }
-
 }
 }
 
 
-Crowi.prototype.init = async function() {
+Crowi.prototype.init = async function () {
   await this.setupDatabase();
   await this.setupDatabase();
   this.models = await setupModelsDependentOnCrowi(this);
   this.models = await setupModelsDependentOnCrowi(this);
   await this.setupConfigManager();
   await this.setupConfigManager();
@@ -183,10 +186,7 @@ Crowi.prototype.init = async function() {
   // customizeService depends on AppService
   // customizeService depends on AppService
   // passportService depends on appService
   // passportService depends on appService
   // export and import depends on setUpGrowiBridge
   // export and import depends on setUpGrowiBridge
-  await Promise.all([
-    this.setUpApp(),
-    this.setUpGrowiBridge(),
-  ]);
+  await Promise.all([this.setUpApp(), this.setUpGrowiBridge()]);
 
 
   await Promise.all([
   await Promise.all([
     this.setupGrowiInfoService(),
     this.setupGrowiInfoService(),
@@ -232,14 +232,13 @@ Crowi.prototype.init = async function() {
 /**
 /**
  * Execute functions that should be run after the express server is ready.
  * Execute functions that should be run after the express server is ready.
  */
  */
-Crowi.prototype.asyncAfterExpressServerReady = async function() {
+Crowi.prototype.asyncAfterExpressServerReady = async function () {
   if (this.pageOperationService != null) {
   if (this.pageOperationService != null) {
     await this.pageOperationService.afterExpressServerReady();
     await this.pageOperationService.afterExpressServerReady();
   }
   }
 };
 };
 
 
-
-Crowi.prototype.isPageId = function(pageId) {
+Crowi.prototype.isPageId = (pageId) => {
   if (!pageId) {
   if (!pageId) {
     return false;
     return false;
   }
   }
@@ -251,15 +250,15 @@ Crowi.prototype.isPageId = function(pageId) {
   return false;
   return false;
 };
 };
 
 
-Crowi.prototype.setConfig = function(config) {
+Crowi.prototype.setConfig = function (config) {
   this.config = config;
   this.config = config;
 };
 };
 
 
-Crowi.prototype.getConfig = function() {
+Crowi.prototype.getConfig = function () {
   return this.config;
   return this.config;
 };
 };
 
 
-Crowi.prototype.getEnv = function() {
+Crowi.prototype.getEnv = function () {
   return this.env;
   return this.env;
 };
 };
 
 
@@ -268,12 +267,10 @@ Crowi.prototype.getEnv = function() {
  * @param {string} modelName
  * @param {string} modelName
  * @returns {mongoose.Model}
  * @returns {mongoose.Model}
  */
  */
-Crowi.prototype.model = function(modelName) {
-  return getModelSafely(modelName);
-};
+Crowi.prototype.model = (modelName) => getModelSafely(modelName);
 
 
 // getter/setter of event instance
 // getter/setter of event instance
-Crowi.prototype.event = function(name, event) {
+Crowi.prototype.event = function (name, event) {
   if (event) {
   if (event) {
     this.events[name] = event;
     this.events[name] = event;
   }
   }
@@ -281,7 +278,7 @@ Crowi.prototype.event = function(name, event) {
   return this.events[name];
   return this.events[name];
 };
 };
 
 
-Crowi.prototype.setupDatabase = function() {
+Crowi.prototype.setupDatabase = () => {
   mongoose.Promise = global.Promise;
   mongoose.Promise = global.Promise;
 
 
   // mongoUri = mongodb://user:password@host/dbname
   // mongoUri = mongodb://user:password@host/dbname
@@ -290,10 +287,12 @@ Crowi.prototype.setupDatabase = function() {
   return mongoose.connect(mongoUri, mongoOptions);
   return mongoose.connect(mongoUri, mongoOptions);
 };
 };
 
 
-Crowi.prototype.setupSessionConfig = async function() {
+Crowi.prototype.setupSessionConfig = async function () {
   const session = require('express-session');
   const session = require('express-session');
-  const sessionMaxAge = this.configManager.getConfig('security:sessionMaxAge') || 2592000000; // default: 30days
-  const redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null;
+  const sessionMaxAge =
+    this.configManager.getConfig('security:sessionMaxAge') || 2592000000; // default: 30days
+  const redisUrl =
+    this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null;
   const uid = require('uid-safe').sync;
   const uid = require('uid-safe').sync;
 
 
   // generate pre-defined uid for healthcheck
   // generate pre-defined uid for healthcheck
@@ -330,18 +329,20 @@ Crowi.prototype.setupSessionConfig = async function() {
   // use MongoDB for session store
   // use MongoDB for session store
   else {
   else {
     const MongoStore = require('connect-mongo');
     const MongoStore = require('connect-mongo');
-    sessionConfig.store = MongoStore.create({ client: mongoose.connection.getClient() });
+    sessionConfig.store = MongoStore.create({
+      client: mongoose.connection.getClient(),
+    });
   }
   }
 
 
   this.sessionConfig = sessionConfig;
   this.sessionConfig = sessionConfig;
 };
 };
 
 
-Crowi.prototype.setupConfigManager = async function() {
+Crowi.prototype.setupConfigManager = async function () {
   this.configManager = configManagerSingletonInstance;
   this.configManager = configManagerSingletonInstance;
   return this.configManager.loadConfigs();
   return this.configManager.loadConfigs();
 };
 };
 
 
-Crowi.prototype.setupS2sMessagingService = async function() {
+Crowi.prototype.setupS2sMessagingService = async function () {
   const s2sMessagingService = require('../service/s2s-messaging')(this);
   const s2sMessagingService = require('../service/s2s-messaging')(this);
   if (s2sMessagingService != null) {
   if (s2sMessagingService != null) {
     s2sMessagingService.subscribe();
     s2sMessagingService.subscribe();
@@ -353,11 +354,11 @@ Crowi.prototype.setupS2sMessagingService = async function() {
   }
   }
 };
 };
 
 
-Crowi.prototype.setupSocketIoService = async function() {
+Crowi.prototype.setupSocketIoService = async function () {
   this.socketIoService = new SocketIoService(this);
   this.socketIoService = new SocketIoService(this);
 };
 };
 
 
-Crowi.prototype.setupCron = function() {
+Crowi.prototype.setupCron = function () {
   instanciatePageBulkExportJobCronService(this);
   instanciatePageBulkExportJobCronService(this);
   checkPageBulkExportJobInProgressCronService.startCron();
   checkPageBulkExportJobInProgressCronService.startCron();
 
 
@@ -368,23 +369,23 @@ Crowi.prototype.setupCron = function() {
   startAccessTokenCron();
   startAccessTokenCron();
 };
 };
 
 
-Crowi.prototype.getSlack = function() {
+Crowi.prototype.getSlack = function () {
   return this.slack;
   return this.slack;
 };
 };
 
 
-Crowi.prototype.getSlackLegacy = function() {
+Crowi.prototype.getSlackLegacy = function () {
   return this.slackLegacy;
   return this.slackLegacy;
 };
 };
 
 
-Crowi.prototype.getGlobalNotificationService = function() {
+Crowi.prototype.getGlobalNotificationService = function () {
   return this.globalNotificationService;
   return this.globalNotificationService;
 };
 };
 
 
-Crowi.prototype.getUserNotificationService = function() {
+Crowi.prototype.getUserNotificationService = function () {
   return this.userNotificationService;
   return this.userNotificationService;
 };
 };
 
 
-Crowi.prototype.setupPassport = async function() {
+Crowi.prototype.setupPassport = async function () {
   logger.debug('Passport is enabled');
   logger.debug('Passport is enabled');
 
 
   // initialize service
   // initialize service
@@ -400,8 +401,7 @@ Crowi.prototype.setupPassport = async function() {
     this.passportService.setupStrategyById('oidc');
     this.passportService.setupStrategyById('oidc');
     this.passportService.setupStrategyById('google');
     this.passportService.setupStrategyById('google');
     this.passportService.setupStrategyById('github');
     this.passportService.setupStrategyById('github');
-  }
-  catch (err) {
+  } catch (err) {
     logger.error(err);
     logger.error(err);
   }
   }
 
 
@@ -413,11 +413,11 @@ Crowi.prototype.setupPassport = async function() {
   return Promise.resolve();
   return Promise.resolve();
 };
 };
 
 
-Crowi.prototype.setupSearcher = async function() {
+Crowi.prototype.setupSearcher = async function () {
   this.searchService = new SearchService(this);
   this.searchService = new SearchService(this);
 };
 };
 
 
-Crowi.prototype.setupMailer = async function() {
+Crowi.prototype.setupMailer = async function () {
   const MailService = require('~/server/service/mail');
   const MailService = require('~/server/service/mail');
   this.mailService = new MailService(this);
   this.mailService = new MailService(this);
 
 
@@ -427,7 +427,7 @@ Crowi.prototype.setupMailer = async function() {
   }
   }
 };
 };
 
 
-Crowi.prototype.autoInstall = async function() {
+Crowi.prototype.autoInstall = async function () {
   const isInstalled = this.configManager.getConfig('app:installed');
   const isInstalled = this.configManager.getConfig('app:installed');
   const username = this.configManager.getConfig('autoInstall:adminUsername');
   const username = this.configManager.getConfig('autoInstall:adminUsername');
 
 
@@ -445,27 +445,32 @@ Crowi.prototype.autoInstall = async function() {
     admin: true,
     admin: true,
   };
   };
   const globalLang = this.configManager.getConfig('autoInstall:globalLang');
   const globalLang = this.configManager.getConfig('autoInstall:globalLang');
-  const allowGuestMode = this.configManager.getConfig('autoInstall:allowGuestMode');
+  const allowGuestMode = this.configManager.getConfig(
+    'autoInstall:allowGuestMode',
+  );
   const serverDate = this.configManager.getConfig('autoInstall:serverDate');
   const serverDate = this.configManager.getConfig('autoInstall:serverDate');
 
 
   const installerService = new InstallerService(this);
   const installerService = new InstallerService(this);
 
 
   try {
   try {
-    await installerService.install(firstAdminUserToSave, globalLang ?? 'en_US', {
-      allowGuestMode,
-      serverDate,
-    });
-  }
-  catch (err) {
+    await installerService.install(
+      firstAdminUserToSave,
+      globalLang ?? 'en_US',
+      {
+        allowGuestMode,
+        serverDate,
+      },
+    );
+  } catch (err) {
     logger.warn('Automatic installation failed.', err);
     logger.warn('Automatic installation failed.', err);
   }
   }
 };
 };
 
 
-Crowi.prototype.getTokens = function() {
+Crowi.prototype.getTokens = function () {
   return this.tokens;
   return this.tokens;
 };
 };
 
 
-Crowi.prototype.start = async function() {
+Crowi.prototype.start = async function () {
   const dev = process.env.NODE_ENV !== 'production';
   const dev = process.env.NODE_ENV !== 'production';
 
 
   await this.init();
   await this.init();
@@ -484,7 +489,10 @@ Crowi.prototype.start = async function() {
 
 
   const { express } = this;
   const { express } = this;
 
 
-  const app = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
+  const app =
+    this.node_env === 'development'
+      ? this.crowiDev.setupServer(express)
+      : express;
 
 
   const httpServer = http.createServer(app);
   const httpServer = http.createServer(app);
 
 
@@ -501,7 +509,9 @@ Crowi.prototype.start = async function() {
 
 
   // listen
   // listen
   const serverListening = httpServer.listen(this.port, () => {
   const serverListening = httpServer.listen(this.port, () => {
-    logger.info(`[${this.node_env}] Express server is listening on port ${this.port}`);
+    logger.info(
+      `[${this.node_env}] Express server is listening on port ${this.port}`,
+    );
     if (this.node_env === 'development') {
     if (this.node_env === 'development') {
       this.crowiDev.setupExpressAfterListening(express);
       this.crowiDev.setupExpressAfterListening(express);
     }
     }
@@ -520,7 +530,7 @@ Crowi.prototype.start = async function() {
   return serverListening;
   return serverListening;
 };
 };
 
 
-Crowi.prototype.buildServer = async function() {
+Crowi.prototype.buildServer = async function () {
   const env = this.node_env;
   const env = this.node_env;
   const express = require('express')();
   const express = require('express')();
 
 
@@ -530,10 +540,12 @@ Crowi.prototype.buildServer = async function() {
   if (env === 'production') {
   if (env === 'production') {
     const expressBunyanLogger = require('express-bunyan-logger');
     const expressBunyanLogger = require('express-bunyan-logger');
     const logger = loggerFactory('express');
     const logger = loggerFactory('express');
-    express.use(expressBunyanLogger({
-      logger,
-      excludes: ['*'],
-    }));
+    express.use(
+      expressBunyanLogger({
+        logger,
+        excludes: ['*'],
+      }),
+    );
   }
   }
   // use morgan
   // use morgan
   else {
   else {
@@ -544,22 +556,22 @@ Crowi.prototype.buildServer = async function() {
   this.express = express;
   this.express = express;
 };
 };
 
 
-Crowi.prototype.setupTerminus = function(server) {
+Crowi.prototype.setupTerminus = (server) => {
   createTerminus(server, {
   createTerminus(server, {
     signals: ['SIGINT', 'SIGTERM'],
     signals: ['SIGINT', 'SIGTERM'],
-    onSignal: async() => {
+    onSignal: async () => {
       logger.info('Server is starting cleanup');
       logger.info('Server is starting cleanup');
 
 
       await mongoose.disconnect();
       await mongoose.disconnect();
       return;
       return;
     },
     },
-    onShutdown: async() => {
+    onShutdown: async () => {
       logger.info('Cleanup finished, server is shutting down');
       logger.info('Cleanup finished, server is shutting down');
     },
     },
   });
   });
 };
 };
 
 
-Crowi.prototype.setupRoutesForPlugins = function() {
+Crowi.prototype.setupRoutesForPlugins = function () {
   lsxRoutes(this, this.express);
   lsxRoutes(this, this.express);
   attachmentRoutes(this, this.express);
   attachmentRoutes(this, this.express);
 };
 };
@@ -568,7 +580,7 @@ Crowi.prototype.setupRoutesForPlugins = function() {
  * setup Express Routes
  * setup Express Routes
  * !! this must be at last because it includes '/*' route !!
  * !! this must be at last because it includes '/*' route !!
  */
  */
-Crowi.prototype.setupRoutesAtLast = function() {
+Crowi.prototype.setupRoutesAtLast = function () {
   require('../routes')(this, this.express);
   require('../routes')(this, this.express);
 };
 };
 
 
@@ -576,7 +588,7 @@ Crowi.prototype.setupRoutesAtLast = function() {
  * setup global error handlers
  * setup global error handlers
  * !! this must be after the Routes setup !!
  * !! this must be after the Routes setup !!
  */
  */
-Crowi.prototype.setupGlobalErrorHandlers = function() {
+Crowi.prototype.setupGlobalErrorHandlers = function () {
   this.express.use(httpErrorHandler);
   this.express.use(httpErrorHandler);
 };
 };
 
 
@@ -588,14 +600,12 @@ Crowi.prototype.setupGlobalErrorHandlers = function() {
  *
  *
  * @memberof Crowi
  * @memberof Crowi
  */
  */
-Crowi.prototype.require = function(modulePath) {
-  return require(modulePath);
-};
+Crowi.prototype.require = (modulePath) => require(modulePath);
 
 
 /**
 /**
  * setup GlobalNotificationService
  * setup GlobalNotificationService
  */
  */
-Crowi.prototype.setUpGlobalNotification = async function() {
+Crowi.prototype.setUpGlobalNotification = async function () {
   const GlobalNotificationService = require('../service/global-notification');
   const GlobalNotificationService = require('../service/global-notification');
   if (this.globalNotificationService == null) {
   if (this.globalNotificationService == null) {
     this.globalNotificationService = new GlobalNotificationService(this);
     this.globalNotificationService = new GlobalNotificationService(this);
@@ -605,7 +615,7 @@ Crowi.prototype.setUpGlobalNotification = async function() {
 /**
 /**
  * setup UserNotificationService
  * setup UserNotificationService
  */
  */
-Crowi.prototype.setUpUserNotification = async function() {
+Crowi.prototype.setUpUserNotification = async function () {
   if (this.userNotificationService == null) {
   if (this.userNotificationService == null) {
     this.userNotificationService = new UserNotificationService(this);
     this.userNotificationService = new UserNotificationService(this);
   }
   }
@@ -614,14 +624,14 @@ Crowi.prototype.setUpUserNotification = async function() {
 /**
 /**
  * setup AclService
  * setup AclService
  */
  */
-Crowi.prototype.setUpAcl = async function() {
+Crowi.prototype.setUpAcl = async function () {
   this.aclService = aclServiceSingletonInstance;
   this.aclService = aclServiceSingletonInstance;
 };
 };
 
 
 /**
 /**
  * setup CustomizeService
  * setup CustomizeService
  */
  */
-Crowi.prototype.setUpCustomize = async function() {
+Crowi.prototype.setUpCustomize = async function () {
   const CustomizeService = require('../service/customize');
   const CustomizeService = require('../service/customize');
   if (this.customizeService == null) {
   if (this.customizeService == null) {
     this.customizeService = new CustomizeService(this);
     this.customizeService = new CustomizeService(this);
@@ -639,7 +649,7 @@ Crowi.prototype.setUpCustomize = async function() {
 /**
 /**
  * setup AppService
  * setup AppService
  */
  */
-Crowi.prototype.setUpApp = async function() {
+Crowi.prototype.setUpApp = async function () {
   if (this.appService == null) {
   if (this.appService == null) {
     this.appService = new AppService(this);
     this.appService = new AppService(this);
 
 
@@ -654,7 +664,7 @@ Crowi.prototype.setUpApp = async function() {
 /**
 /**
  * setup FileUploadService
  * setup FileUploadService
  */
  */
-Crowi.prototype.setUpFileUpload = async function(isForceUpdate = false) {
+Crowi.prototype.setUpFileUpload = async function (isForceUpdate = false) {
   if (this.fileUploadService == null || isForceUpdate) {
   if (this.fileUploadService == null || isForceUpdate) {
     this.fileUploadService = getUploader(this);
     this.fileUploadService = getUploader(this);
   }
   }
@@ -663,7 +673,7 @@ Crowi.prototype.setUpFileUpload = async function(isForceUpdate = false) {
 /**
 /**
  * setup FileUploaderSwitchService
  * setup FileUploaderSwitchService
  */
  */
-Crowi.prototype.setUpFileUploaderSwitchService = async function() {
+Crowi.prototype.setUpFileUploaderSwitchService = async function () {
   const FileUploaderSwitchService = require('../service/file-uploader-switch');
   const FileUploaderSwitchService = require('../service/file-uploader-switch');
   this.fileUploaderSwitchService = new FileUploaderSwitchService(this);
   this.fileUploaderSwitchService = new FileUploaderSwitchService(this);
   // add as a message handler
   // add as a message handler
@@ -672,7 +682,7 @@ Crowi.prototype.setUpFileUploaderSwitchService = async function() {
   }
   }
 };
 };
 
 
-Crowi.prototype.setupGrowiInfoService = async function() {
+Crowi.prototype.setupGrowiInfoService = async function () {
   const { growiInfoService } = await import('../service/growi-info');
   const { growiInfoService } = await import('../service/growi-info');
   this.growiInfoService = growiInfoService;
   this.growiInfoService = growiInfoService;
 };
 };
@@ -680,7 +690,7 @@ Crowi.prototype.setupGrowiInfoService = async function() {
 /**
 /**
  * setup AttachmentService
  * setup AttachmentService
  */
  */
-Crowi.prototype.setupAttachmentService = async function() {
+Crowi.prototype.setupAttachmentService = async function () {
   if (this.attachmentService == null) {
   if (this.attachmentService == null) {
     this.attachmentService = new AttachmentService(this);
     this.attachmentService = new AttachmentService(this);
   }
   }
@@ -689,43 +699,45 @@ Crowi.prototype.setupAttachmentService = async function() {
 /**
 /**
  * setup RestQiitaAPIService
  * setup RestQiitaAPIService
  */
  */
-Crowi.prototype.setUpRestQiitaAPI = async function() {
+Crowi.prototype.setUpRestQiitaAPI = async function () {
   const RestQiitaAPIService = require('../service/rest-qiita-API');
   const RestQiitaAPIService = require('../service/rest-qiita-API');
   if (this.restQiitaAPIService == null) {
   if (this.restQiitaAPIService == null) {
     this.restQiitaAPIService = new RestQiitaAPIService(this);
     this.restQiitaAPIService = new RestQiitaAPIService(this);
   }
   }
 };
 };
 
 
-Crowi.prototype.setupUserGroupService = async function() {
+Crowi.prototype.setupUserGroupService = async function () {
   if (this.userGroupService == null) {
   if (this.userGroupService == null) {
     this.userGroupService = new UserGroupService(this);
     this.userGroupService = new UserGroupService(this);
     return this.userGroupService.init();
     return this.userGroupService.init();
   }
   }
 };
 };
 
 
-Crowi.prototype.setUpGrowiBridge = async function() {
+Crowi.prototype.setUpGrowiBridge = async function () {
   if (this.growiBridgeService == null) {
   if (this.growiBridgeService == null) {
     this.growiBridgeService = new GrowiBridgeService(this);
     this.growiBridgeService = new GrowiBridgeService(this);
   }
   }
 };
 };
 
 
-Crowi.prototype.setupExport = async function() {
+Crowi.prototype.setupExport = async function () {
   instanciateExportService(this);
   instanciateExportService(this);
 };
 };
 
 
-Crowi.prototype.setupImport = async function() {
+Crowi.prototype.setupImport = async function () {
   initializeImportService(this);
   initializeImportService(this);
 };
 };
 
 
-Crowi.prototype.setupGrowiPluginService = async function() {
-  const growiPluginService = await import('~/features/growi-plugin/server/services').then(mod => mod.growiPluginService);
+Crowi.prototype.setupGrowiPluginService = async () => {
+  const growiPluginService = await import(
+    '~/features/growi-plugin/server/services'
+  ).then((mod) => mod.growiPluginService);
 
 
   // download plugin repositories, if document exists but there is no repository
   // download plugin repositories, if document exists but there is no repository
   // TODO: Cannot download unless connected to the Internet at setup.
   // TODO: Cannot download unless connected to the Internet at setup.
   await growiPluginService.downloadNotExistPluginRepositories();
   await growiPluginService.downloadNotExistPluginRepositories();
 };
 };
 
 
-Crowi.prototype.setupPageService = async function() {
+Crowi.prototype.setupPageService = async function () {
   if (this.pageGrantService == null) {
   if (this.pageGrantService == null) {
     this.pageGrantService = new PageGrantService(this);
     this.pageGrantService = new PageGrantService(this);
   }
   }
@@ -737,14 +749,14 @@ Crowi.prototype.setupPageService = async function() {
   this.pageOperationService = instanciatePageOperationService(this);
   this.pageOperationService = instanciatePageOperationService(this);
 };
 };
 
 
-Crowi.prototype.setupInAppNotificationService = async function() {
+Crowi.prototype.setupInAppNotificationService = async function () {
   const InAppNotificationService = require('../service/in-app-notification');
   const InAppNotificationService = require('../service/in-app-notification');
   if (this.inAppNotificationService == null) {
   if (this.inAppNotificationService == null) {
     this.inAppNotificationService = new InAppNotificationService(this);
     this.inAppNotificationService = new InAppNotificationService(this);
   }
   }
 };
 };
 
 
-Crowi.prototype.setupActivityService = async function() {
+Crowi.prototype.setupActivityService = async function () {
   const ActivityService = require('../service/activity');
   const ActivityService = require('../service/activity');
   if (this.activityService == null) {
   if (this.activityService == null) {
     this.activityService = new ActivityService(this);
     this.activityService = new ActivityService(this);
@@ -752,17 +764,21 @@ Crowi.prototype.setupActivityService = async function() {
   }
   }
 };
 };
 
 
-Crowi.prototype.setupCommentService = async function() {
+Crowi.prototype.setupCommentService = async function () {
   const CommentService = require('../service/comment');
   const CommentService = require('../service/comment');
   if (this.commentService == null) {
   if (this.commentService == null) {
     this.commentService = new CommentService(this);
     this.commentService = new CommentService(this);
   }
   }
 };
 };
 
 
-Crowi.prototype.setupSyncPageStatusService = async function() {
+Crowi.prototype.setupSyncPageStatusService = async function () {
   const SyncPageStatusService = require('../service/system-events/sync-page-status');
   const SyncPageStatusService = require('../service/system-events/sync-page-status');
   if (this.syncPageStatusService == null) {
   if (this.syncPageStatusService == null) {
-    this.syncPageStatusService = new SyncPageStatusService(this, this.s2sMessagingService, this.socketIoService);
+    this.syncPageStatusService = new SyncPageStatusService(
+      this,
+      this.s2sMessagingService,
+      this.socketIoService,
+    );
 
 
     // add as a message handler
     // add as a message handler
     if (this.s2sMessagingService != null) {
     if (this.s2sMessagingService != null) {
@@ -771,7 +787,7 @@ Crowi.prototype.setupSyncPageStatusService = async function() {
   }
   }
 };
 };
 
 
-Crowi.prototype.setupSlackIntegrationService = async function() {
+Crowi.prototype.setupSlackIntegrationService = async function () {
   if (this.slackIntegrationService == null) {
   if (this.slackIntegrationService == null) {
     this.slackIntegrationService = new SlackIntegrationService(this);
     this.slackIntegrationService = new SlackIntegrationService(this);
   }
   }
@@ -782,7 +798,7 @@ Crowi.prototype.setupSlackIntegrationService = async function() {
   }
   }
 };
 };
 
 
-Crowi.prototype.setupG2GTransferService = async function() {
+Crowi.prototype.setupG2GTransferService = async function () {
   if (this.g2gTransferPusherService == null) {
   if (this.g2gTransferPusherService == null) {
     this.g2gTransferPusherService = new G2GTransferPusherService(this);
     this.g2gTransferPusherService = new G2GTransferPusherService(this);
   }
   }
@@ -792,17 +808,24 @@ Crowi.prototype.setupG2GTransferService = async function() {
 };
 };
 
 
 // execute after setupPassport
 // execute after setupPassport
-Crowi.prototype.setupExternalAccountService = function() {
+Crowi.prototype.setupExternalAccountService = function () {
   instanciateExternalAccountService(this.passportService);
   instanciateExternalAccountService(this.passportService);
 };
 };
 
 
 // execute after setupPassport, s2sMessagingService, socketIoService
 // execute after setupPassport, s2sMessagingService, socketIoService
-Crowi.prototype.setupExternalUserGroupSyncService = function() {
-  this.ldapUserGroupSyncService = new LdapUserGroupSyncService(this.passportService, this.s2sMessagingService, this.socketIoService);
-  this.keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(this.s2sMessagingService, this.socketIoService);
-};
-
-Crowi.prototype.setupOpenaiService = function() {
+Crowi.prototype.setupExternalUserGroupSyncService = function () {
+  this.ldapUserGroupSyncService = new LdapUserGroupSyncService(
+    this.passportService,
+    this.s2sMessagingService,
+    this.socketIoService,
+  );
+  this.keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(
+    this.s2sMessagingService,
+    this.socketIoService,
+  );
+};
+
+Crowi.prototype.setupOpenaiService = function () {
   initializeOpenaiService(this);
   initializeOpenaiService(this);
 };
 };
 
 

+ 27 - 10
apps/app/src/server/crowi/setup-models.ts

@@ -8,27 +8,42 @@ const logger = loggerFactory('growi:crowi:setup-models');
 
 
 export type ModelsMapDependentOnCrowi = {
 export type ModelsMapDependentOnCrowi = {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  [modelName: string]: Model<any>,
-}
+  [modelName: string]: Model<any>;
+};
 
 
-export const setupModelsDependentOnCrowi = async(crowi: Crowi): Promise<ModelsMapDependentOnCrowi> => {
+export const setupModelsDependentOnCrowi = async (
+  crowi: Crowi,
+): Promise<ModelsMapDependentOnCrowi> => {
   const modelsMap: ModelsMapDependentOnCrowi = {};
   const modelsMap: ModelsMapDependentOnCrowi = {};
 
 
   const modelsDependsOnCrowi = {
   const modelsDependsOnCrowi = {
     Page: (await import('../models/page')).default,
     Page: (await import('../models/page')).default,
     User: (await import('../models/user')).default,
     User: (await import('../models/user')).default,
     Bookmark: (await import('../models/bookmark')).default,
     Bookmark: (await import('../models/bookmark')).default,
-    GlobalNotificationSetting: (await import('../models/GlobalNotificationSetting')).default,
-    GlobalNotificationMailSetting: (await import('../models/GlobalNotificationSetting/GlobalNotificationMailSetting')).default,
-    GlobalNotificationSlackSetting: (await import('../models/GlobalNotificationSetting/GlobalNotificationSlackSetting')).default,
-    SlackAppIntegration: (await import('../models/slack-app-integration')).default,
+    GlobalNotificationSetting: (
+      await import('../models/GlobalNotificationSetting')
+    ).default,
+    GlobalNotificationMailSetting: (
+      await import(
+        '../models/GlobalNotificationSetting/GlobalNotificationMailSetting'
+      )
+    ).default,
+    GlobalNotificationSlackSetting: (
+      await import(
+        '../models/GlobalNotificationSetting/GlobalNotificationSlackSetting'
+      )
+    ).default,
+    SlackAppIntegration: (await import('../models/slack-app-integration'))
+      .default,
   };
   };
 
 
   Object.keys(modelsDependsOnCrowi).forEach((modelName) => {
   Object.keys(modelsDependsOnCrowi).forEach((modelName) => {
     const factory = modelsDependsOnCrowi[modelName];
     const factory = modelsDependsOnCrowi[modelName];
 
 
     if (!(factory instanceof Function)) {
     if (!(factory instanceof Function)) {
-      logger.warn(`modelsDependsOnCrowi['${modelName}'] is not a function. skipped.`);
+      logger.warn(
+        `modelsDependsOnCrowi['${modelName}'] is not a function. skipped.`,
+      );
       return;
       return;
     }
     }
 
 
@@ -38,10 +53,12 @@ export const setupModelsDependentOnCrowi = async(crowi: Crowi): Promise<ModelsMa
   return modelsMap;
   return modelsMap;
 };
 };
 
 
-export const setupIndependentModels = async(): Promise<void> => {
+export const setupIndependentModels = async (): Promise<void> => {
   await Promise.all([
   await Promise.all([
     import('~/features/comment/server/models'),
     import('~/features/comment/server/models'),
-    import('~/features/external-user-group/server/models/external-user-group-relation'),
+    import(
+      '~/features/external-user-group/server/models/external-user-group-relation'
+    ),
     import('~/features/external-user-group/server/models/external-user-group'),
     import('~/features/external-user-group/server/models/external-user-group'),
     import('~/features/growi-plugin/server/models'),
     import('~/features/growi-plugin/server/models'),
     import('../models/activity'),
     import('../models/activity'),

+ 2 - 2
apps/app/src/server/events/bookmark.js

@@ -9,7 +9,7 @@ function BookmarkEvent(crowi) {
 }
 }
 util.inherits(BookmarkEvent, events.EventEmitter);
 util.inherits(BookmarkEvent, events.EventEmitter);
 
 
-BookmarkEvent.prototype.onCreate = function(bookmark) {};
-BookmarkEvent.prototype.onDelete = function(bookmark) {};
+BookmarkEvent.prototype.onCreate = (bookmark) => {};
+BookmarkEvent.prototype.onDelete = (bookmark) => {};
 
 
 module.exports = BookmarkEvent;
 module.exports = BookmarkEvent;

+ 4 - 4
apps/app/src/server/events/page.js

@@ -13,16 +13,16 @@ function PageEvent(crowi) {
 }
 }
 util.inherits(PageEvent, events.EventEmitter);
 util.inherits(PageEvent, events.EventEmitter);
 
 
-PageEvent.prototype.onCreate = function(page, user) {
+PageEvent.prototype.onCreate = (page, user) => {
   logger.debug('onCreate event fired');
   logger.debug('onCreate event fired');
 };
 };
-PageEvent.prototype.onUpdate = function(page, user) {
+PageEvent.prototype.onUpdate = (page, user) => {
   logger.debug('onUpdate event fired');
   logger.debug('onUpdate event fired');
 };
 };
-PageEvent.prototype.onCreateMany = function(pages, user) {
+PageEvent.prototype.onCreateMany = (pages, user) => {
   logger.debug('onCreateMany event fired');
   logger.debug('onCreateMany event fired');
 };
 };
-PageEvent.prototype.onAddSeenUsers = function(pages, user) {
+PageEvent.prototype.onAddSeenUsers = (pages, user) => {
   logger.debug('onAddSeenUsers event fired');
   logger.debug('onAddSeenUsers event fired');
 };
 };
 module.exports = PageEvent;
 module.exports = PageEvent;

+ 1 - 1
apps/app/src/server/events/tag.js

@@ -9,6 +9,6 @@ function TagEvent(crowi) {
 }
 }
 util.inherits(TagEvent, events.EventEmitter);
 util.inherits(TagEvent, events.EventEmitter);
 
 
-TagEvent.prototype.onUpdate = function(tag) { };
+TagEvent.prototype.onUpdate = (tag) => {};
 
 
 module.exports = TagEvent;
 module.exports = TagEvent;

+ 19 - 11
apps/app/src/server/events/user.ts

@@ -1,7 +1,6 @@
-import EventEmitter from 'events';
-
 import { getIdStringForRef, type IUserHasId } from '@growi/core';
 import { getIdStringForRef, type IUserHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import EventEmitter from 'events';
 import type { HydratedDocument } from 'mongoose';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
@@ -13,7 +12,6 @@ import { deleteCompletelyUserHomeBySystem } from '../service/page/delete-complet
 const logger = loggerFactory('growi:events:user');
 const logger = loggerFactory('growi:events:user');
 
 
 class UserEvent extends EventEmitter {
 class UserEvent extends EventEmitter {
-
   crowi: any;
   crowi: any;
 
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -23,14 +21,26 @@ class UserEvent extends EventEmitter {
   }
   }
 
 
   async onActivated(user: IUserHasId): Promise<void> {
   async onActivated(user: IUserHasId): Promise<void> {
-    const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
+    const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>(
+      'Page',
+    );
     const userHomepagePath = pagePathUtils.userHomepagePath(user);
     const userHomepagePath = pagePathUtils.userHomepagePath(user);
 
 
     try {
     try {
-      let page: HydratedDocument<PageDocument> | null = await Page.findByPath(userHomepagePath, true);
-
-      if (page != null && page.creator != null && getIdStringForRef(page.creator) !== user._id.toString()) {
-        await deleteCompletelyUserHomeBySystem(userHomepagePath, this.crowi.pageService);
+      let page: HydratedDocument<PageDocument> | null = await Page.findByPath(
+        userHomepagePath,
+        true,
+      );
+
+      if (
+        page != null &&
+        page.creator != null &&
+        getIdStringForRef(page.creator) !== user._id.toString()
+      ) {
+        await deleteCompletelyUserHomeBySystem(
+          userHomepagePath,
+          this.crowi.pageService,
+        );
         page = null;
         page = null;
       }
       }
 
 
@@ -40,12 +50,10 @@ class UserEvent extends EventEmitter {
         await this.crowi.pageService.create(userHomepagePath, body, user, {});
         await this.crowi.pageService.create(userHomepagePath, body, user, {});
         logger.debug('User page created', page);
         logger.debug('User page created', page);
       }
       }
-    }
-    catch (err) {
+    } catch (err) {
       logger.error('Failed to create user page', err);
       logger.error('Failed to create user page', err);
     }
     }
   }
   }
-
 }
 }
 
 
 export default UserEvent;
 export default UserEvent;

+ 8 - 8
apps/app/src/server/interfaces/attachment.ts

@@ -5,12 +5,12 @@ export const AttachmentType = {
   PAGE_BULK_EXPORT: 'PAGE_BULK_EXPORT',
   PAGE_BULK_EXPORT: 'PAGE_BULK_EXPORT',
 } as const;
 } as const;
 
 
-export type AttachmentType = typeof AttachmentType[keyof typeof AttachmentType];
-
+export type AttachmentType =
+  (typeof AttachmentType)[keyof typeof AttachmentType];
 
 
 export type ExpressHttpHeader<Field = string> = {
 export type ExpressHttpHeader<Field = string> = {
-  field: Field,
-  value: string | string[]
+  field: Field;
+  value: string | string[];
 };
 };
 
 
 export type IContentHeaders = {
 export type IContentHeaders = {
@@ -18,18 +18,18 @@ export type IContentHeaders = {
   contentLength?: ExpressHttpHeader<'Content-Length'>;
   contentLength?: ExpressHttpHeader<'Content-Length'>;
   contentSecurityPolicy?: ExpressHttpHeader<'Content-Security-Policy'>;
   contentSecurityPolicy?: ExpressHttpHeader<'Content-Security-Policy'>;
   contentDisposition?: ExpressHttpHeader<'Content-Disposition'>;
   contentDisposition?: ExpressHttpHeader<'Content-Disposition'>;
-}
+};
 
 
 export type RespondOptions = {
 export type RespondOptions = {
-  download?: boolean,
-}
+  download?: boolean;
+};
 
 
 export const ResponseMode = {
 export const ResponseMode = {
   RELAY: 'relay',
   RELAY: 'relay',
   REDIRECT: 'redirect',
   REDIRECT: 'redirect',
   DELEGATE: 'delegate',
   DELEGATE: 'delegate',
 } as const;
 } as const;
-export type ResponseMode = typeof ResponseMode[keyof typeof ResponseMode];
+export type ResponseMode = (typeof ResponseMode)[keyof typeof ResponseMode];
 
 
 export const FilePathOnStoragePrefix = {
 export const FilePathOnStoragePrefix = {
   attachment: 'attachment',
   attachment: 'attachment',

+ 54 - 26
apps/app/src/server/interfaces/search.ts

@@ -2,49 +2,77 @@
 import type { SearchDelegatorName } from '~/interfaces/named-query';
 import type { SearchDelegatorName } from '~/interfaces/named-query';
 import type { ISearchResult } from '~/interfaces/search';
 import type { ISearchResult } from '~/interfaces/search';
 
 
-
 export type QueryTerms = {
 export type QueryTerms = {
-  match: string[],
-  not_match: string[],
-  phrase: string[],
-  not_phrase: string[],
-  prefix: string[],
-  not_prefix: string[],
-  tag: string[],
-  not_tag: string[],
-}
+  match: string[];
+  not_match: string[];
+  phrase: string[];
+  not_phrase: string[];
+  prefix: string[];
+  not_prefix: string[];
+  tag: string[];
+  not_tag: string[];
+};
 
 
-export type ParsedQuery = { queryString: string, terms: QueryTerms, delegatorName?: string }
+export type ParsedQuery = {
+  queryString: string;
+  terms: QueryTerms;
+  delegatorName?: string;
+};
 
 
 export interface SearchQueryParser {
 export interface SearchQueryParser {
-  parseSearchQuery(queryString: string, nqName: string | null): Promise<ParsedQuery>
+  parseSearchQuery(
+    queryString: string,
+    nqName: string | null,
+  ): Promise<ParsedQuery>;
 }
 }
 
 
 export interface SearchResolver {
 export interface SearchResolver {
-  resolve(parsedQuery: ParsedQuery): Promise<[SearchDelegator, SearchableData | null]>
+  resolve(
+    parsedQuery: ParsedQuery,
+  ): Promise<[SearchDelegator, SearchableData | null]>;
 }
 }
 
 
-export interface SearchDelegator<T = unknown, KEY extends AllTermsKey = AllTermsKey, QTERMS = QueryTerms> {
-  name?: SearchDelegatorName
-  search(data: SearchableData | null, user, userGroups, option): Promise<ISearchResult<T>>
-  isTermsNormalized(terms: Partial<QueryTerms>): terms is Partial<QTERMS>,
-  validateTerms(terms: QueryTerms): UnavailableTermsKey<KEY>[],
+export interface SearchDelegator<
+  T = unknown,
+  KEY extends AllTermsKey = AllTermsKey,
+  QTERMS = QueryTerms,
+> {
+  name?: SearchDelegatorName;
+  search(
+    data: SearchableData | null,
+    user,
+    userGroups,
+    option,
+  ): Promise<ISearchResult<T>>;
+  isTermsNormalized(terms: Partial<QueryTerms>): terms is Partial<QTERMS>;
+  validateTerms(terms: QueryTerms): UnavailableTermsKey<KEY>[];
 }
 }
 
 
 export type SearchableData<T = Partial<QueryTerms>> = {
 export type SearchableData<T = Partial<QueryTerms>> = {
-  queryString: string
-  terms: T
-}
+  queryString: string;
+  terms: T;
+};
 
 
 export type UpdateOrInsertPagesOpts = {
 export type UpdateOrInsertPagesOpts = {
-  shouldEmitProgress?: boolean
-  invokeGarbageCollection?: boolean
-}
+  shouldEmitProgress?: boolean;
+  invokeGarbageCollection?: boolean;
+};
 
 
 // Terms Key types
 // Terms Key types
 export type AllTermsKey = keyof QueryTerms;
 export type AllTermsKey = keyof QueryTerms;
-export type UnavailableTermsKey<K extends AllTermsKey> = Exclude<AllTermsKey, K>;
-export type ESTermsKey = 'match' | 'not_match' | 'phrase' | 'not_phrase' | 'prefix' | 'not_prefix' | 'tag' | 'not_tag';
+export type UnavailableTermsKey<K extends AllTermsKey> = Exclude<
+  AllTermsKey,
+  K
+>;
+export type ESTermsKey =
+  | 'match'
+  | 'not_match'
+  | 'phrase'
+  | 'not_phrase'
+  | 'prefix'
+  | 'not_prefix'
+  | 'tag'
+  | 'not_tag';
 export type MongoTermsKey = 'match' | 'not_match' | 'prefix' | 'not_prefix';
 export type MongoTermsKey = 'match' | 'not_match' | 'prefix' | 'not_prefix';
 
 
 // Query Terms types
 // Query Terms types

+ 1 - 1
apps/app/src/server/interfaces/slack-integration/events.ts

@@ -1 +1 @@
-export type EventActionsPermission = Map<string, boolean | string[]>
+export type EventActionsPermission = Map<string, boolean | string[]>;

+ 20 - 20
apps/app/src/server/interfaces/slack-integration/link-shared-unfurl.ts

@@ -1,32 +1,32 @@
 export type PrivateData = {
 export type PrivateData = {
-  isPublic: false,
-  isPermalink: boolean,
-  id: string,
-  path: string,
-}
+  isPublic: false;
+  isPermalink: boolean;
+  id: string;
+  path: string;
+};
 
 
 export type PublicData = {
 export type PublicData = {
-  isPublic: true,
-  isPermalink: boolean,
-  id: string,
-  path: string,
-  pageBody: string,
-  updatedAt: Date,
-  commentCount: number,
-}
+  isPublic: true;
+  isPermalink: boolean;
+  id: string;
+  path: string;
+  pageBody: string;
+  updatedAt: Date;
+  commentCount: number;
+};
 
 
 export type DataForUnfurl = PrivateData | PublicData;
 export type DataForUnfurl = PrivateData | PublicData;
 
 
 export type UnfurlEventLink = {
 export type UnfurlEventLink = {
-  url: string,
-  domain: string,
-}
+  url: string;
+  domain: string;
+};
 
 
 export type UnfurlRequestEvent = {
 export type UnfurlRequestEvent = {
-  channel: string,
+  channel: string;
 
 
   // eslint-disable-next-line camelcase
   // eslint-disable-next-line camelcase
-  message_ts: string,
+  message_ts: string;
 
 
-  links: UnfurlEventLink[],
-}
+  links: UnfurlEventLink[];
+};

+ 6 - 10
apps/app/src/server/repl.ts

@@ -1,32 +1,28 @@
 import type { REPLServer } from 'node:repl';
 import type { REPLServer } from 'node:repl';
 import repl from 'node:repl';
 import repl from 'node:repl';
-
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 
 
 import Crowi from './crowi';
 import Crowi from './crowi';
 
 
-
-const setupMongoose = async(replServer: REPLServer) => {
+const setupMongoose = async (replServer: REPLServer) => {
   mongoose.Promise = global.Promise;
   mongoose.Promise = global.Promise;
 
 
-  await mongoose.connect(getMongoUri(), mongoOptions)
-    .then(() => {
-      replServer.context.db = mongoose.connection.db;
-    });
+  await mongoose.connect(getMongoUri(), mongoOptions).then(() => {
+    replServer.context.db = mongoose.connection.db;
+  });
 
 
   replServer.context.mongoose = mongoose;
   replServer.context.mongoose = mongoose;
 };
 };
 
 
-
-const setupCrowi = async(replServer: REPLServer) => {
+const setupCrowi = async (replServer: REPLServer) => {
   const crowi = new Crowi();
   const crowi = new Crowi();
   await crowi.init();
   await crowi.init();
   replServer.context.crowi = crowi;
   replServer.context.crowi = crowi;
 };
 };
 
 
-const start = async() => {
+const start = async () => {
   const replServer = repl.start({
   const replServer = repl.start({
     prompt: `${process.env.NODE_ENV} > `,
     prompt: `${process.env.NODE_ENV} > `,
     ignoreUndefined: true,
     ignoreUndefined: true,

+ 6 - 4
apps/app/src/server/util/apiPaginate.js

@@ -5,7 +5,7 @@ const OFFSET_DEFAULT = 0;
 
 
 const DEFAULT_MAX_RESULT_WINDOW = 10000;
 const DEFAULT_MAX_RESULT_WINDOW = 10000;
 
 
-const parseIntValue = function(value, defaultValue, maxLimit) {
+const parseIntValue = (value, defaultValue, maxLimit) => {
   if (!value) {
   if (!value) {
     return defaultValue;
     return defaultValue;
   }
   }
@@ -20,19 +20,21 @@ const parseIntValue = function(value, defaultValue, maxLimit) {
 
 
 function ApiPaginate() {}
 function ApiPaginate() {}
 
 
-ApiPaginate.parseOptionsForElasticSearch = function(params) {
+ApiPaginate.parseOptionsForElasticSearch = (params) => {
   const limit = parseIntValue(params.limit, LIMIT_DEFAULT, LIMIT_MAX);
   const limit = parseIntValue(params.limit, LIMIT_DEFAULT, LIMIT_MAX);
   const offset = parseIntValue(params.offset, OFFSET_DEFAULT);
   const offset = parseIntValue(params.offset, OFFSET_DEFAULT);
 
 
   // See https://github.com/crowi/crowi/pull/293
   // See https://github.com/crowi/crowi/pull/293
   if (limit + offset > DEFAULT_MAX_RESULT_WINDOW) {
   if (limit + offset > DEFAULT_MAX_RESULT_WINDOW) {
-    throw new Error(`(limit + offset) must be less than or equal to ${DEFAULT_MAX_RESULT_WINDOW}`);
+    throw new Error(
+      `(limit + offset) must be less than or equal to ${DEFAULT_MAX_RESULT_WINDOW}`,
+    );
   }
   }
 
 
   return { limit, offset };
   return { limit, offset };
 };
 };
 
 
-ApiPaginate.parseOptions = function(params) {
+ApiPaginate.parseOptions = (params) => {
   const limit = parseIntValue(params.limit, LIMIT_DEFAULT, LIMIT_MAX);
   const limit = parseIntValue(params.limit, LIMIT_DEFAULT, LIMIT_MAX);
   const offset = parseIntValue(params.offset, OFFSET_DEFAULT);
   const offset = parseIntValue(params.offset, OFFSET_DEFAULT);
 
 

+ 4 - 6
apps/app/src/server/util/apiResponse.js

@@ -1,7 +1,6 @@
-function ApiResponse() {
-}
+function ApiResponse() {}
 
 
-ApiResponse.error = function(err, code, data) {
+ApiResponse.error = (err, code, data) => {
   const result = {};
   const result = {};
 
 
   result.ok = false;
   result.ok = false;
@@ -10,15 +9,14 @@ ApiResponse.error = function(err, code, data) {
 
 
   if (err instanceof Error) {
   if (err instanceof Error) {
     result.error = err.toString();
     result.error = err.toString();
-  }
-  else {
+  } else {
     result.error = err;
     result.error = err;
   }
   }
 
 
   return result;
   return result;
 };
 };
 
 
-ApiResponse.success = function(data) {
+ApiResponse.success = (data) => {
   const result = data || {};
   const result = data || {};
 
 
   result.ok = true;
   result.ok = true;

+ 0 - 1
apps/app/src/server/util/batch-stream.js

@@ -26,7 +26,6 @@ function createBatchStream(batchSize) {
       }
       }
       callback();
       callback();
     },
     },
-
   });
   });
 }
 }
 
 

+ 4 - 2
apps/app/src/server/util/collect-ancestor-paths.ts

@@ -1,5 +1,4 @@
 import { dirname } from 'node:path';
 import { dirname } from 'node:path';
-
 import { isTopPage } from '@growi/core/dist/utils/page-path-utils';
 import { isTopPage } from '@growi/core/dist/utils/page-path-utils';
 
 
 /**
 /**
@@ -8,7 +7,10 @@ import { isTopPage } from '@growi/core/dist/utils/page-path-utils';
  * @param {string[]} ancestorPaths
  * @param {string[]} ancestorPaths
  * @returns {string[]}
  * @returns {string[]}
  */
  */
-export const collectAncestorPaths = (path: string, ancestorPaths: string[] = []): string[] => {
+export const collectAncestorPaths = (
+  path: string,
+  ancestorPaths: string[] = [],
+): string[] => {
   if (isTopPage(path)) return ancestorPaths;
   if (isTopPage(path)) return ancestorPaths;
 
 
   const parentPath = dirname(path);
   const parentPath = dirname(path);

+ 1 - 2
apps/app/src/server/util/compare-objectId.spec.ts

@@ -38,7 +38,7 @@ describe('Objectid comparison utils', () => {
       });
       });
     });
     });
 
 
-    describe('When arrays don\'t have intersection', () => {
+    describe("When arrays don't have intersection", () => {
       const arr1 = [id1, id2];
       const arr1 = [id1, id2];
       const arr2 = [id3, id4];
       const arr2 = [id3, id4];
 
 
@@ -47,5 +47,4 @@ describe('Objectid comparison utils', () => {
       });
       });
     });
     });
   });
   });
-
 });
 });

+ 25 - 14
apps/app/src/server/util/compare-objectId.ts

@@ -11,11 +11,14 @@ const ObjectId = mongoose.Types.ObjectId;
  * @param potentialSubset array that is potentially a subset of arr
  * @param potentialSubset array that is potentially a subset of arr
  * @returns Whether or not arr includes all elements of potentialSubset
  * @returns Whether or not arr includes all elements of potentialSubset
  */
  */
-export const includesObjectIds = (arr: ObjectIdLike[], potentialSubset: ObjectIdLike[]): boolean => {
-  const _arr = arr.map(i => i.toString());
-  const _potentialSubset = potentialSubset.map(i => i.toString());
+export const includesObjectIds = (
+  arr: ObjectIdLike[],
+  potentialSubset: ObjectIdLike[],
+): boolean => {
+  const _arr = arr.map((i) => i.toString());
+  const _potentialSubset = potentialSubset.map((i) => i.toString());
 
 
-  return _potentialSubset.every(id => _arr.includes(id));
+  return _potentialSubset.every((id) => _arr.includes(id));
 };
 };
 
 
 /**
 /**
@@ -24,11 +27,14 @@ export const includesObjectIds = (arr: ObjectIdLike[], potentialSubset: ObjectId
  * @param arr2 another array with ObjectIds
  * @param arr2 another array with ObjectIds
  * @returns Whether or not arr1 and arr2 have an intersection
  * @returns Whether or not arr1 and arr2 have an intersection
  */
  */
-export const hasIntersection = (arr1: ObjectIdLike[], arr2: ObjectIdLike[]): boolean => {
-  const _arr1 = arr1.map(i => i.toString());
-  const _arr2 = arr2.map(i => i.toString());
+export const hasIntersection = (
+  arr1: ObjectIdLike[],
+  arr2: ObjectIdLike[],
+): boolean => {
+  const _arr1 = arr1.map((i) => i.toString());
+  const _arr2 = arr2.map((i) => i.toString());
 
 
-  return _arr1.some(item => _arr2.includes(item));
+  return _arr1.some((item) => _arr2.includes(item));
 };
 };
 
 
 /**
 /**
@@ -37,19 +43,24 @@ export const hasIntersection = (arr1: ObjectIdLike[], arr2: ObjectIdLike[]): boo
  * @param testIds Array of mongoose.Types.ObjectId
  * @param testIds Array of mongoose.Types.ObjectId
  * @returns Array of mongoose.Types.ObjectId
  * @returns Array of mongoose.Types.ObjectId
  */
  */
-export const excludeTestIdsFromTargetIds = <T extends { toString: any } = IObjectId>(
-  targetIds: T[], testIds: ObjectIdLike[],
+export const excludeTestIdsFromTargetIds = <
+  T extends { toString: any } = IObjectId,
+>(
+  targetIds: T[],
+  testIds: ObjectIdLike[],
 ): T[] => {
 ): T[] => {
   // cast to string
   // cast to string
-  const arr1 = targetIds.map(e => e.toString());
-  const arr2 = testIds.map(e => e.toString());
+  const arr1 = targetIds.map((e) => e.toString());
+  const arr2 = testIds.map((e) => e.toString());
 
 
   // filter
   // filter
-  const excluded = arr1.filter(e => !arr2.includes(e));
+  const excluded = arr1.filter((e) => !arr2.includes(e));
   // cast to ObjectId
   // cast to ObjectId
   const shouldReturnString = (arr: any[]): arr is string[] => {
   const shouldReturnString = (arr: any[]): arr is string[] => {
     return typeof arr[0] === 'string';
     return typeof arr[0] === 'string';
   };
   };
 
 
-  return shouldReturnString(targetIds) ? excluded : excluded.map(e => new ObjectId(e));
+  return shouldReturnString(targetIds)
+    ? excluded
+    : excluded.map((e) => new ObjectId(e));
 };
 };

+ 2 - 3
apps/app/src/server/util/createApiRouter.ts

@@ -1,4 +1,5 @@
 import express, { type Router } from 'express';
 import express, { type Router } from 'express';
+
 import CertifyOrigin from '~/server/middlewares/certify-origin';
 import CertifyOrigin from '~/server/middlewares/certify-origin';
 
 
 function createApiRouter(): Router {
 function createApiRouter(): Router {
@@ -7,6 +8,4 @@ function createApiRouter(): Router {
   return router;
   return router;
 }
 }
 
 
-export {
-  createApiRouter,
-};
+export { createApiRouter };

+ 7 - 6
apps/app/src/server/util/createGrowiPagesFromImports.js

@@ -14,7 +14,7 @@ module.exports = (crowi) => {
    *    user: Object
    *    user: Object
    * }]
    * }]
    */
    */
-  const createGrowiPages = async(pages) => {
+  const createGrowiPages = async (pages) => {
     const promises = [];
     const promises = [];
     const errors = [];
     const errors = [];
 
 
@@ -28,14 +28,15 @@ module.exports = (crowi) => {
 
 
       if (isCreatableName && !isPageNameTaken) {
       if (isCreatableName && !isPageNameTaken) {
         try {
         try {
-          const promise = crowi.pageService.create(path, body, user, { grant: Page.GRANT_PUBLIC, grantUserGroupId: null });
+          const promise = crowi.pageService.create(path, body, user, {
+            grant: Page.GRANT_PUBLIC,
+            grantUserGroupId: null,
+          });
           promises.push(promise);
           promises.push(promise);
-        }
-        catch (err) {
+        } catch (err) {
           errors.push(err);
           errors.push(err);
         }
         }
-      }
-      else {
+      } else {
         if (!isCreatableName) {
         if (!isCreatableName) {
           errors.push(new Error(`${path} is not a creatable name in GROWI`));
           errors.push(new Error(`${path} is not a creatable name in GROWI`));
         }
         }

+ 4 - 2
apps/app/src/server/util/createRedirectToForUnauthenticated.ts

@@ -1,6 +1,8 @@
-import { USER_STATUS, type IUserStatus } from '@growi/core';
+import { type IUserStatus, USER_STATUS } from '@growi/core';
 
 
-export const createRedirectToForUnauthenticated = (userStatus: IUserStatus): string | null => {
+export const createRedirectToForUnauthenticated = (
+  userStatus: IUserStatus,
+): string | null => {
   switch (userStatus) {
   switch (userStatus) {
     case USER_STATUS.REGISTERED:
     case USER_STATUS.REGISTERED:
       return '/login/error/registered';
       return '/login/error/registered';

+ 1 - 5
apps/app/src/server/util/formUtil.js

@@ -1,10 +1,6 @@
-
-
 module.exports = {
 module.exports = {
   normalizeCRLFFilter(value) {
   normalizeCRLFFilter(value) {
-    return value
-      .replace(/\r\n/g, '\n')
-      .replace(/\r/g, '\n');
+    return value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
   },
   },
   stringToArrayFilter(value) {
   stringToArrayFilter(value) {
     if (!value || value === '') {
     if (!value || value === '') {

+ 3 - 3
apps/app/src/server/util/getToday.js

@@ -2,10 +2,10 @@
  * getToday
  * getToday
  */
  */
 
 
-module.exports = function() {
+module.exports = () => {
   const today = new Date();
   const today = new Date();
-  const month = (`0${today.getMonth() + 1}`).slice(-2);
-  const day = (`0${today.getDate()}`).slice(-2);
+  const month = `0${today.getMonth() + 1}`.slice(-2);
+  const day = `0${today.getDate()}`.slice(-2);
   const dateString = `${today.getFullYear()}/${month}/${day}`;
   const dateString = `${today.getFullYear()}/${month}/${day}`;
 
 
   return dateString;
   return dateString;

+ 5 - 4
apps/app/src/server/util/granted-group.ts

@@ -1,8 +1,10 @@
-import { type IGrantedGroup, GroupType } from '@growi/core';
+import { GroupType, type IGrantedGroup } from '@growi/core';
 
 
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
 
 
-export const divideByType = (grantedGroups: IGrantedGroup[] | null): {
+export const divideByType = (
+  grantedGroups: IGrantedGroup[] | null,
+): {
   grantedUserGroups: ObjectIdLike[];
   grantedUserGroups: ObjectIdLike[];
   grantedExternalUserGroups: ObjectIdLike[];
   grantedExternalUserGroups: ObjectIdLike[];
 } => {
 } => {
@@ -17,8 +19,7 @@ export const divideByType = (grantedGroups: IGrantedGroup[] | null): {
     const id = typeof group.item === 'string' ? group.item : group.item._id;
     const id = typeof group.item === 'string' ? group.item : group.item._id;
     if (group.type === GroupType.userGroup) {
     if (group.type === GroupType.userGroup) {
       grantedUserGroups.push(id);
       grantedUserGroups.push(id);
-    }
-    else {
+    } else {
       grantedExternalUserGroups.push(id);
       grantedExternalUserGroups.push(id);
     }
     }
   });
   });

+ 34 - 30
apps/app/src/server/util/importer.js

@@ -55,29 +55,33 @@ module.exports = (crowi) => {
    */
    */
   const importPostsFromEsa = (pageNum, user, errors) => {
   const importPostsFromEsa = (pageNum, user, errors) => {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
-      esaClient.posts({ page: pageNum, per_page: 100 }).then(async(res) => {
-        const nextPage = res.next_page;
-        const postsReceived = res.posts;
-
-        const data = convertEsaDataForGrowi(postsReceived, user);
-        const newErrors = await createGrowiPages(data);
-
-        if (nextPage) {
-          return resolve(importPostsFromEsa(nextPage, user, errors.concat(newErrors)));
-        }
-
-        resolve(errors.concat(newErrors));
-
-      }).catch((err) => {
-        reject(new Error(`error in page ${pageNum}: ${err}`));
-      });
+      esaClient
+        .posts({ page: pageNum, per_page: 100 })
+        .then(async (res) => {
+          const nextPage = res.next_page;
+          const postsReceived = res.posts;
+
+          const data = convertEsaDataForGrowi(postsReceived, user);
+          const newErrors = await createGrowiPages(data);
+
+          if (nextPage) {
+            return resolve(
+              importPostsFromEsa(nextPage, user, errors.concat(newErrors)),
+            );
+          }
+
+          resolve(errors.concat(newErrors));
+        })
+        .catch((err) => {
+          reject(new Error(`error in page ${pageNum}: ${err}`));
+        });
     });
     });
   };
   };
 
 
   /**
   /**
    * Import page data from qiita to GROWI
    * Import page data from qiita to GROWI
    */
    */
-  importer.importDataFromQiita = async(user) => {
+  importer.importDataFromQiita = async (user) => {
     const firstPage = 1;
     const firstPage = 1;
     const errors = await importPostsFromQiita(firstPage, user, []);
     const errors = await importPostsFromQiita(firstPage, user, []);
     return errors;
     return errors;
@@ -87,7 +91,7 @@ module.exports = (crowi) => {
    * post page data from qiita and create GROWI page
    * post page data from qiita and create GROWI page
    * @param {string} pageNum default value is '1'
    * @param {string} pageNum default value is '1'
    */
    */
-  const importPostsFromQiita = async(pageNum, user, errors) => {
+  const importPostsFromQiita = async (pageNum, user, errors) => {
     const perPage = '100';
     const perPage = '100';
     const res = await crowi.restQiitaAPIService.getQiitaPages(pageNum, perPage);
     const res = await crowi.restQiitaAPIService.getQiitaPages(pageNum, perPage);
     const next = pageNum * perPage;
     const next = pageNum * perPage;
@@ -116,11 +120,9 @@ module.exports = (crowi) => {
 
 
       if (category && name) {
       if (category && name) {
         path = `${category}/${name}`;
         path = `${category}/${name}`;
-      }
-      else if (category) {
+      } else if (category) {
         path = category;
         path = category;
-      }
-      else if (name) {
+      } else if (name) {
         path = name;
         path = name;
       }
       }
 
 
@@ -156,14 +158,14 @@ module.exports = (crowi) => {
   /**
   /**
    * Import page data from esa to GROWI
    * Import page data from esa to GROWI
    */
    */
-  importer.testConnectionToEsa = async() => {
+  importer.testConnectionToEsa = async () => {
     await getTeamNameFromEsa();
     await getTeamNameFromEsa();
   };
   };
 
 
   /**
   /**
    * Import page data from qiita to GROWI
    * Import page data from qiita to GROWI
    */
    */
-  importer.testConnectionToQiita = async() => {
+  importer.testConnectionToQiita = async () => {
     await crowi.restQiitaAPIService.getQiitaUser();
     await crowi.restQiitaAPIService.getQiitaUser();
   };
   };
 
 
@@ -173,12 +175,14 @@ module.exports = (crowi) => {
   const getTeamNameFromEsa = () => {
   const getTeamNameFromEsa = () => {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
       const team = configManager.getConfig('importer:esa:team_name');
       const team = configManager.getConfig('importer:esa:team_name');
-      esaClient.team(team).then((res) => {
-        resolve(res);
-      }).catch((err) => {
-        return reject(err);
-      });
-
+      esaClient
+        .team(team)
+        .then((res) => {
+          resolve(res);
+        })
+        .catch((err) => {
+          return reject(err);
+        });
     });
     });
   };
   };
 
 

+ 13 - 19
apps/app/src/server/util/is-simple-request.spec.ts

@@ -4,11 +4,8 @@ import { mock } from 'vitest-mock-extended';
 import isSimpleRequest from './is-simple-request';
 import isSimpleRequest from './is-simple-request';
 
 
 describe('isSimpleRequest', () => {
 describe('isSimpleRequest', () => {
-
-
   // method
   // method
   describe('When request method is checked', () => {
   describe('When request method is checked', () => {
-
     // allow
     // allow
     describe('When allowed method is given', () => {
     describe('When allowed method is given', () => {
       const allowedMethods = ['GET', 'HEAD', 'POST'];
       const allowedMethods = ['GET', 'HEAD', 'POST'];
@@ -30,13 +27,10 @@ describe('isSimpleRequest', () => {
         expect(isSimpleRequest(reqMock)).toBe(false);
         expect(isSimpleRequest(reqMock)).toBe(false);
       });
       });
     });
     });
-
   });
   });
 
 
-
   // headers
   // headers
   describe('When request headers are checked', () => {
   describe('When request headers are checked', () => {
-
     // allow(Other than content-type)
     // allow(Other than content-type)
     describe('When only safe headers are given', () => {
     describe('When only safe headers are given', () => {
       const safeHeaders = [
       const safeHeaders = [
@@ -94,13 +88,16 @@ describe('isSimpleRequest', () => {
         'X-Requested-With',
         'X-Requested-With',
         'X-CSRF-Token',
         'X-CSRF-Token',
       ];
       ];
-      it.each(unsafeHeaders)('returns false for unsafe header: %s', (headerName) => {
-        const reqMock = mock<Request>({
-          method: 'POST',
-          headers: { [headerName]: 'test-value' },
-        });
-        expect(isSimpleRequest(reqMock)).toBe(false);
-      });
+      it.each(unsafeHeaders)(
+        'returns false for unsafe header: %s',
+        (headerName) => {
+          const reqMock = mock<Request>({
+            method: 'POST',
+            headers: { [headerName]: 'test-value' },
+          });
+          expect(isSimpleRequest(reqMock)).toBe(false);
+        },
+      );
       // combination
       // combination
       it('returns false when safe and unsafe headers are mixed', () => {
       it('returns false when safe and unsafe headers are mixed', () => {
         const reqMock = mock<Request>();
         const reqMock = mock<Request>();
@@ -112,13 +109,10 @@ describe('isSimpleRequest', () => {
         expect(isSimpleRequest(reqMock)).toBe(false);
         expect(isSimpleRequest(reqMock)).toBe(false);
       });
       });
     });
     });
-
   });
   });
 
 
-
   // content-type
   // content-type
   describe('When content-type is checked', () => {
   describe('When content-type is checked', () => {
-
     // allow
     // allow
     describe('When a safe content-type is given', () => {
     describe('When a safe content-type is given', () => {
       const safeContentTypes = [
       const safeContentTypes = [
@@ -164,17 +158,17 @@ describe('isSimpleRequest', () => {
         expect(isSimpleRequest(reqMock)).toBe(false);
         expect(isSimpleRequest(reqMock)).toBe(false);
       });
       });
     });
     });
-
   });
   });
 
 
   // integration
   // integration
   describe('When multiple conditions are checked', () => {
   describe('When multiple conditions are checked', () => {
-
     describe('When all conditions are met', () => {
     describe('When all conditions are met', () => {
       it('returns true', () => {
       it('returns true', () => {
         const reqMock = mock<Request>();
         const reqMock = mock<Request>();
         reqMock.method = 'POST';
         reqMock.method = 'POST';
-        reqMock.headers = { 'content-type': 'application/x-www-form-urlencoded' };
+        reqMock.headers = {
+          'content-type': 'application/x-www-form-urlencoded',
+        };
         expect(isSimpleRequest(reqMock)).toBe(true);
         expect(isSimpleRequest(reqMock)).toBe(true);
       });
       });
     });
     });

+ 9 - 5
apps/app/src/server/util/is-simple-request.ts

@@ -3,7 +3,7 @@ import type { Request } from 'express';
 import type { AccessTokenParserReq } from '~/server/middlewares/access-token-parser/interfaces';
 import type { AccessTokenParserReq } from '~/server/middlewares/access-token-parser/interfaces';
 
 
 const allowedMethods = ['GET', 'HEAD', 'POST'] as const;
 const allowedMethods = ['GET', 'HEAD', 'POST'] as const;
-type AllowedMethod = typeof allowedMethods[number];
+type AllowedMethod = (typeof allowedMethods)[number];
 function isAllowedMethod(method: string): method is AllowedMethod {
 function isAllowedMethod(method: string): method is AllowedMethod {
   return allowedMethods.includes(method as AllowedMethod);
   return allowedMethods.includes(method as AllowedMethod);
 }
 }
@@ -21,7 +21,7 @@ const safeRequestHeaders = [
   'viewport-width',
   'viewport-width',
   'width',
   'width',
 ] as const;
 ] as const;
-type SafeRequestHeader = typeof safeRequestHeaders[number];
+type SafeRequestHeader = (typeof safeRequestHeaders)[number];
 
 
 function isSafeRequestHeader(header: string): header is SafeRequestHeader {
 function isSafeRequestHeader(header: string): header is SafeRequestHeader {
   return safeRequestHeaders.includes(header.toLowerCase() as SafeRequestHeader);
   return safeRequestHeaders.includes(header.toLowerCase() as SafeRequestHeader);
@@ -32,10 +32,14 @@ const allowedContentTypes = [
   'multipart/form-data',
   'multipart/form-data',
   'text/plain',
   'text/plain',
 ] as const;
 ] as const;
-type AllowedContentType = typeof allowedContentTypes[number];
+type AllowedContentType = (typeof allowedContentTypes)[number];
 
 
-function isAllowedContentType(contentType: string): contentType is AllowedContentType {
-  return allowedContentTypes.some(allowed => contentType.toLowerCase().startsWith(allowed));
+function isAllowedContentType(
+  contentType: string,
+): contentType is AllowedContentType {
+  return allowedContentTypes.some((allowed) =>
+    contentType.toLowerCase().startsWith(allowed),
+  );
 }
 }
 
 
 const isSimpleRequest = (req: Request | AccessTokenParserReq): boolean => {
 const isSimpleRequest = (req: Request | AccessTokenParserReq): boolean => {

+ 12 - 9
apps/app/src/server/util/locale-utils.ts

@@ -1,6 +1,5 @@
-import type { IncomingHttpHeaders } from 'http';
-
 import { Lang } from '@growi/core/dist/interfaces';
 import { Lang } from '@growi/core/dist/interfaces';
+import type { IncomingHttpHeaders } from 'http';
 
 
 import * as i18nextConfig from '^/config/i18next.config';
 import * as i18nextConfig from '^/config/i18next.config';
 
 
@@ -18,17 +17,21 @@ const ACCEPT_LANG_MAP = {
  */
  */
 const getPreferredLanguage = (sortedAcceptLanguagesArray: string[]): Lang => {
 const getPreferredLanguage = (sortedAcceptLanguagesArray: string[]): Lang => {
   for (const lang of sortedAcceptLanguagesArray) {
   for (const lang of sortedAcceptLanguagesArray) {
-    const matchingLang = Object.keys(ACCEPT_LANG_MAP).find(key => lang.includes(key));
+    const matchingLang = Object.keys(ACCEPT_LANG_MAP).find((key) =>
+      lang.includes(key),
+    );
     if (matchingLang) return ACCEPT_LANG_MAP[matchingLang];
     if (matchingLang) return ACCEPT_LANG_MAP[matchingLang];
   }
   }
   return i18nextConfig.defaultLang;
   return i18nextConfig.defaultLang;
 };
 };
 
 
 /**
 /**
-  * Detect locale from browser accept language
-  * @param headers
-  */
-export const detectLocaleFromBrowserAcceptLanguage = (headers: IncomingHttpHeaders): Lang => {
+ * Detect locale from browser accept language
+ * @param headers
+ */
+export const detectLocaleFromBrowserAcceptLanguage = (
+  headers: IncomingHttpHeaders,
+): Lang => {
   // 1. get the header accept-language
   // 1. get the header accept-language
   // ex. "ja,ar-SA;q=0.8,en;q=0.6,en-CA;q=0.4,en-US;q=0.2"
   // ex. "ja,ar-SA;q=0.8,en;q=0.6,en-CA;q=0.4,en-US;q=0.2"
   const acceptLanguages = headers['accept-language'];
   const acceptLanguages = headers['accept-language'];
@@ -45,7 +48,7 @@ export const detectLocaleFromBrowserAcceptLanguage = (headers: IncomingHttpHeade
   const acceptLanguagesDict = acceptLanguages
   const acceptLanguagesDict = acceptLanguages
     .replace(/\s+/g, '')
     .replace(/\s+/g, '')
     .split(',')
     .split(',')
-    .map(item => item.split(/\s*;\s*q\s*=\s*/))
+    .map((item) => item.split(/\s*;\s*q\s*=\s*/))
     .reduce((acc, [key, value = '1']) => {
     .reduce((acc, [key, value = '1']) => {
       acc[value] = key;
       acc[value] = key;
       return acc;
       return acc;
@@ -55,7 +58,7 @@ export const detectLocaleFromBrowserAcceptLanguage = (headers: IncomingHttpHeade
   // ex. [ 'ja', 'ar-SA', 'en', 'en-CA', 'en-US' ]
   // ex. [ 'ja', 'ar-SA', 'en', 'en-CA', 'en-US' ]
   const sortedAcceptLanguagesArray = Object.keys(acceptLanguagesDict)
   const sortedAcceptLanguagesArray = Object.keys(acceptLanguagesDict)
     .sort((x, y) => y.localeCompare(x))
     .sort((x, y) => y.localeCompare(x))
-    .map(item => acceptLanguagesDict[item]);
+    .map((item) => acceptLanguagesDict[item]);
 
 
   return getPreferredLanguage(sortedAcceptLanguagesArray);
   return getPreferredLanguage(sortedAcceptLanguagesArray);
 };
 };

+ 30 - 14
apps/app/src/server/util/mongoose-utils.ts

@@ -1,33 +1,49 @@
+import type { ConnectOptions, Document, Model } from 'mongoose';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
-import type {
-  Model, Document, ConnectOptions,
-} from 'mongoose';
 
 
 // suppress DeprecationWarning: current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version
 // suppress DeprecationWarning: current Server Discovery and Monitoring engine is deprecated, and will be removed in a future version
 type ConnectionOptionsExtend = {
 type ConnectionOptionsExtend = {
-  useUnifiedTopology: boolean
-}
+  useUnifiedTopology: boolean;
+};
 
 
 export const getMongoUri = (): string => {
 export const getMongoUri = (): string => {
   const { env } = process;
   const { env } = process;
 
 
-  return env.MONGOLAB_URI // for B.C.
-    || env.MONGODB_URI // MONGOLAB changes their env name
-    || env.MONGOHQ_URL
-    || env.MONGO_URI
-    || ((env.NODE_ENV === 'test') ? 'mongodb://mongo/growi_test' : 'mongodb://mongo/growi');
+  return (
+    env.MONGOLAB_URI || // for B.C.
+    env.MONGODB_URI || // MONGOLAB changes their env name
+    env.MONGOHQ_URL ||
+    env.MONGO_URI ||
+    (env.NODE_ENV === 'test'
+      ? 'mongodb://mongo/growi_test'
+      : 'mongodb://mongo/growi')
+  );
 };
 };
 
 
-export const getModelSafely = <Interface, Method = Interface>(modelName: string): Method & Model<Interface & Document> | null => {
+export const getModelSafely = <Interface, Method = Interface>(
+  modelName: string,
+): (Method & Model<Interface & Document>) | null => {
   if (mongoose.modelNames().includes(modelName)) {
   if (mongoose.modelNames().includes(modelName)) {
-    return mongoose.model<Interface & Document, Method & Model<Interface & Document>>(modelName);
+    return mongoose.model<
+      Interface & Document,
+      Method & Model<Interface & Document>
+    >(modelName);
   }
   }
   return null;
   return null;
 };
 };
 
 
 // TODO: Do not use any type
 // TODO: Do not use any type
-export const getOrCreateModel = <Interface, Method>(modelName: string, schema: any): Method & Model<Interface & Document> => {
-  return getModelSafely(modelName) ?? mongoose.model<Interface & Document, Method & Model<Interface & Document>>(modelName, schema);
+export const getOrCreateModel = <Interface, Method>(
+  modelName: string,
+  schema: any,
+): Method & Model<Interface & Document> => {
+  return (
+    getModelSafely(modelName) ??
+    mongoose.model<Interface & Document, Method & Model<Interface & Document>>(
+      modelName,
+      schema,
+    )
+  );
 };
 };
 
 
 // supress deprecation warnings
 // supress deprecation warnings

+ 0 - 1
apps/app/src/server/util/project-dir-utils.ts

@@ -1,7 +1,6 @@
 import fs from 'node:fs';
 import fs from 'node:fs';
 import path from 'node:path';
 import path from 'node:path';
 import process from 'node:process';
 import process from 'node:process';
-
 import { isServer } from '@growi/core/dist/utils/browser-utils';
 import { isServer } from '@growi/core/dist/utils/browser-utils';
 
 
 const isCurrentDirRoot = isServer() && fs.existsSync('./next.config.js');
 const isCurrentDirRoot = isServer() && fs.existsSync('./next.config.js');

+ 6 - 6
apps/app/src/server/util/runtime-versions.ts

@@ -6,19 +6,18 @@ type RuntimeVersions = {
   pnpm: string | undefined;
   pnpm: string | undefined;
 };
 };
 
 
-
 // define original types because the object returned is not according to the official type definition
 // define original types because the object returned is not according to the official type definition
 type SatisfiedVersionInfo = {
 type SatisfiedVersionInfo = {
   isSatisfied: true;
   isSatisfied: true;
   version: {
   version: {
     version: string;
     version: string;
-  }
-}
+  };
+};
 
 
 type NotfoundVersionInfo = {
 type NotfoundVersionInfo = {
   isSatisfied: true;
   isSatisfied: true;
   notfound: true;
   notfound: true;
-}
+};
 
 
 type VersionInfo = SatisfiedVersionInfo | NotfoundVersionInfo;
 type VersionInfo = SatisfiedVersionInfo | NotfoundVersionInfo;
 
 
@@ -26,7 +25,9 @@ function isNotfoundVersionInfo(info: VersionInfo): info is NotfoundVersionInfo {
   return 'notfound' in info;
   return 'notfound' in info;
 }
 }
 
 
-function isSatisfiedVersionInfo(info: VersionInfo): info is SatisfiedVersionInfo {
+function isSatisfiedVersionInfo(
+  info: VersionInfo,
+): info is SatisfiedVersionInfo {
   return 'version' in info;
   return 'version' in info;
 }
 }
 
 
@@ -42,7 +43,6 @@ const getVersion = (versionInfo: VersionInfo): string | undefined => {
   return undefined;
   return undefined;
 };
 };
 
 
-
 export function getRuntimeVersions(): Promise<RuntimeVersions> {
 export function getRuntimeVersions(): Promise<RuntimeVersions> {
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
     checkNodeVersion({}, (error, result) => {
     checkNodeVersion({}, (error, result) => {

+ 14 - 6
apps/app/src/server/util/scope-util.spec.ts

@@ -1,16 +1,20 @@
 import { SCOPE } from '@growi/core/dist/interfaces';
 import { SCOPE } from '@growi/core/dist/interfaces';
-import { describe, it, expect } from 'vitest';
-
+import { describe, expect, it } from 'vitest';
 
 
 import {
 import {
-  isValidScope, hasAllScope, extractAllScope, extractScopes,
+  extractAllScope,
+  extractScopes,
+  hasAllScope,
+  isValidScope,
 } from './scope-utils';
 } from './scope-utils';
 
 
 describe('scope-utils', () => {
 describe('scope-utils', () => {
   describe('isValidScope', () => {
   describe('isValidScope', () => {
     it('should return true for valid scopes', () => {
     it('should return true for valid scopes', () => {
       expect(isValidScope(SCOPE.READ.USER_SETTINGS.API.API_TOKEN)).toBe(true);
       expect(isValidScope(SCOPE.READ.USER_SETTINGS.API.API_TOKEN)).toBe(true);
-      expect(isValidScope(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN)).toBe(true);
+      expect(isValidScope(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN)).toBe(
+        true,
+      );
       expect(isValidScope(SCOPE.READ.ADMIN.APP)).toBe(true);
       expect(isValidScope(SCOPE.READ.ADMIN.APP)).toBe(true);
     });
     });
 
 
@@ -29,7 +33,9 @@ describe('scope-utils', () => {
 
 
     it('should return false for specific scopes', () => {
     it('should return false for specific scopes', () => {
       expect(hasAllScope(SCOPE.READ.USER_SETTINGS.API.API_TOKEN)).toBe(false);
       expect(hasAllScope(SCOPE.READ.USER_SETTINGS.API.API_TOKEN)).toBe(false);
-      expect(hasAllScope(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN)).toBe(false);
+      expect(hasAllScope(SCOPE.WRITE.USER_SETTINGS.API.ACCESS_TOKEN)).toBe(
+        false,
+      );
     });
     });
   });
   });
 
 
@@ -83,7 +89,9 @@ describe('scope-utils', () => {
       ];
       ];
       const extracted = extractScopes(scopes);
       const extracted = extractScopes(scopes);
 
 
-      const accessTokenScopes = extracted.filter(s => s.endsWith('access_token'));
+      const accessTokenScopes = extracted.filter((s) =>
+        s.endsWith('access_token'),
+      );
       expect(accessTokenScopes).toHaveLength(2); // Only READ and WRITE, no duplicates
       expect(accessTokenScopes).toHaveLength(2); // Only READ and WRITE, no duplicates
     });
     });
   });
   });

+ 11 - 7
apps/app/src/server/util/scope-utils.ts

@@ -1,16 +1,21 @@
 import {
 import {
-  ACTION, ALL_SIGN, SCOPE, type Scope,
+  ACTION,
+  ALL_SIGN,
+  SCOPE,
+  type Scope,
 } from '@growi/core/dist/interfaces';
 } from '@growi/core/dist/interfaces';
 
 
 export const isValidScope = (scope: Scope): boolean => {
 export const isValidScope = (scope: Scope): boolean => {
-  const scopeParts = scope.split(':').map(x => (x === ALL_SIGN ? 'ALL' : x.toUpperCase()));
+  const scopeParts = scope
+    .split(':')
+    .map((x) => (x === ALL_SIGN ? 'ALL' : x.toUpperCase()));
   let obj: any = SCOPE;
   let obj: any = SCOPE;
-  scopeParts.forEach((part) => {
+  for (const part of scopeParts) {
     if (obj[part] == null) {
     if (obj[part] == null) {
       return false;
       return false;
     }
     }
     obj = obj[part];
     obj = obj[part];
-  });
+  }
   return obj === scope;
   return obj === scope;
 };
 };
 
 
@@ -60,7 +65,7 @@ export const extractAllScope = (scope: Scope): Scope[] => {
     return [scope];
     return [scope];
   }
   }
   const result = [] as Scope[];
   const result = [] as Scope[];
-  const scopeParts = scope.split(':').map(x => (x.toUpperCase()));
+  const scopeParts = scope.split(':').map((x) => x.toUpperCase());
   let obj: any = SCOPE;
   let obj: any = SCOPE;
   scopeParts.forEach((part) => {
   scopeParts.forEach((part) => {
     if (part === ALL_SIGN) {
     if (part === ALL_SIGN) {
@@ -71,10 +76,9 @@ export const extractAllScope = (scope: Scope): Scope[] => {
   getAllScopeValuesFromObj(obj).forEach((value) => {
   getAllScopeValuesFromObj(obj).forEach((value) => {
     result.push(value);
     result.push(value);
   });
   });
-  return result.filter(scope => !hasAllScope(scope));
+  return result.filter((scope) => !hasAllScope(scope));
 };
 };
 
 
-
 /**
 /**
  * Extracts scopes from a given array of scopes
  * Extracts scopes from a given array of scopes
  * And delete all scopes
  * And delete all scopes

+ 4 - 2
apps/app/src/server/util/slack-integration.ts

@@ -2,10 +2,12 @@ import type { IChannelOptionalId } from '@growi/slack';
 import { getSupportedGrowiActionsRegExp } from '@growi/slack/dist/utils/get-supported-growi-actions-regexps';
 import { getSupportedGrowiActionsRegExp } from '@growi/slack/dist/utils/get-supported-growi-actions-regexps';
 import { permissionParser } from '@growi/slack/dist/utils/permission-parser';
 import { permissionParser } from '@growi/slack/dist/utils/permission-parser';
 
 
-type CommandPermission = { [key:string]: string[] | boolean }
+type CommandPermission = { [key: string]: string[] | boolean };
 
 
 export const checkPermission = (
 export const checkPermission = (
-    commandPermission: CommandPermission, commandOrActionIdOrCallbackId: string, fromChannel: IChannelOptionalId,
+  commandPermission: CommandPermission,
+  commandOrActionIdOrCallbackId: string,
+  fromChannel: IChannelOptionalId,
 ): boolean => {
 ): boolean => {
   let isPermitted = false;
   let isPermitted = false;
 
 

+ 15 - 12
apps/app/src/server/util/slack-legacy.ts

@@ -1,37 +1,40 @@
 import type { ChatPostMessageArguments } from '@slack/web-api';
 import type { ChatPostMessageArguments } from '@slack/web-api';
 import { WebClient } from '@slack/web-api';
 import { WebClient } from '@slack/web-api';
-import { IncomingWebhook, type IncomingWebhookSendArguments } from '@slack/webhook';
+import {
+  IncomingWebhook,
+  type IncomingWebhookSendArguments,
+} from '@slack/webhook';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:util:slack-legacy');
 const logger = loggerFactory('growi:util:slack-legacy');
 
 
-
 interface SlackLegacyUtil {
 interface SlackLegacyUtil {
-  postMessage(messageObj: IncomingWebhookSendArguments | ChatPostMessageArguments): Promise<void>,
+  postMessage(
+    messageObj: IncomingWebhookSendArguments | ChatPostMessageArguments,
+  ): Promise<void>;
 }
 }
 
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 export const slackLegacyUtilFactory = (configManager: any): SlackLegacyUtil => {
 export const slackLegacyUtilFactory = (configManager: any): SlackLegacyUtil => {
-
-  const postWithIwh = async(messageObj: IncomingWebhookSendArguments) => {
-    const webhook = new IncomingWebhook(configManager.getConfig('slack:incomingWebhookUrl'));
+  const postWithIwh = async (messageObj: IncomingWebhookSendArguments) => {
+    const webhook = new IncomingWebhook(
+      configManager.getConfig('slack:incomingWebhookUrl'),
+    );
     try {
     try {
       await webhook.send(messageObj);
       await webhook.send(messageObj);
-    }
-    catch (error) {
+    } catch (error) {
       logger.debug('Post error', error);
       logger.debug('Post error', error);
       logger.debug('Sent data to slack is:', messageObj);
       logger.debug('Sent data to slack is:', messageObj);
       throw error;
       throw error;
     }
     }
   };
   };
 
 
-  const postWithWebApi = async(messageObj?: ChatPostMessageArguments) => {
+  const postWithWebApi = async (messageObj?: ChatPostMessageArguments) => {
     const client = new WebClient(configManager.getConfig('slack:token'));
     const client = new WebClient(configManager.getConfig('slack:token'));
     try {
     try {
       await client.chat.postMessage(messageObj);
       await client.chat.postMessage(messageObj);
-    }
-    catch (error) {
+    } catch (error) {
       logger.debug('Post error', error);
       logger.debug('Post error', error);
       logger.debug('Sent data to slack is:', messageObj);
       logger.debug('Sent data to slack is:', messageObj);
       throw error;
       throw error;
@@ -39,7 +42,7 @@ export const slackLegacyUtilFactory = (configManager: any): SlackLegacyUtil => {
   };
   };
 
 
   return {
   return {
-    postMessage: async(messageObj) => {
+    postMessage: async (messageObj) => {
       // when incoming Webhooks is prioritized
       // when incoming Webhooks is prioritized
       if (configManager.getConfig('slack:isIncomingWebhookPrioritized')) {
       if (configManager.getConfig('slack:isIncomingWebhookPrioritized')) {
         if (configManager.getConfig('slack:incomingWebhookUrl')) {
         if (configManager.getConfig('slack:incomingWebhookUrl')) {

+ 55 - 29
apps/app/src/server/util/slack.js

@@ -10,15 +10,14 @@ const logger = loggerFactory('growi:util:slack');
 
 
 /* eslint-disable no-use-before-define */
 /* eslint-disable no-use-before-define */
 
 
-const convertMarkdownToMarkdown = function(body, siteUrl) {
-  return body
+const convertMarkdownToMarkdown = (body, siteUrl) =>
+  body
     .replace(/\n\*\s(.+)/g, '\n• $1')
     .replace(/\n\*\s(.+)/g, '\n• $1')
     .replace(/#{1,}\s?(.+)/g, '\n*$1*')
     .replace(/#{1,}\s?(.+)/g, '\n*$1*')
     .replace(/(\[(.+)\]\((https?:\/\/.+)\))/g, '<$3|$2>')
     .replace(/(\[(.+)\]\((https?:\/\/.+)\))/g, '<$3|$2>')
     .replace(/(\[(.+)\]\((\/.+)\))/g, `<${siteUrl}$3|$2>`);
     .replace(/(\[(.+)\]\((\/.+)\))/g, `<${siteUrl}$3|$2>`);
-};
 
 
-const prepareAttachmentTextForCreate = function(page, siteUrl) {
+const prepareAttachmentTextForCreate = (page, siteUrl) => {
   let body = page.revision.body;
   let body = page.revision.body;
   if (body.length > 2000) {
   if (body.length > 2000) {
     body = `${body.substr(0, 2000)}...`;
     body = `${body.substr(0, 2000)}...`;
@@ -33,7 +32,7 @@ const prepareAttachmentTextForCreate = function(page, siteUrl) {
  * @param {string} siteUrl
  * @param {string} siteUrl
  * @param {IRevisionHasId} previousRevision
  * @param {IRevisionHasId} previousRevision
  */
  */
-const prepareAttachmentTextForUpdate = function(page, siteUrl, previousRevision) {
+const prepareAttachmentTextForUpdate = (page, siteUrl, previousRevision) => {
   if (previousRevision == null) {
   if (previousRevision == null) {
     return;
     return;
   }
   }
@@ -46,15 +45,13 @@ const prepareAttachmentTextForUpdate = function(page, siteUrl, previousRevision)
     const value = line.value.replace(/\r\n|\r/g, '\n'); // eslint-disable-line no-unused-vars
     const value = line.value.replace(/\r\n|\r/g, '\n'); // eslint-disable-line no-unused-vars
     if (line.added) {
     if (line.added) {
       diffText += `${line.value} ... :lower_left_fountain_pen:`;
       diffText += `${line.value} ... :lower_left_fountain_pen:`;
-    }
-    else if (line.removed) {
+    } else if (line.removed) {
       // diffText += '-' + line.value.replace(/(.+)?\n/g, '- $1\n');
       // diffText += '-' + line.value.replace(/(.+)?\n/g, '- $1\n');
       // 1以下は無視
       // 1以下は無視
       if (line.count > 1) {
       if (line.count > 1) {
         diffText += `:wastebasket: ... ${line.count} lines\n`;
         diffText += `:wastebasket: ... ${line.count} lines\n`;
       }
       }
-    }
-    else {
+    } else {
       // diffText += '...\n';
       // diffText += '...\n';
     }
     }
   });
   });
@@ -64,7 +61,7 @@ const prepareAttachmentTextForUpdate = function(page, siteUrl, previousRevision)
   return diffText;
   return diffText;
 };
 };
 
 
-const prepareAttachmentTextForComment = function(comment) {
+const prepareAttachmentTextForComment = (comment) => {
   let body = comment.comment;
   let body = comment.comment;
   if (body.length > 2000) {
   if (body.length > 2000) {
     body = `${body.substr(0, 2000)}...`;
     body = `${body.substr(0, 2000)}...`;
@@ -77,27 +74,39 @@ const prepareAttachmentTextForComment = function(comment) {
   return body;
   return body;
 };
 };
 
 
-const generateSlackMessageTextForPage = function(path, pageId, user, siteUrl, updateType) {
+const generateSlackMessageTextForPage = (
+  path,
+  pageId,
+  user,
+  siteUrl,
+  updateType,
+) => {
   let text;
   let text;
 
 
   const pageUrl = `<${urljoin(siteUrl, pageId)}|${path}>`;
   const pageUrl = `<${urljoin(siteUrl, pageId)}|${path}>`;
   if (updateType === 'create') {
   if (updateType === 'create') {
     text = `:rocket: ${user.username} created a new page! ${pageUrl}`;
     text = `:rocket: ${user.username} created a new page! ${pageUrl}`;
-  }
-  else {
+  } else {
     text = `:heavy_check_mark: ${user.username} updated ${pageUrl}`;
     text = `:heavy_check_mark: ${user.username} updated ${pageUrl}`;
   }
   }
 
 
   return text;
   return text;
 };
 };
 
 
-export const prepareSlackMessageForPage = (page, user, appTitle, siteUrl, channel, updateType, previousRevision) => {
+export const prepareSlackMessageForPage = (
+  page,
+  user,
+  appTitle,
+  siteUrl,
+  channel,
+  updateType,
+  previousRevision,
+) => {
   let body = page.revision.body;
   let body = page.revision.body;
 
 
   if (updateType === 'create') {
   if (updateType === 'create') {
     body = prepareAttachmentTextForCreate(page, siteUrl);
     body = prepareAttachmentTextForCreate(page, siteUrl);
-  }
-  else {
+  } else {
     body = prepareAttachmentTextForUpdate(page, siteUrl, previousRevision);
     body = prepareAttachmentTextForUpdate(page, siteUrl, previousRevision);
   }
   }
 
 
@@ -116,16 +125,29 @@ export const prepareSlackMessageForPage = (page, user, appTitle, siteUrl, channe
   }
   }
 
 
   const message = {
   const message = {
-    channel: (channel != null) ? `#${channel}` : undefined,
+    channel: channel != null ? `#${channel}` : undefined,
     username: appTitle,
     username: appTitle,
-    text: generateSlackMessageTextForPage(page.path, page.id, user, siteUrl, updateType),
+    text: generateSlackMessageTextForPage(
+      page.path,
+      page.id,
+      user,
+      siteUrl,
+      updateType,
+    ),
     attachments: [attachment],
     attachments: [attachment],
   };
   };
 
 
   return message;
   return message;
 };
 };
 
 
-export const prepareSlackMessageForComment = (comment, user, appTitle, siteUrl, channel, path) => {
+export const prepareSlackMessageForComment = (
+  comment,
+  user,
+  appTitle,
+  siteUrl,
+  channel,
+  path,
+) => {
   const body = prepareAttachmentTextForComment(comment);
   const body = prepareAttachmentTextForComment(comment);
 
 
   const attachment = {
   const attachment = {
@@ -144,7 +166,7 @@ export const prepareSlackMessageForComment = (comment, user, appTitle, siteUrl,
   const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
   const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
 
 
   const message = {
   const message = {
-    channel: (channel != null) ? `#${channel}` : undefined,
+    channel: channel != null ? `#${channel}` : undefined,
     username: appTitle,
     username: appTitle,
     text,
     text,
     attachments: [attachment],
     attachments: [attachment],
@@ -154,14 +176,18 @@ export const prepareSlackMessageForComment = (comment, user, appTitle, siteUrl,
 };
 };
 
 
 /**
 /**
-   * For GlobalNotification
-   *
-   * @param {string} messageBody
-   * @param {string} attachmentBody
-   * @param {string} slackChannel
-  */
-export const prepareSlackMessageForGlobalNotification = (messageBody, attachmentBody, appTitle, slackChannel) => {
-
+ * For GlobalNotification
+ *
+ * @param {string} messageBody
+ * @param {string} attachmentBody
+ * @param {string} slackChannel
+ */
+export const prepareSlackMessageForGlobalNotification = (
+  messageBody,
+  attachmentBody,
+  appTitle,
+  slackChannel,
+) => {
   const attachment = {
   const attachment = {
     color: '#263a3c',
     color: '#263a3c',
     text: attachmentBody,
     text: attachmentBody,
@@ -169,7 +195,7 @@ export const prepareSlackMessageForGlobalNotification = (messageBody, attachment
   };
   };
 
 
   const message = {
   const message = {
-    channel: (slackChannel != null) ? `#${slackChannel}` : undefined,
+    channel: slackChannel != null ? `#${slackChannel}` : undefined,
     username: appTitle,
     username: appTitle,
     text: messageBody,
     text: messageBody,
     attachments: JSON.stringify([attachment]),
     attachments: JSON.stringify([attachment]),

+ 13 - 4
apps/app/src/server/util/stream.spec.ts

@@ -1,4 +1,4 @@
-import { Readable, Writable, pipeline } from 'stream';
+import { pipeline, Readable, Writable } from 'stream';
 import { promisify } from 'util';
 import { promisify } from 'util';
 
 
 import { getBufferToFixedSizeTransform } from './stream';
 import { getBufferToFixedSizeTransform } from './stream';
@@ -7,11 +7,16 @@ const pipelinePromise = promisify(pipeline);
 
 
 describe('stream util', () => {
 describe('stream util', () => {
   describe('getBufferToFixedSizeTransform', () => {
   describe('getBufferToFixedSizeTransform', () => {
-    it('should buffer data to fixed size and push to next stream', async() => {
+    it('should buffer data to fixed size and push to next stream', async () => {
       const bufferSize = 10;
       const bufferSize = 10;
       const chunks: Buffer[] = [];
       const chunks: Buffer[] = [];
 
 
-      const readable = Readable.from([Buffer.from('1234567890A'), Buffer.from('BCDE'), Buffer.from('FGH'), Buffer.from('IJKL')]);
+      const readable = Readable.from([
+        Buffer.from('1234567890A'),
+        Buffer.from('BCDE'),
+        Buffer.from('FGH'),
+        Buffer.from('IJKL'),
+      ]);
       const transform = getBufferToFixedSizeTransform(bufferSize);
       const transform = getBufferToFixedSizeTransform(bufferSize);
       const writable = new Writable({
       const writable = new Writable({
         write(chunk: Buffer, encoding, callback) {
         write(chunk: Buffer, encoding, callback) {
@@ -20,7 +25,11 @@ describe('stream util', () => {
         },
         },
       });
       });
 
 
-      const expectedChunks = [Buffer.from('1234567890'), Buffer.from('ABCDEFGHIJ'), Buffer.from('KL')];
+      const expectedChunks = [
+        Buffer.from('1234567890'),
+        Buffer.from('ABCDEFGHIJ'),
+        Buffer.from('KL'),
+      ];
 
 
       await pipelinePromise(readable, transform, writable);
       await pipelinePromise(readable, transform, writable);
 
 

+ 5 - 5
apps/app/src/server/util/stream.ts

@@ -1,17 +1,14 @@
 import { Transform } from 'stream';
 import { Transform } from 'stream';
 
 
 export const convertStreamToBuffer = (stream: any): Promise<Buffer> => {
 export const convertStreamToBuffer = (stream: any): Promise<Buffer> => {
-
   return new Promise((resolve, reject) => {
   return new Promise((resolve, reject) => {
-
     const buffer: Uint8Array[] = [];
     const buffer: Uint8Array[] = [];
 
 
     stream.on('data', (chunk: Uint8Array) => {
     stream.on('data', (chunk: Uint8Array) => {
       buffer.push(chunk);
       buffer.push(chunk);
     });
     });
     stream.on('end', () => resolve(Buffer.concat(buffer)));
     stream.on('end', () => resolve(Buffer.concat(buffer)));
-    stream.on('error', err => reject(err));
-
+    stream.on('error', (err) => reject(err));
   });
   });
 };
 };
 
 
@@ -29,7 +26,10 @@ export const getBufferToFixedSizeTransform = (size: number): Transform => {
         // - If the remaining chunk size is larger than the remaining buffer size:
         // - If the remaining chunk size is larger than the remaining buffer size:
         //     - Fill the buffer, and upload => dataSize is the remaining buffer size
         //     - Fill the buffer, and upload => dataSize is the remaining buffer size
         //     - The remaining chunk after upload will be added to buffer in the next iteration
         //     - The remaining chunk after upload will be added to buffer in the next iteration
-        const dataSize = Math.min(size - filledBufferSize, chunk.length - offset);
+        const dataSize = Math.min(
+          size - filledBufferSize,
+          chunk.length - offset,
+        );
         // Add chunk data to buffer
         // Add chunk data to buffer
         chunk.copy(buffer, filledBufferSize, offset, offset + dataSize);
         chunk.copy(buffer, filledBufferSize, offset, offset + dataSize);
         filledBufferSize += dataSize;
         filledBufferSize += dataSize;

+ 5 - 1
apps/app/src/services/renderer/rehype-plugins/add-class.ts

@@ -45,5 +45,9 @@ const adder = (entry: AdditionsEntry) => {
 export const rehypePlugin: Plugin<[Additions]> = (additions) => {
 export const rehypePlugin: Plugin<[Additions]> = (additions) => {
   const adders = Object.entries(additions).map(adder);
   const adders = Object.entries(additions).map(adder);
 
 
-  return (node) => adders.forEach((a) => a(node as HastNode));
+  return (node) => {
+    adders.forEach((a) => {
+      a(node as HastNode);
+    });
+  };
 };
 };

+ 3 - 3
apps/app/src/services/renderer/rehype-plugins/keyword-highlighter.ts

@@ -93,9 +93,9 @@ export const rehypePlugin: Plugin<[KeywordHighlighterPluginParams]> = (
   return rehypeRewrite.bind(this)({
   return rehypeRewrite.bind(this)({
     rewrite: (node, index, parent) => {
     rewrite: (node, index, parent) => {
       if (parent != null && index != null && node.type === 'text') {
       if (parent != null && index != null && node.type === 'text') {
-        lowercasedKeywords.forEach((keyword) =>
-          highlight(keyword, node, index, parent),
-        );
+        lowercasedKeywords.forEach((keyword) => {
+          highlight(keyword, node, index, parent);
+        });
       }
       }
     },
     },
   });
   });

+ 0 - 1
apps/app/src/services/renderer/rehype-plugins/relative-links-by-pukiwiki-like-linker.spec.ts

@@ -5,7 +5,6 @@ import rehype from 'remark-rehype';
 import { unified } from 'unified';
 import { unified } from 'unified';
 
 
 import { pukiwikiLikeLinker } from '../remark-plugins/pukiwiki-like-linker';
 import { pukiwikiLikeLinker } from '../remark-plugins/pukiwiki-like-linker';
-
 import { relativeLinksByPukiwikiLikeLinker } from './relative-links-by-pukiwiki-like-linker';
 import { relativeLinksByPukiwikiLikeLinker } from './relative-links-by-pukiwiki-like-linker';
 
 
 describe('relativeLinksByPukiwikiLikeLinker', () => {
 describe('relativeLinksByPukiwikiLikeLinker', () => {

+ 0 - 1
apps/app/src/services/renderer/rehype-plugins/relative-links.ts

@@ -1,5 +1,4 @@
 import assert from 'assert';
 import assert from 'assert';
-
 import type { Element, Nodes as HastNode } from 'hast';
 import type { Element, Nodes as HastNode } from 'hast';
 import { selectAll } from 'hast-util-select';
 import { selectAll } from 'hast-util-select';
 import isAbsolute from 'is-absolute-url';
 import isAbsolute from 'is-absolute-url';

+ 14 - 4
apps/app/src/stores/activity.ts

@@ -6,13 +6,23 @@ import type { IActivityHasId, ISearchFilter } from '~/interfaces/activity';
 import type { PaginateResult } from '~/interfaces/mongoose-utils';
 import type { PaginateResult } from '~/interfaces/mongoose-utils';
 import { useAuditLogEnabled } from '~/stores-universal/context';
 import { useAuditLogEnabled } from '~/stores-universal/context';
 
 
-export const useSWRxActivity = (limit?: number, offset?: number, searchFilter?: ISearchFilter): SWRResponse<PaginateResult<IActivityHasId>, Error> => {
+export const useSWRxActivity = (
+  limit?: number,
+  offset?: number,
+  searchFilter?: ISearchFilter,
+): SWRResponse<PaginateResult<IActivityHasId>, Error> => {
   const { data: auditLogEnabled } = useAuditLogEnabled();
   const { data: auditLogEnabled } = useAuditLogEnabled();
 
 
   const stringifiedSearchFilter = JSON.stringify(searchFilter);
   const stringifiedSearchFilter = JSON.stringify(searchFilter);
   return useSWRImmutable(
   return useSWRImmutable(
-    auditLogEnabled ? ['/activity', limit, offset, stringifiedSearchFilter] : null,
-    ([endpoint, limit, offset, stringifiedSearchFilter]) => apiv3Get(endpoint, { limit, offset, searchFilter: stringifiedSearchFilter })
-      .then(result => result.data.serializedPaginationResult),
+    auditLogEnabled
+      ? ['/activity', limit, offset, stringifiedSearchFilter]
+      : null,
+    ([endpoint, limit, offset, stringifiedSearchFilter]) =>
+      apiv3Get(endpoint, {
+        limit,
+        offset,
+        searchFilter: stringifiedSearchFilter,
+      }).then((result) => result.data.serializedPaginationResult),
   );
   );
 };
 };

+ 2 - 3
apps/app/src/stores/admin/app-settings.tsx

@@ -5,9 +5,8 @@ import { apiv3Get } from '~/client/util/apiv3-client';
 import type { IResAppSettings } from '~/interfaces/res/admin/app-settings';
 import type { IResAppSettings } from '~/interfaces/res/admin/app-settings';
 
 
 export const useSWRxAppSettings = (): SWRResponse<IResAppSettings, Error> => {
 export const useSWRxAppSettings = (): SWRResponse<IResAppSettings, Error> => {
-  return useSWR(
-    '/app-settings/',
-    endpoint => apiv3Get(endpoint).then((response) => {
+  return useSWR('/app-settings/', (endpoint) =>
+    apiv3Get(endpoint).then((response) => {
       return response.data.appSettingsParams;
       return response.data.appSettingsParams;
     }),
     }),
   );
   );

+ 20 - 22
apps/app/src/stores/admin/customize.tsx

@@ -1,26 +1,28 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 import useSWR from 'swr';
 import useSWR from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import type { updateConfigMethodForAdmin } from '~/interfaces/admin';
 import type { updateConfigMethodForAdmin } from '~/interfaces/admin';
-import type { IResLayoutSetting, IResGrowiTheme } from '~/interfaces/customize';
-
-export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> & updateConfigMethodForAdmin<IResLayoutSetting> => {
+import type { IResGrowiTheme, IResLayoutSetting } from '~/interfaces/customize';
 
 
-  const fetcher = useCallback(async() => {
+export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> &
+  updateConfigMethodForAdmin<IResLayoutSetting> => {
+  const fetcher = useCallback(async () => {
     const res = await apiv3Get('/customize-setting/layout');
     const res = await apiv3Get('/customize-setting/layout');
     return res.data;
     return res.data;
   }, []);
   }, []);
 
 
   const swrResponse = useSWRImmutable('/customize-setting/layout', fetcher);
   const swrResponse = useSWRImmutable('/customize-setting/layout', fetcher);
 
 
-  const update = useCallback(async(layoutSetting: IResLayoutSetting) => {
-    await apiv3Put('/customize-setting/layout', layoutSetting);
-    await swrResponse.mutate();
-  }, [swrResponse]);
+  const update = useCallback(
+    async (layoutSetting: IResLayoutSetting) => {
+      await apiv3Put('/customize-setting/layout', layoutSetting);
+      await swrResponse.mutate();
+    },
+    [swrResponse],
+  );
 
 
   return {
   return {
     ...swrResponse,
     ...swrResponse,
@@ -29,19 +31,18 @@ export const useSWRxLayoutSetting = (): SWRResponse<IResLayoutSetting, Error> &
 };
 };
 
 
 type UpdateThemeArgs = {
 type UpdateThemeArgs = {
-  theme: string,
-}
-export const useSWRxGrowiThemeSetting = (): SWRResponse<IResGrowiTheme, Error> & updateConfigMethodForAdmin<UpdateThemeArgs> => {
-
-  const fetcher = useCallback(async() => {
+  theme: string;
+};
+export const useSWRxGrowiThemeSetting = (): SWRResponse<IResGrowiTheme, Error> &
+  updateConfigMethodForAdmin<UpdateThemeArgs> => {
+  const fetcher = useCallback(async () => {
     const res = await apiv3Get<IResGrowiTheme>('/customize-setting/theme');
     const res = await apiv3Get<IResGrowiTheme>('/customize-setting/theme');
     return res.data;
     return res.data;
   }, []);
   }, []);
 
 
   const swrResponse = useSWR('/customize-setting/theme', fetcher);
   const swrResponse = useSWR('/customize-setting/theme', fetcher);
 
 
-  const update = async({ theme }: UpdateThemeArgs) => {
-
+  const update = async ({ theme }: UpdateThemeArgs) => {
     await apiv3Put('/customize-setting/theme', { theme });
     await apiv3Put('/customize-setting/theme', { theme });
 
 
     if (swrResponse.data == null) {
     if (swrResponse.data == null) {
@@ -53,10 +54,7 @@ export const useSWRxGrowiThemeSetting = (): SWRResponse<IResGrowiTheme, Error> &
     swrResponse.mutate(newData, { optimisticData: newData });
     swrResponse.mutate(newData, { optimisticData: newData });
   };
   };
 
 
-  return Object.assign(
-    swrResponse,
-    {
-      update,
-    },
-  );
+  return Object.assign(swrResponse, {
+    update,
+  });
 };
 };

+ 37 - 28
apps/app/src/stores/admin/sidebar-config.tsx

@@ -1,5 +1,4 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -7,16 +6,20 @@ import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 
 
 type SidebarConfigOption = {
 type SidebarConfigOption = {
-  update: () => Promise<void>,
+  update: () => Promise<void>;
 
 
-  setIsSidebarCollapsedMode: (isSidebarCollapsedMode: boolean) => void,
-  setIsSidebarClosedAtDockMode: (isSidebarClosedAtDockMode: boolean | undefined) => void,
-}
+  setIsSidebarCollapsedMode: (isSidebarCollapsedMode: boolean) => void;
+  setIsSidebarClosedAtDockMode: (
+    isSidebarClosedAtDockMode: boolean | undefined,
+  ) => void;
+};
 
 
-export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & SidebarConfigOption => {
+export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> &
+  SidebarConfigOption => {
   const swrResponse = useSWRImmutable<ISidebarConfig>(
   const swrResponse = useSWRImmutable<ISidebarConfig>(
     '/customize-setting/sidebar',
     '/customize-setting/sidebar',
-    endpoint => apiv3Get<ISidebarConfig>(endpoint).then(result => result.data),
+    (endpoint) =>
+      apiv3Get<ISidebarConfig>(endpoint).then((result) => result.data),
     {
     {
       keepPreviousData: true,
       keepPreviousData: true,
     },
     },
@@ -26,7 +29,7 @@ export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & Sid
 
 
   return {
   return {
     ...swrResponse,
     ...swrResponse,
-    update: useCallback(async() => {
+    update: useCallback(async () => {
       if (data == null) {
       if (data == null) {
         return;
         return;
       }
       }
@@ -35,25 +38,31 @@ export const useSWRxSidebarConfig = (): SWRResponse<ISidebarConfig, Error> & Sid
       await apiv3Put<ISidebarConfig>('/customize-setting/sidebar', data);
       await apiv3Put<ISidebarConfig>('/customize-setting/sidebar', data);
     }, [data]),
     }, [data]),
 
 
-    setIsSidebarCollapsedMode: useCallback((isSidebarCollapsedMode) => {
-      // update isSidebarCollapsedMode in cache, not revalidate
-      mutate((prevData) => {
-        if (prevData == null) {
-          return;
-        }
-
-        return { ...prevData, isSidebarCollapsedMode };
-      }, false);
-    }, [mutate]),
-
-    setIsSidebarClosedAtDockMode: useCallback((isSidebarClosedAtDockMode) => {
-      // update isSidebarClosedAtDockMode in cache, not revalidate
-      mutate((prevData) => {
-        if (prevData == null) {
-          return;
-        }
-        return { ...prevData, isSidebarClosedAtDockMode };
-      }, false);
-    }, [mutate]),
+    setIsSidebarCollapsedMode: useCallback(
+      (isSidebarCollapsedMode) => {
+        // update isSidebarCollapsedMode in cache, not revalidate
+        mutate((prevData) => {
+          if (prevData == null) {
+            return;
+          }
+
+          return { ...prevData, isSidebarCollapsedMode };
+        }, false);
+      },
+      [mutate],
+    ),
+
+    setIsSidebarClosedAtDockMode: useCallback(
+      (isSidebarClosedAtDockMode) => {
+        // update isSidebarClosedAtDockMode in cache, not revalidate
+        mutate((prevData) => {
+          if (prevData == null) {
+            return;
+          }
+          return { ...prevData, isSidebarClosedAtDockMode };
+        }, false);
+      },
+      [mutate],
+    ),
   };
   };
 };
 };

+ 30 - 20
apps/app/src/stores/alert.tsx

@@ -1,38 +1,48 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
 import { useSWRStatic } from '@growi/core/dist/swr';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 
 
 import type { EditorMode } from '../stores-universal/ui';
 import type { EditorMode } from '../stores-universal/ui';
 
 
 /*
 /*
-* PageStatusAlert
-*/
+ * PageStatusAlert
+ */
 type OpenPageStatusAlertOptions = {
 type OpenPageStatusAlertOptions = {
-  hideEditorMode?: EditorMode
-  onRefleshPage?: () => void
-  onResolveConflict?: () => void
-}
+  hideEditorMode?: EditorMode;
+  onRefleshPage?: () => void;
+  onResolveConflict?: () => void;
+};
 
 
 type PageStatusAlertStatus = {
 type PageStatusAlertStatus = {
-  isOpen: boolean
-  hideEditorMode?: EditorMode,
-  onRefleshPage?: () => void
-  onResolveConflict?: () => void
-}
+  isOpen: boolean;
+  hideEditorMode?: EditorMode;
+  onRefleshPage?: () => void;
+  onResolveConflict?: () => void;
+};
 
 
 type PageStatusAlertUtils = {
 type PageStatusAlertUtils = {
-  open: (openPageStatusAlert: OpenPageStatusAlertOptions) => void,
-  close: () => void,
-}
-export const usePageStatusAlert = (): SWRResponse<PageStatusAlertStatus, Error> & PageStatusAlertUtils => {
+  open: (openPageStatusAlert: OpenPageStatusAlertOptions) => void;
+  close: () => void;
+};
+export const usePageStatusAlert = (): SWRResponse<
+  PageStatusAlertStatus,
+  Error
+> &
+  PageStatusAlertUtils => {
   const initialData: PageStatusAlertStatus = { isOpen: false };
   const initialData: PageStatusAlertStatus = { isOpen: false };
-  const swrResponse = useSWRStatic<PageStatusAlertStatus, Error>('pageStatusAlert', undefined, { fallbackData: initialData });
+  const swrResponse = useSWRStatic<PageStatusAlertStatus, Error>(
+    'pageStatusAlert',
+    undefined,
+    { fallbackData: initialData },
+  );
   const { mutate } = swrResponse;
   const { mutate } = swrResponse;
 
 
-  const open = useCallback(({ ...options }) => {
-    mutate({ isOpen: true, ...options });
-  }, [mutate]);
+  const open = useCallback(
+    ({ ...options }) => {
+      mutate({ isOpen: true, ...options });
+    },
+    [mutate],
+  );
 
 
   const close = useCallback(() => {
   const close = useCallback(() => {
     mutate({ isOpen: false });
     mutate({ isOpen: false });

+ 47 - 38
apps/app/src/stores/attachment.tsx

@@ -1,12 +1,6 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
-import type {
-  IAttachmentHasId, Nullable,
-} from '@growi/core';
-import {
-  type SWRResponseWithUtils, withUtils,
-} from '@growi/core/dist/swr';
-import type { Util } from 'reactstrap';
+import type { IAttachmentHasId, Nullable } from '@growi/core';
+import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
 import useSWR, { useSWRConfig } from 'swr';
 import useSWR, { useSWRConfig } from 'swr';
 
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
@@ -14,44 +8,54 @@ import { apiv3Get } from '~/client/util/apiv3-client';
 import type { IResAttachmentList } from '~/interfaces/attachment';
 import type { IResAttachmentList } from '~/interfaces/attachment';
 
 
 type Util = {
 type Util = {
-  remove(body: { attachment_id: string }): Promise<void>
+  remove(body: { attachment_id: string }): Promise<void>;
 };
 };
 
 
 type IDataAttachmentList = {
 type IDataAttachmentList = {
-  attachments: IAttachmentHasId[]
-  totalAttachments: number
-  limit: number
+  attachments: IAttachmentHasId[];
+  totalAttachments: number;
+  limit: number;
 };
 };
 
 
-export const useSWRxAttachment = (attachmentId: string): SWRResponseWithUtils<Util, IAttachmentHasId, Error> => {
+export const useSWRxAttachment = (
+  attachmentId: string,
+): SWRResponseWithUtils<Util, IAttachmentHasId, Error> => {
   const swrResponse = useSWR(
   const swrResponse = useSWR(
     [`/attachment/${attachmentId}`],
     [`/attachment/${attachmentId}`],
-    useCallback(async([endpoint]) => {
+    useCallback(async ([endpoint]) => {
       const res = await apiv3Get(endpoint);
       const res = await apiv3Get(endpoint);
       return res.data.attachment;
       return res.data.attachment;
     }, []),
     }, []),
   );
   );
 
 
   // Utils
   // Utils
-  const remove = useCallback(async(body: { attachment_id: string }) => {
-    try {
-      await apiPost('/attachments.remove', body);
-      swrResponse.mutate(body.attachment_id);
-    }
-    catch (err) {
-      throw err;
-    }
-  }, [swrResponse]);
+  const remove = useCallback(
+    async (body: { attachment_id: string }) => {
+      try {
+        await apiPost('/attachments.remove', body);
+        swrResponse.mutate(body.attachment_id);
+      } catch (err) {
+        throw err;
+      }
+    },
+    [swrResponse],
+  );
 
 
   return withUtils<Util, IAttachmentHasId, Error>(swrResponse, { remove });
   return withUtils<Util, IAttachmentHasId, Error>(swrResponse, { remove });
 };
 };
 
 
-export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: number): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
+export const useSWRxAttachments = (
+  pageId?: Nullable<string>,
+  pageNumber?: number,
+): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
   const { mutate: mutateUseSWRxAttachment } = useSWRConfig();
   const { mutate: mutateUseSWRxAttachment } = useSWRConfig();
   const shouldFetch = pageId != null && pageNumber != null;
   const shouldFetch = pageId != null && pageNumber != null;
 
 
-  const fetcher = useCallback(async([endpoint, pageId, pageNumber]) => {
-    const res = await apiv3Get<IResAttachmentList>(endpoint, { pageId, pageNumber });
+  const fetcher = useCallback(async ([endpoint, pageId, pageNumber]) => {
+    const res = await apiv3Get<IResAttachmentList>(endpoint, {
+      pageId,
+      pageNumber,
+    });
     const resAttachmentList = res.data;
     const resAttachmentList = res.data;
     const { paginateResult } = resAttachmentList;
     const { paginateResult } = resAttachmentList;
     return {
     return {
@@ -67,19 +71,24 @@ export const useSWRxAttachments = (pageId?: Nullable<string>, pageNumber?: numbe
   );
   );
 
 
   // Utils
   // Utils
-  const remove = useCallback(async(body: { attachment_id: string }) => {
-    const { mutate } = swrResponse;
+  const remove = useCallback(
+    async (body: { attachment_id: string }) => {
+      const { mutate } = swrResponse;
 
 
-    try {
-      await apiPost('/attachments.remove', body);
-      mutate();
-      // Mutation for rich attachment rendering
-      mutateUseSWRxAttachment([`/attachment/${body.attachment_id}`], body.attachment_id);
-    }
-    catch (err) {
-      throw err;
-    }
-  }, [mutateUseSWRxAttachment, swrResponse]);
+      try {
+        await apiPost('/attachments.remove', body);
+        mutate();
+        // Mutation for rich attachment rendering
+        mutateUseSWRxAttachment(
+          [`/attachment/${body.attachment_id}`],
+          body.attachment_id,
+        );
+      } catch (err) {
+        throw err;
+      }
+    },
+    [mutateUseSWRxAttachment, swrResponse],
+  );
 
 
   return withUtils<Util, IDataAttachmentList, Error>(swrResponse, { remove });
   return withUtils<Util, IDataAttachmentList, Error>(swrResponse, { remove });
 };
 };

+ 7 - 5
apps/app/src/stores/bookmark-folder.ts

@@ -4,12 +4,14 @@ import useSWRImmutable from 'swr/immutable';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 
 
-export const useSWRxBookmarkFolderAndChild = (userId?: string): SWRResponse<BookmarkFolderItems[], Error> => {
-
+export const useSWRxBookmarkFolderAndChild = (
+  userId?: string,
+): SWRResponse<BookmarkFolderItems[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     userId != null ? `/bookmark-folder/list/${userId}` : null,
     userId != null ? `/bookmark-folder/list/${userId}` : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return response.data.bookmarkFolderItems;
-    }),
+    (endpoint) =>
+      apiv3Get(endpoint).then((response) => {
+        return response.data.bookmarkFolderItems;
+      }),
   );
   );
 };
 };

+ 25 - 15
apps/app/src/stores/bookmark.ts

@@ -1,41 +1,51 @@
-import type { IUser, IPageHasId } from '@growi/core';
+import type { IPageHasId, IUser } from '@growi/core';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 import useSWR from 'swr';
 import useSWR from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 
 
-
 import { useCurrentUser } from '~/stores-universal/context';
 import { useCurrentUser } from '~/stores-universal/context';
 
 
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
 import type { IBookmarkInfo } from '../interfaces/bookmark-info';
 import type { IBookmarkInfo } from '../interfaces/bookmark-info';
 
 
-
-export const useSWRxBookmarkedUsers = (pageId: string | null): SWRResponse<IUser[], Error> => {
+export const useSWRxBookmarkedUsers = (
+  pageId: string | null,
+): SWRResponse<IUser[], Error> => {
   return useSWR(
   return useSWR(
     pageId != null ? `/bookmarks/info?pageId=${pageId}` : null,
     pageId != null ? `/bookmarks/info?pageId=${pageId}` : null,
-    endpoint => apiv3Get<IBookmarkInfo>(endpoint).then(response => response.data.bookmarkedUsers),
+    (endpoint) =>
+      apiv3Get<IBookmarkInfo>(endpoint).then(
+        (response) => response.data.bookmarkedUsers,
+      ),
   );
   );
 };
 };
 
 
-export const useSWRxUserBookmarks = (userId: string | null): SWRResponse<(IPageHasId | null)[], Error> => {
+export const useSWRxUserBookmarks = (
+  userId: string | null,
+): SWRResponse<(IPageHasId | null)[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     userId != null ? `/bookmarks/${userId}` : null,
     userId != null ? `/bookmarks/${userId}` : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
-      const { userRootBookmarks } = response.data;
-      return userRootBookmarks.map(item => item.page); // page will be null if the page is deleted
-    }),
+    (endpoint) =>
+      apiv3Get(endpoint).then((response) => {
+        const { userRootBookmarks } = response.data;
+        return userRootBookmarks.map((item) => item.page); // page will be null if the page is deleted
+      }),
   );
   );
 };
 };
 
 
-export const useSWRMUTxCurrentUserBookmarks = (): SWRMutationResponse<(IPageHasId | null)[], Error> => {
+export const useSWRMUTxCurrentUserBookmarks = (): SWRMutationResponse<
+  (IPageHasId | null)[],
+  Error
+> => {
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
 
 
   return useSWRMutation(
   return useSWRMutation(
     currentUser != null ? `/bookmarks/${currentUser?._id}` : null,
     currentUser != null ? `/bookmarks/${currentUser?._id}` : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
-      const { userRootBookmarks } = response.data;
-      return userRootBookmarks.map(item => item.page); // page will be null if the page is deleted
-    }),
+    (endpoint) =>
+      apiv3Get(endpoint).then((response) => {
+        const { userRootBookmarks } = response.data;
+        return userRootBookmarks.map((item) => item.page); // page will be null if the page is deleted
+      }),
   );
   );
 };
 };

+ 51 - 38
apps/app/src/stores/comment.tsx

@@ -1,63 +1,76 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
 import type { Nullable } from '@growi/core';
 import type { Nullable } from '@growi/core';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 import useSWR from 'swr';
 import useSWR from 'swr';
 
 
 import { apiGet, apiPost } from '~/client/util/apiv1-client';
 import { apiGet, apiPost } from '~/client/util/apiv1-client';
 
 
-import type { ICommentHasIdList, ICommentPostArgs } from '../interfaces/comment';
+import type {
+  ICommentHasIdList,
+  ICommentPostArgs,
+} from '../interfaces/comment';
 
 
 type IResponseComment = {
 type IResponseComment = {
-  comments: ICommentHasIdList,
-  ok: boolean,
-}
+  comments: ICommentHasIdList;
+  ok: boolean;
+};
 
 
 type CommentOperation = {
 type CommentOperation = {
-  update(comment: string, revisionId: string, commentId: string): Promise<void>,
-  post(args: ICommentPostArgs): Promise<void>
-}
+  update(comment: string, revisionId: string, commentId: string): Promise<void>;
+  post(args: ICommentPostArgs): Promise<void>;
+};
 
 
-export const useSWRxPageComment = (pageId: Nullable<string>): SWRResponse<ICommentHasIdList, Error> & CommentOperation => {
+export const useSWRxPageComment = (
+  pageId: Nullable<string>,
+): SWRResponse<ICommentHasIdList, Error> & CommentOperation => {
   const shouldFetch: boolean = pageId != null;
   const shouldFetch: boolean = pageId != null;
 
 
   const swrResponse = useSWR(
   const swrResponse = useSWR(
     shouldFetch ? ['/comments.get', pageId] : null,
     shouldFetch ? ['/comments.get', pageId] : null,
-    ([endpoint, pageId]) => apiGet(endpoint, { page_id: pageId }).then((response:IResponseComment) => response.comments),
+    ([endpoint, pageId]) =>
+      apiGet(endpoint, { page_id: pageId }).then(
+        (response: IResponseComment) => response.comments,
+      ),
   );
   );
 
 
   const { mutate } = swrResponse;
   const { mutate } = swrResponse;
 
 
-  const update = useCallback(async(comment: string, revisionId: string, commentId: string) => {
-    await apiPost('/comments.update', {
-      commentForm: {
-        comment,
-        revision_id: revisionId,
-        comment_id: commentId,
-      },
-    });
-    mutate();
-  }, [mutate]);
+  const update = useCallback(
+    async (comment: string, revisionId: string, commentId: string) => {
+      await apiPost('/comments.update', {
+        commentForm: {
+          comment,
+          revision_id: revisionId,
+          comment_id: commentId,
+        },
+      });
+      mutate();
+    },
+    [mutate],
+  );
 
 
-  const post = useCallback(async(args: ICommentPostArgs) => {
-    const { commentForm, slackNotificationForm } = args;
-    const { comment, revisionId, replyTo } = commentForm;
-    const { isSlackEnabled, slackChannels } = slackNotificationForm;
+  const post = useCallback(
+    async (args: ICommentPostArgs) => {
+      const { commentForm, slackNotificationForm } = args;
+      const { comment, revisionId, replyTo } = commentForm;
+      const { isSlackEnabled, slackChannels } = slackNotificationForm;
 
 
-    await apiPost('/comments.add', {
-      commentForm: {
-        comment,
-        page_id: pageId,
-        revision_id: revisionId,
-        replyTo,
-      },
-      slackNotificationForm: {
-        isSlackEnabled,
-        slackChannels,
-      },
-    });
-    mutate();
-  }, [mutate, pageId]);
+      await apiPost('/comments.add', {
+        commentForm: {
+          comment,
+          page_id: pageId,
+          revision_id: revisionId,
+          replyTo,
+        },
+        slackNotificationForm: {
+          isSlackEnabled,
+          slackChannels,
+        },
+      });
+      mutate();
+    },
+    [mutate, pageId],
+  );
 
 
   return {
   return {
     ...swrResponse,
     ...swrResponse,

+ 48 - 30
apps/app/src/stores/editor.tsx

@@ -1,7 +1,10 @@
 import { useCallback, useEffect } from 'react';
 import { useCallback, useEffect } from 'react';
-
-import { type Nullable } from '@growi/core';
-import { withUtils, type SWRResponseWithUtils, useSWRStatic } from '@growi/core/dist/swr';
+import type { Nullable } from '@growi/core';
+import {
+  type SWRResponseWithUtils,
+  useSWRStatic,
+  withUtils,
+} from '@growi/core/dist/swr';
 import type { EditorSettings } from '@growi/editor';
 import type { EditorSettings } from '@growi/editor';
 import useSWR, { type SWRResponse } from 'swr';
 import useSWR, { type SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
@@ -10,39 +13,49 @@ import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import type { SlackChannels } from '~/interfaces/user-trigger-notification';
 import type { SlackChannels } from '~/interfaces/user-trigger-notification';
 import {
 import {
-  useCurrentUser, useDefaultIndentSize, useIsGuestUser, useIsReadOnlyUser,
+  useCurrentUser,
+  useDefaultIndentSize,
+  useIsGuestUser,
+  useIsReadOnlyUser,
 } from '~/stores-universal/context';
 } from '~/stores-universal/context';
 
 
 // import { localStorageMiddleware } from './middlewares/sync-to-storage';
 // import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { useSWRxTagsInfo } from './page';
 import { useSWRxTagsInfo } from './page';
 
 
-
 export const useWaitingSaveProcessing = (): SWRResponse<boolean, Error> => {
 export const useWaitingSaveProcessing = (): SWRResponse<boolean, Error> => {
-  return useSWRStatic('waitingSaveProcessing', undefined, { fallbackData: false });
+  return useSWRStatic('waitingSaveProcessing', undefined, {
+    fallbackData: false,
+  });
 };
 };
 
 
-
-export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
+export const useEditingMarkdown = (
+  initialData?: string,
+): SWRResponse<string, Error> => {
   return useSWRStatic('editingMarkdown', initialData);
   return useSWRStatic('editingMarkdown', initialData);
 };
 };
 
 
-
 type EditorSettingsOperation = {
 type EditorSettingsOperation = {
-  update: (updateData: Partial<EditorSettings>) => Promise<void>,
-}
+  update: (updateData: Partial<EditorSettings>) => Promise<void>;
+};
 
 
 // TODO: Enable localStorageMiddleware
 // TODO: Enable localStorageMiddleware
 //   - Unabling localStorageMiddleware occurrs a flickering problem when loading theme.
 //   - Unabling localStorageMiddleware occurrs a flickering problem when loading theme.
 //   - see: https://github.com/growilabs/growi/pull/6781#discussion_r1000285786
 //   - see: https://github.com/growilabs/growi/pull/6781#discussion_r1000285786
-export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperation, EditorSettings, Error> => {
+export const useEditorSettings = (): SWRResponseWithUtils<
+  EditorSettingsOperation,
+  EditorSettings,
+  Error
+> => {
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
 
   const swrResult = useSWRImmutable(
   const swrResult = useSWRImmutable(
-    (isGuestUser || isReadOnlyUser) ? null : ['/personal-setting/editor-settings', currentUser?.username],
+    isGuestUser || isReadOnlyUser
+      ? null
+      : ['/personal-setting/editor-settings', currentUser?.username],
     ([endpoint]) => {
     ([endpoint]) => {
-      return apiv3Get(endpoint).then(result => result.data);
+      return apiv3Get(endpoint).then((result) => result.data);
     },
     },
     {
     {
       // use: [localStorageMiddleware], // store to localStorage for initialization fastly
       // use: [localStorageMiddleware], // store to localStorage for initialization fastly
@@ -51,7 +64,7 @@ export const useEditorSettings = (): SWRResponseWithUtils<EditorSettingsOperatio
   );
   );
 
 
   return withUtils<EditorSettingsOperation, EditorSettings, Error>(swrResult, {
   return withUtils<EditorSettingsOperation, EditorSettings, Error>(swrResult, {
-    update: async(updateData) => {
+    update: async (updateData) => {
       const { data, mutate } = swrResult;
       const { data, mutate } = swrResult;
 
 
       if (data == null) {
       if (data == null) {
@@ -76,13 +89,18 @@ export const useCurrentIndentSize = (): SWRResponse<number, Error> => {
 };
 };
 
 
 /*
 /*
-* Slack Notification
-*/
-export const useSWRxSlackChannels = (currentPagePath: Nullable<string>): SWRResponse<string[], Error> => {
+ * Slack Notification
+ */
+export const useSWRxSlackChannels = (
+  currentPagePath: Nullable<string>,
+): SWRResponse<string[], Error> => {
   const shouldFetch: boolean = currentPagePath != null;
   const shouldFetch: boolean = currentPagePath != null;
   return useSWR(
   return useSWR(
     shouldFetch ? ['/pages.updatePost', currentPagePath] : null,
     shouldFetch ? ['/pages.updatePost', currentPagePath] : null,
-    ([endpoint, path]) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
+    ([endpoint, path]) =>
+      apiGet(endpoint, { path }).then(
+        (response: SlackChannels) => response.updatePost,
+      ),
     {
     {
       revalidateOnFocus: false,
       revalidateOnFocus: false,
       fallbackData: [''],
       fallbackData: [''],
@@ -91,18 +109,16 @@ export const useSWRxSlackChannels = (currentPagePath: Nullable<string>): SWRResp
 };
 };
 
 
 export const useIsSlackEnabled = (): SWRResponse<boolean, Error> => {
 export const useIsSlackEnabled = (): SWRResponse<boolean, Error> => {
-  return useSWRStatic(
-    'isSlackEnabled',
-    undefined,
-    { fallbackData: false },
-  );
+  return useSWRStatic('isSlackEnabled', undefined, { fallbackData: false });
 };
 };
 
 
 export type IPageTagsForEditorsOption = {
 export type IPageTagsForEditorsOption = {
   sync: (tags?: string[]) => void;
   sync: (tags?: string[]) => void;
-}
+};
 
 
-export const usePageTagsForEditors = (pageId: Nullable<string>): SWRResponse<string[], Error> & IPageTagsForEditorsOption => {
+export const usePageTagsForEditors = (
+  pageId: Nullable<string>,
+): SWRResponse<string[], Error> & IPageTagsForEditorsOption => {
   const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
   const { data: tagsInfoData } = useSWRxTagsInfo(pageId);
   const swrResult = useSWRStatic<string[], Error>('pageTags', undefined);
   const swrResult = useSWRStatic<string[], Error>('pageTags', undefined);
   const { mutate } = swrResult;
   const { mutate } = swrResult;
@@ -120,10 +136,12 @@ export const useIsEnabledUnsavedWarning = (): SWRResponse<boolean, Error> => {
   return useSWRStatic<boolean, Error>('isEnabledUnsavedWarning');
   return useSWRStatic<boolean, Error>('isEnabledUnsavedWarning');
 };
 };
 
 
-
-export const useReservedNextCaretLine = (initialData?: number): SWRResponse<number> => {
-
-  const swrResponse = useSWRStatic('saveNextCaretLine', initialData, { fallbackData: 0 });
+export const useReservedNextCaretLine = (
+  initialData?: number,
+): SWRResponse<number> => {
+  const swrResponse = useSWRStatic('saveNextCaretLine', initialData, {
+    fallbackData: 0,
+  });
   const { mutate } = swrResponse;
   const { mutate } = swrResponse;
 
 
   useEffect(() => {
   useEffect(() => {

+ 18 - 13
apps/app/src/stores/global-notification.ts

@@ -5,24 +5,26 @@ import type { IGlobalNotification } from '~/client/interfaces/global-notificatio
 
 
 import { apiv3Get, apiv3Put } from '../client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '../client/util/apiv3-client';
 
 
-
 type Util = {
 type Util = {
-  update(updateData: any): Promise<void>
+  update(updateData: any): Promise<void>;
 };
 };
 
 
-
-export const useSWRxGlobalNotification = (globalNotificationId: string): SWRResponseWithUtils<Util, any, Error> => {
+export const useSWRxGlobalNotification = (
+  globalNotificationId: string,
+): SWRResponseWithUtils<Util, any, Error> => {
   const swrResult = useSWRImmutable(
   const swrResult = useSWRImmutable(
-    globalNotificationId != null ? `/notification-setting/global-notification/${globalNotificationId}` : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return {
-        globalNotification: response.data.globalNotification,
-      };
-    }),
+    globalNotificationId != null
+      ? `/notification-setting/global-notification/${globalNotificationId}`
+      : null,
+    (endpoint) =>
+      apiv3Get(endpoint).then((response) => {
+        return {
+          globalNotification: response.data.globalNotification,
+        };
+      }),
   );
   );
 
 
-
-  const update = async(updateData: IGlobalNotification) => {
+  const update = async (updateData: IGlobalNotification) => {
     const { data } = swrResult;
     const { data } = swrResult;
 
 
     if (data == null) {
     if (data == null) {
@@ -30,7 +32,10 @@ export const useSWRxGlobalNotification = (globalNotificationId: string): SWRResp
     }
     }
 
 
     // invoke API
     // invoke API
-    await apiv3Put(`/notification-setting/global-notification/${globalNotificationId}`, updateData);
+    await apiv3Put(
+      `/notification-setting/global-notification/${globalNotificationId}`,
+      updateData,
+    );
   };
   };
 
 
   return withUtils<Util, any, Error>(swrResult, { update });
   return withUtils<Util, any, Error>(swrResult, { update });

+ 30 - 25
apps/app/src/stores/in-app-notification.ts

@@ -1,9 +1,12 @@
 import type { SWRConfiguration, SWRResponse } from 'swr';
 import type { SWRConfiguration, SWRResponse } from 'swr';
 import useSWR from 'swr';
 import useSWR from 'swr';
 
 
-
 import { SupportedTargetModel } from '~/interfaces/activity';
 import { SupportedTargetModel } from '~/interfaces/activity';
-import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import type {
+  IInAppNotification,
+  InAppNotificationStatuses,
+  PaginateResult,
+} from '~/interfaces/in-app-notification';
 import * as userSerializers from '~/models/serializers/in-app-notification-snapshot/user';
 import * as userSerializers from '~/models/serializers/in-app-notification-snapshot/user';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -11,38 +14,40 @@ import { apiv3Get } from '../client/util/apiv3-client';
 
 
 const logger = loggerFactory('growi:cli:InAppNotification');
 const logger = loggerFactory('growi:cli:InAppNotification');
 
 
-type inAppNotificationPaginateResult = PaginateResult<IInAppNotification>
+type inAppNotificationPaginateResult = PaginateResult<IInAppNotification>;
 
 
 export const useSWRxInAppNotifications = (
 export const useSWRxInAppNotifications = (
-    limit: number,
-    offset?: number,
-    status?: InAppNotificationStatuses,
-    config?: SWRConfiguration,
+  limit: number,
+  offset?: number,
+  status?: InAppNotificationStatuses,
+  config?: SWRConfiguration,
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
     ['/in-app-notification/list', limit, offset, status],
-    ([endpoint]) => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
-      const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
-      inAppNotificationPaginateResult.docs.forEach((doc) => {
-        try {
-          if (doc.targetModel === SupportedTargetModel.MODEL_USER) {
-            doc.parsedSnapshot = userSerializers.parseSnapshot(doc.snapshot);
+    ([endpoint]) =>
+      apiv3Get(endpoint, { limit, offset, status }).then((response) => {
+        const inAppNotificationPaginateResult =
+          response.data as inAppNotificationPaginateResult;
+        inAppNotificationPaginateResult.docs.forEach((doc) => {
+          try {
+            if (doc.targetModel === SupportedTargetModel.MODEL_USER) {
+              doc.parsedSnapshot = userSerializers.parseSnapshot(doc.snapshot);
+            }
+          } catch (err) {
+            logger.warn('Failed to parse snapshot', err);
           }
           }
-        }
-        catch (err) {
-          logger.warn('Failed to parse snapshot', err);
-        }
-      });
-      return inAppNotificationPaginateResult;
-    }),
+        });
+        return inAppNotificationPaginateResult;
+      }),
     config,
     config,
   );
   );
 };
 };
 
 
-export const useSWRxInAppNotificationStatus = (
-): SWRResponse<number, Error> => {
-  return useSWR(
-    '/in-app-notification/status',
-    endpoint => apiv3Get(endpoint).then(response => response.data.count),
+export const useSWRxInAppNotificationStatus = (): SWRResponse<
+  number,
+  Error
+> => {
+  return useSWR('/in-app-notification/status', (endpoint) =>
+    apiv3Get(endpoint).then((response) => response.data.count),
   );
   );
 };
 };

+ 14 - 9
apps/app/src/stores/maintenanceMode.tsx

@@ -1,26 +1,31 @@
-import { withUtils, type SWRResponseWithUtils } from '@growi/core/dist/swr';
+import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
 
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
 
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
-
 type maintenanceModeUtils = {
 type maintenanceModeUtils = {
-  start(): Promise<void>,
-  end(): Promise<void>,
-}
+  start(): Promise<void>;
+  end(): Promise<void>;
+};
 
 
-export const useIsMaintenanceMode = (initialData?: boolean): SWRResponseWithUtils<maintenanceModeUtils, boolean> => {
-  const swrResult = useStaticSWR<boolean, Error>('isMaintenanceMode', initialData, { fallbackData: false });
+export const useIsMaintenanceMode = (
+  initialData?: boolean,
+): SWRResponseWithUtils<maintenanceModeUtils, boolean> => {
+  const swrResult = useStaticSWR<boolean, Error>(
+    'isMaintenanceMode',
+    initialData,
+    { fallbackData: false },
+  );
 
 
   const utils = {
   const utils = {
-    start: async() => {
+    start: async () => {
       const { mutate } = swrResult;
       const { mutate } = swrResult;
       await apiv3Post('/app-settings/maintenance-mode', { flag: true });
       await apiv3Post('/app-settings/maintenance-mode', { flag: true });
       mutate(true);
       mutate(true);
     },
     },
 
 
-    end: async() => {
+    end: async () => {
       const { mutate } = swrResult;
       const { mutate } = swrResult;
       await apiv3Post('/app-settings/maintenance-mode', { flag: false });
       await apiv3Post('/app-settings/maintenance-mode', { flag: false });
       mutate(false);
       mutate(false);

+ 3 - 2
apps/app/src/stores/middlewares/user.ts

@@ -3,11 +3,12 @@ import type { Middleware, SWRHook } from 'swr';
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
 
-export const checkAndUpdateImageUrlCached: Middleware = (useSWRNext: SWRHook) => {
+export const checkAndUpdateImageUrlCached: Middleware = (
+  useSWRNext: SWRHook,
+) => {
   return (key, fetcher, config) => {
   return (key, fetcher, config) => {
     const swrNext = useSWRNext(key, fetcher, config);
     const swrNext = useSWRNext(key, fetcher, config);
     if (swrNext.data != null) {
     if (swrNext.data != null) {
-
       const userIds = Object(swrNext.data)
       const userIds = Object(swrNext.data)
         .filter((user: IUserHasId) => user.imageUrlCached == null)
         .filter((user: IUserHasId) => user.imageUrlCached == null)
         .map((user: IUserHasId) => user._id);
         .map((user: IUserHasId) => user._id);

File diff suppressed because it is too large
+ 436 - 277
apps/app/src/stores/modal.tsx


+ 119 - 79
apps/app/src/stores/page-listing.tsx

@@ -1,11 +1,17 @@
-import assert from 'assert';
-
 import type {
 import type {
-  Nullable, HasObjectId,
-  IDataWithMeta, IPageHasId, IPageInfoForListing, IPageInfoForOperation,
+  HasObjectId,
+  IDataWithMeta,
+  IPageHasId,
+  IPageInfoForListing,
+  IPageInfoForOperation,
+  Nullable,
 } from '@growi/core';
 } from '@growi/core';
+import assert from 'assert';
 import useSWR, {
 import useSWR, {
-  mutate, type SWRConfiguration, type SWRResponse, type Arguments,
+  type Arguments,
+  mutate,
+  type SWRConfiguration,
+  type SWRResponse,
 } from 'swr';
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 import type { SWRInfiniteResponse } from 'swr/infinite';
 import type { SWRInfiniteResponse } from 'swr/infinite';
@@ -15,48 +21,60 @@ import type { IPagingResult } from '~/interfaces/paging-result';
 
 
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
 import type {
 import type {
-  ChildrenResult, V5MigrationStatus, RootPageResult,
+  ChildrenResult,
+  RootPageResult,
+  V5MigrationStatus,
 } from '../interfaces/page-listing-results';
 } from '../interfaces/page-listing-results';
 
 
-
-export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {
+export const useSWRxPagesByPath = (
+  path?: Nullable<string>,
+): SWRResponse<IPageHasId[], Error> => {
   const findAll = true;
   const findAll = true;
   const includeEmpty = true;
   const includeEmpty = true;
   return useSWR(
   return useSWR(
     path != null ? ['/page', path, findAll, includeEmpty] : null,
     path != null ? ['/page', path, findAll, includeEmpty] : null,
-    ([endpoint, path, findAll, includeEmpty]) => apiv3Get(endpoint, { path, findAll, includeEmpty }).then(result => result.data.pages),
+    ([endpoint, path, findAll, includeEmpty]) =>
+      apiv3Get(endpoint, { path, findAll, includeEmpty }).then(
+        (result) => result.data.pages,
+      ),
   );
   );
 };
 };
 
 
 type RecentApiResult = {
 type RecentApiResult = {
-  pages: IPageHasId[],
-  totalCount: number,
-  offset: number,
-}
+  pages: IPageHasId[];
+  totalCount: number;
+  offset: number;
+};
 
 
 export const getRecentlyUpdatedKey = (
 export const getRecentlyUpdatedKey = (
-    pageIndex: number,
-    previousPageData: RecentApiResult | null,
-    includeWipPage?: boolean,
+  pageIndex: number,
+  previousPageData: RecentApiResult | null,
+  includeWipPage?: boolean,
 ): [string, number | undefined, boolean | undefined] | null => {
 ): [string, number | undefined, boolean | undefined] | null => {
-  if (previousPageData != null && previousPageData.pages.length === 0) return null;
+  if (previousPageData != null && previousPageData.pages.length === 0)
+    return null;
 
 
   if (pageIndex === 0 || previousPageData == null) {
   if (pageIndex === 0 || previousPageData == null) {
     return ['/pages/recent', undefined, includeWipPage];
     return ['/pages/recent', undefined, includeWipPage];
   }
   }
   const offset = previousPageData.offset + previousPageData.pages.length;
   const offset = previousPageData.offset + previousPageData.pages.length;
   return ['/pages/recent', offset, includeWipPage];
   return ['/pages/recent', offset, includeWipPage];
-
 };
 };
 
 
 export const useSWRINFxRecentlyUpdated = (
 export const useSWRINFxRecentlyUpdated = (
-    includeWipPage?: boolean,
-    config?: SWRConfiguration,
+  includeWipPage?: boolean,
+  config?: SWRConfiguration,
 ): SWRInfiniteResponse<RecentApiResult, Error> => {
 ): SWRInfiniteResponse<RecentApiResult, Error> => {
   const PER_PAGE = 20;
   const PER_PAGE = 20;
   return useSWRInfinite(
   return useSWRInfinite(
-    (pageIndex, previousPageData) => getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
-    ([endpoint, offset, includeWipPage]) => apiv3Get<RecentApiResult>(endpoint, { offset, limit: PER_PAGE, includeWipPage }).then(response => response.data),
+    (pageIndex, previousPageData) =>
+      getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
+    ([endpoint, offset, includeWipPage]) =>
+      apiv3Get<RecentApiResult>(endpoint, {
+        offset,
+        limit: PER_PAGE,
+        includeWipPage,
+      }).then((response) => response.data),
     {
     {
       ...config,
       ...config,
       revalidateFirstPage: false,
       revalidateFirstPage: false,
@@ -65,43 +83,46 @@ export const useSWRINFxRecentlyUpdated = (
   );
   );
 };
 };
 
 
-export const mutateRecentlyUpdated = async(): Promise<undefined> => {
-  [true, false].forEach(includeWipPage => mutate(
-    unstable_serialize(
-      (pageIndex, previousPageData) => getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
-    ),
-  ));
+export const mutateRecentlyUpdated = async (): Promise<undefined> => {
+  [true, false].forEach((includeWipPage) => {
+    mutate(
+      unstable_serialize((pageIndex, previousPageData) =>
+        getRecentlyUpdatedKey(pageIndex, previousPageData, includeWipPage),
+      ),
+    );
+  });
   return;
   return;
 };
 };
 
 
-export const mutatePageList = async(): Promise<void[]> => {
-  return mutate(
-    key => Array.isArray(key) && key[0] === '/pages/list',
-  );
+export const mutatePageList = async (): Promise<void[]> => {
+  return mutate((key) => Array.isArray(key) && key[0] === '/pages/list');
 };
 };
 
 
 export const useSWRxPageList = (
 export const useSWRxPageList = (
-    path: string | null, pageNumber?: number, limit?: number,
+  path: string | null,
+  pageNumber?: number,
+  limit?: number,
 ): SWRResponse<IPagingResult<IPageHasId>, Error> => {
 ): SWRResponse<IPagingResult<IPageHasId>, Error> => {
   return useSWR(
   return useSWR(
-    path == null
-      ? null
-      : ['/pages/list', path, pageNumber, limit],
+    path == null ? null : ['/pages/list', path, pageNumber, limit],
     ([endpoint, path, pageNumber, limit]) => {
     ([endpoint, path, pageNumber, limit]) => {
       const args = Object.assign(
       const args = Object.assign(
         { path, page: pageNumber ?? 1 },
         { path, page: pageNumber ?? 1 },
         // if limit exist then add it as query string
         // if limit exist then add it as query string
-        (limit != null) ? { limit } : {},
+        limit != null ? { limit } : {},
       );
       );
 
 
-      return apiv3Get<{pages: IPageHasId[], totalCount: number, limit: number}>(endpoint, args)
-        .then((response) => {
-          return {
-            items: response.data.pages,
-            totalCount: response.data.totalCount,
-            limit: response.data.limit,
-          };
-        });
+      return apiv3Get<{
+        pages: IPageHasId[];
+        totalCount: number;
+        limit: number;
+      }>(endpoint, args).then((response) => {
+        return {
+          items: response.data.pages,
+          totalCount: response.data.totalCount,
+          limit: response.data.limit,
+        };
+      });
     },
     },
     {
     {
       keepPreviousData: true,
       keepPreviousData: true,
@@ -109,33 +130,44 @@ export const useSWRxPageList = (
   );
   );
 };
 };
 
 
-
 type PageInfoInjector = {
 type PageInfoInjector = {
-  injectTo: <D extends HasObjectId>(pages: (D | IDataWithMeta<D>)[]) => IDataWithMeta<D, IPageInfoForOperation>[],
-}
+  injectTo: <D extends HasObjectId>(
+    pages: (D | IDataWithMeta<D>)[],
+  ) => IDataWithMeta<D, IPageInfoForOperation>[];
+};
 
 
-const isIDataWithMeta = (item: HasObjectId | IDataWithMeta): item is IDataWithMeta => {
+const isIDataWithMeta = (
+  item: HasObjectId | IDataWithMeta,
+): item is IDataWithMeta => {
   return 'data' in item;
   return 'data' in item;
 };
 };
 
 
 export const useSWRxPageInfoForList = (
 export const useSWRxPageInfoForList = (
-    pageIds: string[] | null | undefined,
-    path: string | null | undefined = null,
-    attachBookmarkCount = false,
-    attachShortBody = false,
-): SWRResponse<Record<string, IPageInfoForListing>, Error> & PageInfoInjector => {
-
+  pageIds: string[] | null | undefined,
+  path: string | null | undefined = null,
+  attachBookmarkCount = false,
+  attachShortBody = false,
+): SWRResponse<Record<string, IPageInfoForListing>, Error> &
+  PageInfoInjector => {
   const shouldFetch = (pageIds != null && pageIds.length > 0) || path != null;
   const shouldFetch = (pageIds != null && pageIds.length > 0) || path != null;
 
 
   const swrResult = useSWRImmutable(
   const swrResult = useSWRImmutable(
-    shouldFetch ? ['/page-listing/info', pageIds, path, attachBookmarkCount, attachShortBody] : null,
+    shouldFetch
+      ? [
+          '/page-listing/info',
+          pageIds,
+          path,
+          attachBookmarkCount,
+          attachShortBody,
+        ]
+      : null,
     ([endpoint, pageIds, path, attachBookmarkCount, attachShortBody]) => {
     ([endpoint, pageIds, path, attachBookmarkCount, attachShortBody]) => {
       return apiv3Get(endpoint, {
       return apiv3Get(endpoint, {
         pageIds: pageIds != null ? pageIds : undefined, // Do not pass null to avoid empty query parameter
         pageIds: pageIds != null ? pageIds : undefined, // Do not pass null to avoid empty query parameter
         path: path != null ? path : undefined, // Do not pass null to avoid empty query parameter
         path: path != null ? path : undefined, // Do not pass null to avoid empty query parameter
         attachBookmarkCount,
         attachBookmarkCount,
         attachShortBody,
         attachShortBody,
-      }).then(response => response.data);
+      }).then((response) => response.data);
     },
     },
   );
   );
 
 
@@ -158,14 +190,17 @@ export const useSWRxPageInfoForList = (
   };
   };
 };
 };
 
 
-export const useSWRxRootPage = (config?: SWRConfiguration): SWRResponse<RootPageResult, Error> => {
+export const useSWRxRootPage = (
+  config?: SWRConfiguration,
+): SWRResponse<RootPageResult, Error> => {
   return useSWR(
   return useSWR(
     '/page-listing/root',
     '/page-listing/root',
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return {
-        rootPage: response.data.rootPage,
-      };
-    }),
+    (endpoint) =>
+      apiv3Get(endpoint).then((response) => {
+        return {
+          rootPage: response.data.rootPage,
+        };
+      }),
     {
     {
       ...config,
       ...config,
       keepPreviousData: true,
       keepPreviousData: true,
@@ -179,15 +214,16 @@ const MUTATION_ID_FOR_PAGETREE = 'pageTree';
 const keyMatcherForPageTree = (key: Arguments): boolean => {
 const keyMatcherForPageTree = (key: Arguments): boolean => {
   return Array.isArray(key) && key[0] === MUTATION_ID_FOR_PAGETREE;
   return Array.isArray(key) && key[0] === MUTATION_ID_FOR_PAGETREE;
 };
 };
-export const mutatePageTree = async(): Promise<undefined[]> => {
+export const mutatePageTree = async (): Promise<undefined[]> => {
   return mutate(keyMatcherForPageTree);
   return mutate(keyMatcherForPageTree);
 };
 };
 
 
-
 export const useSWRxPageChildren = (
 export const useSWRxPageChildren = (
-    id?: string | null,
+  id?: string | null,
 ): SWRResponse<ChildrenResult, Error> => {
 ): SWRResponse<ChildrenResult, Error> => {
-  const key = id ? [MUTATION_ID_FOR_PAGETREE, '/page-listing/children', id] : null;
+  const key = id
+    ? [MUTATION_ID_FOR_PAGETREE, '/page-listing/children', id]
+    : null;
 
 
   if (key != null) {
   if (key != null) {
     assert(keyMatcherForPageTree(key));
     assert(keyMatcherForPageTree(key));
@@ -195,11 +231,12 @@ export const useSWRxPageChildren = (
 
 
   return useSWR(
   return useSWR(
     key,
     key,
-    ([, endpoint, id]) => apiv3Get(endpoint, { id }).then((response) => {
-      return {
-        children: response.data.children,
-      };
-    }),
+    ([, endpoint, id]) =>
+      apiv3Get(endpoint, { id }).then((response) => {
+        return {
+          children: response.data.children,
+        };
+      }),
     {
     {
       keepPreviousData: true,
       keepPreviousData: true,
       revalidateOnFocus: false,
       revalidateOnFocus: false,
@@ -208,15 +245,18 @@ export const useSWRxPageChildren = (
   );
   );
 };
 };
 
 
-export const useSWRxV5MigrationStatus = (config?: SWRConfiguration): SWRResponse<V5MigrationStatus, Error> => {
+export const useSWRxV5MigrationStatus = (
+  config?: SWRConfiguration,
+): SWRResponse<V5MigrationStatus, Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     '/pages/v5-migration-status',
     '/pages/v5-migration-status',
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return {
-        isV5Compatible: response.data.isV5Compatible,
-        migratablePagesCount: response.data.migratablePagesCount,
-      };
-    }),
+    (endpoint) =>
+      apiv3Get(endpoint).then((response) => {
+        return {
+          isV5Compatible: response.data.isV5Compatible,
+          migratablePagesCount: response.data.migratablePagesCount,
+        };
+      }),
     config,
     config,
   );
   );
 };
 };

+ 3 - 2
apps/app/src/stores/page-redirect.tsx

@@ -2,7 +2,8 @@ import type { SWRResponse } from 'swr';
 
 
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
-
-export const useRedirectFrom = (initialData?: string | null): SWRResponse<string | null, Error> => {
+export const useRedirectFrom = (
+  initialData?: string | null,
+): SWRResponse<string | null, Error> => {
   return useStaticSWR('redirectFrom', initialData);
   return useStaticSWR('redirectFrom', initialData);
 };
 };

+ 14 - 9
apps/app/src/stores/page-timeline.tsx

@@ -1,25 +1,30 @@
-
 import type { IPageHasId } from '@growi/core';
 import type { IPageHasId } from '@growi/core';
 import type { SWRInfiniteResponse } from 'swr/infinite';
 import type { SWRInfiniteResponse } from 'swr/infinite';
 import useSWRInfinite from 'swr/infinite';
 import useSWRInfinite from 'swr/infinite';
 
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 
 
-
 type PageTimelineResult = {
 type PageTimelineResult = {
-  pages: IPageHasId[],
-  totalCount: number,
-  offset: number,
-}
-export const useSWRINFxPageTimeline = (path: string | undefined, limit: number) : SWRInfiniteResponse<PageTimelineResult, Error> => {
+  pages: IPageHasId[];
+  totalCount: number;
+  offset: number;
+};
+export const useSWRINFxPageTimeline = (
+  path: string | undefined,
+  limit: number,
+): SWRInfiniteResponse<PageTimelineResult, Error> => {
   return useSWRInfinite(
   return useSWRInfinite(
     (pageIndex, previousPageData) => {
     (pageIndex, previousPageData) => {
-      if (previousPageData != null && previousPageData.pages.length === 0) return null;
+      if (previousPageData != null && previousPageData.pages.length === 0)
+        return null;
       if (path === undefined) return null;
       if (path === undefined) return null;
 
 
       return ['/pages/list', path, pageIndex + 1, limit];
       return ['/pages/list', path, pageIndex + 1, limit];
     },
     },
-    ([endpoint, path, page, limit]) => apiv3Get<PageTimelineResult>(endpoint, { path, page, limit }).then(response => response.data),
+    ([endpoint, path, page, limit]) =>
+      apiv3Get<PageTimelineResult>(endpoint, { path, page, limit }).then(
+        (response) => response.data,
+      ),
     {
     {
       revalidateFirstPage: false,
       revalidateFirstPage: false,
       revalidateAll: false,
       revalidateAll: false,

+ 184 - 107
apps/app/src/stores/page.tsx

@@ -1,16 +1,22 @@
 import { useEffect, useMemo } from 'react';
 import { useEffect, useMemo } from 'react';
-
 import type {
 import type {
-  Ref, Nullable,
-  IPageInfoForEntity, IPagePopulatedToShowRevision,
+  IPageInfo,
+  IPageInfoForEntity,
+  IPageInfoForOperation,
+  IPagePopulatedToShowRevision,
+  IRevision,
+  IRevisionHasId,
+  Nullable,
+  Ref,
   SWRInfinitePageRevisionsResponse,
   SWRInfinitePageRevisionsResponse,
-  IPageInfo, IPageInfoForOperation,
-  IRevision, IRevisionHasId,
 } from '@growi/core';
 } from '@growi/core';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import { isClient, pagePathUtils } from '@growi/core/dist/utils';
 import { isClient, pagePathUtils } from '@growi/core/dist/utils';
 import useSWR, {
 import useSWR, {
-  mutate, useSWRConfig, type SWRResponse, type SWRConfiguration,
+  mutate,
+  type SWRConfiguration,
+  type SWRResponse,
+  useSWRConfig,
 } from 'swr';
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
 import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
@@ -19,43 +25,59 @@ import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import type { IPagePathWithDescendantCount } from '~/interfaces/page';
 import type { IPagePathWithDescendantCount } from '~/interfaces/page';
-import type { IRecordApplicableGrant, IResCurrentGrantData } from '~/interfaces/page-grant';
+import type {
+  IRecordApplicableGrant,
+  IResCurrentGrantData,
+} from '~/interfaces/page-grant';
 import {
 import {
-  useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
+  useCurrentPathname,
+  useIsGuestUser,
+  useIsReadOnlyUser,
+  useShareLinkId,
 } from '~/stores-universal/context';
 } from '~/stores-universal/context';
 import type { AxiosResponse } from '~/utils/axios';
 import type { AxiosResponse } from '~/utils/axios';
 
 
 import type { IPageTagsInfo } from '../interfaces/tag';
 import type { IPageTagsInfo } from '../interfaces/tag';
-
-
 import { useRemoteRevisionId } from './remote-latest-page';
 import { useRemoteRevisionId } from './remote-latest-page';
 
 
-
 const { isPermalink: _isPermalink } = pagePathUtils;
 const { isPermalink: _isPermalink } = pagePathUtils;
 
 
-
-export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
+export const useCurrentPageId = (
+  initialData?: Nullable<string>,
+): SWRResponse<Nullable<string>, Error> => {
   return useSWRStatic<Nullable<string>, Error>('currentPageId', initialData);
   return useSWRStatic<Nullable<string>, Error>('currentPageId', initialData);
 };
 };
 
 
-export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
+export const useIsLatestRevision = (
+  initialData?: boolean,
+): SWRResponse<boolean, any> => {
   return useSWRStatic('isLatestRevision', initialData);
   return useSWRStatic('isLatestRevision', initialData);
 };
 };
 
 
-export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useSWRStatic<boolean, Error>('isNotFound', initialData, { fallbackData: false });
+export const useIsNotFound = (
+  initialData?: boolean,
+): SWRResponse<boolean, Error> => {
+  return useSWRStatic<boolean, Error>('isNotFound', initialData, {
+    fallbackData: false,
+  });
 };
 };
 
 
-export const useTemplateTagData = (initialData?: string[]): SWRResponse<string[], Error> => {
+export const useTemplateTagData = (
+  initialData?: string[],
+): SWRResponse<string[], Error> => {
   return useSWRStatic<string[], Error>('templateTagData', initialData);
   return useSWRStatic<string[], Error>('templateTagData', initialData);
 };
 };
 
 
-export const useTemplateBodyData = (initialData?: string): SWRResponse<string, Error> => {
+export const useTemplateBodyData = (
+  initialData?: string,
+): SWRResponse<string, Error> => {
   return useSWRStatic<string, Error>('templateBodyData', initialData);
   return useSWRStatic<string, Error>('templateBodyData', initialData);
 };
 };
 
 
 /** "useSWRxCurrentPage" is intended for initial data retrieval only. Use "useSWRMUTxCurrentPage" for revalidation */
 /** "useSWRxCurrentPage" is intended for initial data retrieval only. Use "useSWRMUTxCurrentPage" for revalidation */
-export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision | null): SWRResponse<IPagePopulatedToShowRevision | null> => {
+export const useSWRxCurrentPage = (
+  initialData?: IPagePopulatedToShowRevision | null,
+): SWRResponse<IPagePopulatedToShowRevision | null> => {
   const key = 'currentPage';
   const key = 'currentPage';
 
 
   const { data: isLatestRevision } = useIsLatestRevision();
   const { data: isLatestRevision } = useIsLatestRevision();
@@ -76,7 +98,8 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision |
       return true;
       return true;
     }
     }
 
 
-    const cachedData = cache.get(key)?.data as IPagePopulatedToShowRevision | null;
+    const cachedData = cache.get(key)
+      ?.data as IPagePopulatedToShowRevision | null;
     if (initialData._id !== cachedData?._id) {
     if (initialData._id !== cachedData?._id) {
       return true;
       return true;
     }
     }
@@ -87,9 +110,11 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision |
     }
     }
 
 
     // mutate when opening a previous revision.
     // mutate when opening a previous revision.
-    if (!isLatestRevision
-      && cachedData.revision?._id != null && initialData.revision?._id != null
-      && cachedData.revision._id !== initialData.revision._id
+    if (
+      !isLatestRevision &&
+      cachedData.revision?._id != null &&
+      initialData.revision?._id != null &&
+      cachedData.revision._id !== initialData.revision._id
     ) {
     ) {
       return true;
       return true;
     }
     }
@@ -105,7 +130,7 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision |
         revalidate: false,
         revalidate: false,
       });
       });
     }
     }
-  }, [initialData, key, shouldMutate]);
+  }, [initialData, shouldMutate]);
 
 
   return useSWR(key, null, {
   return useSWR(key, null, {
     keepPreviousData: true,
     keepPreviousData: true,
@@ -113,7 +138,9 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision |
 };
 };
 
 
 const getPageApiErrorHandler = (errs: AxiosResponse[]) => {
 const getPageApiErrorHandler = (errs: AxiosResponse[]) => {
-  if (!Array.isArray(errs)) { throw Error('error is not array') }
+  if (!Array.isArray(errs)) {
+    throw Error('error is not array');
+  }
 
 
   const statusCode = errs[0].status;
   const statusCode = errs[0].status;
   if (statusCode === 403 || statusCode === 404) {
   if (statusCode === 403 || statusCode === 404) {
@@ -123,45 +150,55 @@ const getPageApiErrorHandler = (errs: AxiosResponse[]) => {
   throw Error('failed to get page');
   throw Error('failed to get page');
 };
 };
 
 
-export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToShowRevision | null> => {
-  const key = 'currentPage';
-
-  const { data: currentPageId } = useCurrentPageId();
-  const { data: shareLinkId } = useShareLinkId();
+export const useSWRMUTxCurrentPage =
+  (): SWRMutationResponse<IPagePopulatedToShowRevision | null> => {
+    const key = 'currentPage';
 
 
-  // Get URL parameter for specific revisionId
-  let revisionId: string | undefined;
-  if (isClient()) {
-    const urlParams = new URLSearchParams(window.location.search);
-    const requestRevisionId = urlParams.get('revisionId');
-    revisionId = requestRevisionId != null ? requestRevisionId : undefined;
-  }
+    const { data: currentPageId } = useCurrentPageId();
+    const { data: shareLinkId } = useShareLinkId();
 
 
-  return useSWRMutation(
-    key,
-    () => apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', { pageId: currentPageId, shareLinkId, revisionId })
-      .then((result) => {
-        const newData = result.data.page;
-
-        // for the issue https://redmine.weseek.co.jp/issues/156150
-        mutate('currentPage', newData, false);
+    // Get URL parameter for specific revisionId
+    let revisionId: string | undefined;
+    if (isClient()) {
+      const urlParams = new URLSearchParams(window.location.search);
+      const requestRevisionId = urlParams.get('revisionId');
+      revisionId = requestRevisionId != null ? requestRevisionId : undefined;
+    }
 
 
-        return newData;
-      })
-      .catch(getPageApiErrorHandler),
-    {
-      populateCache: true,
-      revalidate: false,
-    },
-  );
-};
+    return useSWRMutation(
+      key,
+      () =>
+        apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', {
+          pageId: currentPageId,
+          shareLinkId,
+          revisionId,
+        })
+          .then((result) => {
+            const newData = result.data.page;
+
+            // for the issue https://redmine.weseek.co.jp/issues/156150
+            mutate('currentPage', newData, false);
+
+            return newData;
+          })
+          .catch(getPageApiErrorHandler),
+      {
+        populateCache: true,
+        revalidate: false,
+      },
+    );
+  };
 
 
-export const useSWRxPageByPath = (path?: string, config?: SWRConfiguration): SWRResponse<IPagePopulatedToShowRevision | null, Error> => {
+export const useSWRxPageByPath = (
+  path?: string,
+  config?: SWRConfiguration,
+): SWRResponse<IPagePopulatedToShowRevision | null, Error> => {
   return useSWR(
   return useSWR(
     path != null ? ['/page', path] : null,
     path != null ? ['/page', path] : null,
-    ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path })
-      .then(result => result.data.page)
-      .catch(getPageApiErrorHandler),
+    ([endpoint, path]) =>
+      apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path })
+        .then((result) => result.data.page)
+        .catch(getPageApiErrorHandler),
     {
     {
       ...config,
       ...config,
       keepPreviousData: true,
       keepPreviousData: true,
@@ -171,16 +208,20 @@ export const useSWRxPageByPath = (path?: string, config?: SWRConfiguration): SWR
   );
   );
 };
 };
 
 
-export const useSWRxTagsInfo = (pageId: Nullable<string>, config?: SWRConfiguration): SWRResponse<IPageTagsInfo | null, Error> => {
+export const useSWRxTagsInfo = (
+  pageId: Nullable<string>,
+  config?: SWRConfiguration,
+): SWRResponse<IPageTagsInfo | null, Error> => {
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
 
 
   const endpoint = `/pages.getPageTag?pageId=${pageId}`;
   const endpoint = `/pages.getPageTag?pageId=${pageId}`;
 
 
   return useSWR(
   return useSWR(
     shareLinkId == null && pageId != null ? [endpoint, pageId] : null,
     shareLinkId == null && pageId != null ? [endpoint, pageId] : null,
-    ([endpoint, pageId]) => apiGet<IPageTagsInfo>(endpoint, { pageId })
-      .then(result => result)
-      .catch(getPageApiErrorHandler),
+    ([endpoint, pageId]) =>
+      apiGet<IPageTagsInfo>(endpoint, { pageId })
+        .then((result) => result)
+        .catch(getPageApiErrorHandler),
     {
     {
       ...config,
       ...config,
       revalidateOnFocus: false,
       revalidateOnFocus: false,
@@ -190,17 +231,14 @@ export const useSWRxTagsInfo = (pageId: Nullable<string>, config?: SWRConfigurat
 };
 };
 
 
 export const mutateAllPageInfo = (): Promise<void[]> => {
 export const mutateAllPageInfo = (): Promise<void[]> => {
-  return mutate(
-    key => Array.isArray(key) && key[0] === '/page/info',
-  );
+  return mutate((key) => Array.isArray(key) && key[0] === '/page/info');
 };
 };
 
 
 export const useSWRxPageInfo = (
 export const useSWRxPageInfo = (
-    pageId: string | null | undefined,
-    shareLinkId?: string | null,
-    initialData?: IPageInfoForEntity,
+  pageId: string | null | undefined,
+  shareLinkId?: string | null,
+  initialData?: IPageInfoForEntity,
 ): SWRResponse<IPageInfo | IPageInfoForOperation> => {
 ): SWRResponse<IPageInfo | IPageInfoForOperation> => {
-
   // Cache remains from guest mode when logging in via the Login lead, so add 'isGuestUser' key
   // Cache remains from guest mode when logging in via the Login lead, so add 'isGuestUser' key
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
@@ -208,12 +246,17 @@ export const useSWRxPageInfo = (
   const fixedShareLinkId = shareLinkId ?? null;
   const fixedShareLinkId = shareLinkId ?? null;
 
 
   const key = useMemo(() => {
   const key = useMemo(() => {
-    return pageId != null ? ['/page/info', pageId, fixedShareLinkId, isGuestUser] : null;
+    return pageId != null
+      ? ['/page/info', pageId, fixedShareLinkId, isGuestUser]
+      : null;
   }, [fixedShareLinkId, isGuestUser, pageId]);
   }, [fixedShareLinkId, isGuestUser, pageId]);
 
 
   const swrResult = useSWRImmutable(
   const swrResult = useSWRImmutable(
     key,
     key,
-    ([endpoint, pageId, shareLinkId]: [string, string, string | null]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
+    ([endpoint, pageId, shareLinkId]: [string, string, string | null]) =>
+      apiv3Get(endpoint, { pageId, shareLinkId }).then(
+        (response) => response.data,
+      ),
     { fallbackData: initialData },
     { fallbackData: initialData },
   );
   );
 
 
@@ -231,10 +274,9 @@ export const useSWRxPageInfo = (
 };
 };
 
 
 export const useSWRMUTxPageInfo = (
 export const useSWRMUTxPageInfo = (
-    pageId: string | null | undefined,
-    shareLinkId?: string | null,
+  pageId: string | null | undefined,
+  shareLinkId?: string | null,
 ): SWRMutationResponse<IPageInfo | IPageInfoForOperation> => {
 ): SWRMutationResponse<IPageInfo | IPageInfoForOperation> => {
-
   // Cache remains from guest mode when logging in via the Login lead, so add 'isGuestUser' key
   // Cache remains from guest mode when logging in via the Login lead, so add 'isGuestUser' key
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
@@ -242,20 +284,29 @@ export const useSWRMUTxPageInfo = (
   const fixedShareLinkId = shareLinkId ?? null;
   const fixedShareLinkId = shareLinkId ?? null;
 
 
   const key = useMemo(() => {
   const key = useMemo(() => {
-    return pageId != null ? ['/page/info', pageId, fixedShareLinkId, isGuestUser] : null;
+    return pageId != null
+      ? ['/page/info', pageId, fixedShareLinkId, isGuestUser]
+      : null;
   }, [fixedShareLinkId, isGuestUser, pageId]);
   }, [fixedShareLinkId, isGuestUser, pageId]);
 
 
   return useSWRMutation(
   return useSWRMutation(
     key,
     key,
-    ([endpoint, pageId, shareLinkId]: [string, string, string | null]) => apiv3Get(endpoint, { pageId, shareLinkId }).then(response => response.data),
+    ([endpoint, pageId, shareLinkId]: [string, string, string | null]) =>
+      apiv3Get(endpoint, { pageId, shareLinkId }).then(
+        (response) => response.data,
+      ),
   );
   );
 };
 };
 
 
-export const useSWRxPageRevision = (pageId: string, revisionId: Ref<IRevision>): SWRResponse<IRevisionHasId> => {
+export const useSWRxPageRevision = (
+  pageId: string,
+  revisionId: Ref<IRevision>,
+): SWRResponse<IRevisionHasId> => {
   const key = [`/revisions/${revisionId}`, pageId, revisionId];
   const key = [`/revisions/${revisionId}`, pageId, revisionId];
-  return useSWRImmutable(
-    key,
-    () => apiv3Get<{ revision: IRevisionHasId }>(`/revisions/${revisionId}`, { pageId }).then(response => response.data.revision),
+  return useSWRImmutable(key, () =>
+    apiv3Get<{ revision: IRevisionHasId }>(`/revisions/${revisionId}`, {
+      pageId,
+    }).then((response) => response.data.revision),
   );
   );
 };
 };
 
 
@@ -264,12 +315,16 @@ export const useSWRxPageRevision = (pageId: string, revisionId: Ref<IRevision>):
  */
  */
 
 
 export const useSWRxInfinitePageRevisions = (
 export const useSWRxInfinitePageRevisions = (
-    pageId: string,
-    limit: number,
+  pageId: string,
+  limit: number,
 ): SWRInfiniteResponse<SWRInfinitePageRevisionsResponse, Error> => {
 ): SWRInfiniteResponse<SWRInfinitePageRevisionsResponse, Error> => {
   return useSWRInfinite(
   return useSWRInfinite(
     (pageIndex, previousRevisionData) => {
     (pageIndex, previousRevisionData) => {
-      if (previousRevisionData != null && previousRevisionData.revisions.length === 0) return null;
+      if (
+        previousRevisionData != null &&
+        previousRevisionData.revisions.length === 0
+      )
+        return null;
 
 
       if (pageIndex === 0 || previousRevisionData == null) {
       if (pageIndex === 0 || previousRevisionData == null) {
         return ['/revisions/list', pageId, undefined, limit];
         return ['/revisions/list', pageId, undefined, limit];
@@ -277,7 +332,12 @@ export const useSWRxInfinitePageRevisions = (
       const offset = previousRevisionData.offset + limit;
       const offset = previousRevisionData.offset + limit;
       return ['/revisions/list', pageId, offset, limit];
       return ['/revisions/list', pageId, offset, limit];
     },
     },
-    ([endpoint, pageId, offset, limit]) => apiv3Get<SWRInfinitePageRevisionsResponse>(endpoint, { pageId, offset, limit }).then(response => response.data),
+    ([endpoint, pageId, offset, limit]) =>
+      apiv3Get<SWRInfinitePageRevisionsResponse>(endpoint, {
+        pageId,
+        offset,
+        limit,
+      }).then((response) => response.data),
     {
     {
       revalidateFirstPage: true,
       revalidateFirstPage: true,
       revalidateAll: false,
       revalidateAll: false,
@@ -289,39 +349,40 @@ export const useSWRxInfinitePageRevisions = (
  * Grant data fetching hooks
  * Grant data fetching hooks
  */
  */
 export const useSWRxCurrentGrantData = (
 export const useSWRxCurrentGrantData = (
-    pageId: string | null | undefined,
+  pageId: string | null | undefined,
 ): SWRResponse<IResCurrentGrantData, Error> => {
 ): SWRResponse<IResCurrentGrantData, Error> => {
-
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
 
 
-  const key = !isGuestUser && !isReadOnlyUser && !isNotFound && pageId != null
-    ? ['/page/grant-data', pageId]
-    : null;
+  const key =
+    !isGuestUser && !isReadOnlyUser && !isNotFound && pageId != null
+      ? ['/page/grant-data', pageId]
+      : null;
 
 
-  return useSWR(
-    key,
-    ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
+  return useSWR(key, ([endpoint, pageId]) =>
+    apiv3Get(endpoint, { pageId }).then((response) => response.data),
   );
   );
 };
 };
 
 
 export const useSWRxApplicableGrant = (
 export const useSWRxApplicableGrant = (
-    pageId: string | null | undefined,
+  pageId: string | null | undefined,
 ): SWRResponse<IRecordApplicableGrant, Error> => {
 ): SWRResponse<IRecordApplicableGrant, Error> => {
-
   return useSWR(
   return useSWR(
     pageId != null ? ['/page/applicable-grant', pageId] : null,
     pageId != null ? ['/page/applicable-grant', pageId] : null,
-    ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
+    ([endpoint, pageId]) =>
+      apiv3Get(endpoint, { pageId }).then((response) => response.data),
   );
   );
 };
 };
 
 
-
 /** **********************************************************
 /** **********************************************************
  *                     Computed states
  *                     Computed states
  *********************************************************** */
  *********************************************************** */
 
 
-export const useCurrentPagePath = (): SWRResponse<string | undefined, Error> => {
+export const useCurrentPagePath = (): SWRResponse<
+  string | undefined,
+  Error
+> => {
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
 
 
@@ -359,21 +420,37 @@ export const useIsRevisionOutdated = (): SWRResponse<boolean, Error> => {
   const currentRevisionId = currentPage?.revision?._id;
   const currentRevisionId = currentPage?.revision?._id;
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    currentRevisionId != null && remoteRevisionId != null ? ['useIsRevisionOutdated', currentRevisionId, remoteRevisionId] : null,
-    ([, remoteRevisionId, currentRevisionId]) => { return remoteRevisionId !== currentRevisionId },
+    currentRevisionId != null && remoteRevisionId != null
+      ? ['useIsRevisionOutdated', currentRevisionId, remoteRevisionId]
+      : null,
+    ([, remoteRevisionId, currentRevisionId]) => {
+      return remoteRevisionId !== currentRevisionId;
+    },
   );
   );
 };
 };
 
 
-
 export const useSWRxPagePathsWithDescendantCount = (
 export const useSWRxPagePathsWithDescendantCount = (
-    paths?: string[], userGroups?: string[], isIncludeEmpty?: boolean, includeAnyoneWithTheLink?: boolean,
+  paths?: string[],
+  userGroups?: string[],
+  isIncludeEmpty?: boolean,
+  includeAnyoneWithTheLink?: boolean,
 ): SWRResponse<IPagePathWithDescendantCount[], Error> => {
 ): SWRResponse<IPagePathWithDescendantCount[], Error> => {
   return useSWR(
   return useSWR(
-    (paths != null && paths.length !== 0) ? ['/page/page-paths-with-descendant-count', paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink] : null,
-    ([endpoint, paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink]) => apiv3Get(
-      endpoint, {
-        paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink,
-      },
-    ).then(result => result.data.pagePathsWithDescendantCount),
+    paths != null && paths.length !== 0
+      ? [
+          '/page/page-paths-with-descendant-count',
+          paths,
+          userGroups,
+          isIncludeEmpty,
+          includeAnyoneWithTheLink,
+        ]
+      : null,
+    ([endpoint, paths, userGroups, isIncludeEmpty, includeAnyoneWithTheLink]) =>
+      apiv3Get(endpoint, {
+        paths,
+        userGroups,
+        isIncludeEmpty,
+        includeAnyoneWithTheLink,
+      }).then((result) => result.data.pagePathsWithDescendantCount),
   );
   );
 };
 };

+ 70 - 46
apps/app/src/stores/personal-settings.tsx

@@ -1,61 +1,81 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
-import type { HasObjectId, IExternalAccount, IUser } from '@growi/core/dist/interfaces';
+import type {
+  HasObjectId,
+  IExternalAccount,
+  IUser,
+} from '@growi/core/dist/interfaces';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import type { SWRConfiguration, SWRResponse } from 'swr';
 import type { SWRConfiguration, SWRResponse } from 'swr';
 import useSWR from 'swr';
 import useSWR from 'swr';
 
 
 import type {
 import type {
-  IResGenerateAccessToken, IResGetAccessToken, IAccessTokenInfo,
+  IAccessTokenInfo,
+  IResGenerateAccessToken,
+  IResGetAccessToken,
 } from '~/interfaces/access-token';
 } from '~/interfaces/access-token';
 import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import { useIsGuestUser } from '~/stores-universal/context';
 import { useIsGuestUser } from '~/stores-universal/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import {
 import {
-  apiv3Delete, apiv3Get, apiv3Put, apiv3Post,
+  apiv3Delete,
+  apiv3Get,
+  apiv3Post,
+  apiv3Put,
 } from '../client/util/apiv3-client';
 } from '../client/util/apiv3-client';
-
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
-
 const logger = loggerFactory('growi:stores:personal-settings');
 const logger = loggerFactory('growi:stores:personal-settings');
 
 
-
-export const useSWRxPersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> => {
+export const useSWRxPersonalSettings = (
+  config?: SWRConfiguration,
+): SWRResponse<IUser, Error> => {
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
   const key = !isGuestUser ? '/personal-setting' : null;
   const key = !isGuestUser ? '/personal-setting' : null;
 
 
   return useSWR(
   return useSWR(
     key,
     key,
-    endpoint => apiv3Get(endpoint).then(response => response.data.currentUser),
+    (endpoint) =>
+      apiv3Get(endpoint).then((response) => response.data.currentUser),
     config,
     config,
   );
   );
 };
 };
 
 
 export type IPersonalSettingsInfoOption = {
 export type IPersonalSettingsInfoOption = {
-  sync: () => void,
-  updateBasicInfo: () => Promise<void>,
-  associateLdapAccount: (account: { username: string, password: string }) => Promise<void>,
-  disassociateLdapAccount: (account: { providerType: IExternalAuthProviderType, accountId: string }) => Promise<void>,
-}
+  sync: () => void;
+  updateBasicInfo: () => Promise<void>;
+  associateLdapAccount: (account: {
+    username: string;
+    password: string;
+  }) => Promise<void>;
+  disassociateLdapAccount: (account: {
+    providerType: IExternalAuthProviderType;
+    accountId: string;
+  }) => Promise<void>;
+};
 
 
-export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
+export const usePersonalSettings = (
+  config?: SWRConfiguration,
+): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
   const { i18n } = useTranslation();
   const { i18n } = useTranslation();
-  const { data: personalSettingsDataFromDB, mutate: revalidate } = useSWRxPersonalSettings(config);
-  const key = personalSettingsDataFromDB != null ? 'personalSettingsInfo' : null;
+  const { data: personalSettingsDataFromDB, mutate: revalidate } =
+    useSWRxPersonalSettings(config);
+  const key =
+    personalSettingsDataFromDB != null ? 'personalSettingsInfo' : null;
 
 
-  const swrResult = useStaticSWR<IUser, Error>(key, undefined, { fallbackData: personalSettingsDataFromDB });
+  const swrResult = useStaticSWR<IUser, Error>(key, undefined, {
+    fallbackData: personalSettingsDataFromDB,
+  });
 
 
   // Sync with database
   // Sync with database
-  const sync = async(): Promise<void> => {
+  const sync = async (): Promise<void> => {
     const { mutate } = swrResult;
     const { mutate } = swrResult;
     const result = await revalidate();
     const result = await revalidate();
     mutate(result);
     mutate(result);
   };
   };
 
 
-  const updateBasicInfo = async(): Promise<void> => {
+  const updateBasicInfo = async (): Promise<void> => {
     const { data } = swrResult;
     const { data } = swrResult;
 
 
     if (data == null) {
     if (data == null) {
@@ -74,29 +94,25 @@ export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUse
     try {
     try {
       await apiv3Put('/personal-setting/', updateData);
       await apiv3Put('/personal-setting/', updateData);
       i18n.changeLanguage(updateData.lang);
       i18n.changeLanguage(updateData.lang);
-    }
-    catch (errs) {
+    } catch (errs) {
       logger.error(errs);
       logger.error(errs);
       throw errs;
       throw errs;
     }
     }
   };
   };
 
 
-
-  const associateLdapAccount = async(account): Promise<void> => {
+  const associateLdapAccount = async (account): Promise<void> => {
     try {
     try {
       await apiv3Put('/personal-setting/associate-ldap', account);
       await apiv3Put('/personal-setting/associate-ldap', account);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       logger.error(err);
       throw new Error('Failed to associate ldap account');
       throw new Error('Failed to associate ldap account');
     }
     }
   };
   };
 
 
-  const disassociateLdapAccount = async(account): Promise<void> => {
+  const disassociateLdapAccount = async (account): Promise<void> => {
     try {
     try {
       await apiv3Put('/personal-setting/disassociate-ldap', account);
       await apiv3Put('/personal-setting/disassociate-ldap', account);
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       logger.error(err);
       throw new Error('Failed to disassociate ldap account');
       throw new Error('Failed to disassociate ldap account');
     }
     }
@@ -111,35 +127,44 @@ export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUse
   };
   };
 };
 };
 
 
-export const useSWRxPersonalExternalAccounts = (): SWRResponse<(IExternalAccount<IExternalAuthProviderType> & HasObjectId)[], Error> => {
-  return useSWR(
-    '/personal-setting/external-accounts',
-    endpoint => apiv3Get(endpoint).then(response => response.data.externalAccounts),
+export const useSWRxPersonalExternalAccounts = (): SWRResponse<
+  (IExternalAccount<IExternalAuthProviderType> & HasObjectId)[],
+  Error
+> => {
+  return useSWR('/personal-setting/external-accounts', (endpoint) =>
+    apiv3Get(endpoint).then((response) => response.data.externalAccounts),
   );
   );
 };
 };
 
 
-
 interface IAccessTokenOption {
 interface IAccessTokenOption {
-  generateAccessToken: (info: IAccessTokenInfo) => Promise<IResGenerateAccessToken>,
-  deleteAccessToken: (tokenId: string) => Promise<void>,
-  deleteAllAccessTokens: (userId: string) => Promise<void>,
+  generateAccessToken: (
+    info: IAccessTokenInfo,
+  ) => Promise<IResGenerateAccessToken>;
+  deleteAccessToken: (tokenId: string) => Promise<void>;
+  deleteAllAccessTokens: (userId: string) => Promise<void>;
 }
 }
 
 
-export const useSWRxAccessToken = (): SWRResponse< IResGetAccessToken[] | null, Error> & IAccessTokenOption => {
-  const generateAccessToken = useCallback(async(info) => {
-    const res = await apiv3Post<IResGenerateAccessToken>('/personal-setting/access-token', info);
+export const useSWRxAccessToken = (): SWRResponse<
+  IResGetAccessToken[] | null,
+  Error
+> &
+  IAccessTokenOption => {
+  const generateAccessToken = useCallback(async (info) => {
+    const res = await apiv3Post<IResGenerateAccessToken>(
+      '/personal-setting/access-token',
+      info,
+    );
     return res.data;
     return res.data;
   }, []);
   }, []);
-  const deleteAccessToken = useCallback(async(tokenId: string) => {
+  const deleteAccessToken = useCallback(async (tokenId: string) => {
     await apiv3Delete('/personal-setting/access-token', { tokenId });
     await apiv3Delete('/personal-setting/access-token', { tokenId });
   }, []);
   }, []);
-  const deleteAllAccessTokens = useCallback(async() => {
+  const deleteAllAccessTokens = useCallback(async () => {
     await apiv3Delete('/personal-setting/access-token/all');
     await apiv3Delete('/personal-setting/access-token/all');
   }, []);
   }, []);
 
 
-  const swrResult = useSWR(
-    '/personal-setting/access-token',
-    endpoint => apiv3Get(endpoint).then(response => response.data.accessTokens),
+  const swrResult = useSWR('/personal-setting/access-token', (endpoint) =>
+    apiv3Get(endpoint).then((response) => response.data.accessTokens),
   );
   );
 
 
   return {
   return {
@@ -148,5 +173,4 @@ export const useSWRxAccessToken = (): SWRResponse< IResGetAccessToken[] | null,
     deleteAccessToken,
     deleteAccessToken,
     deleteAllAccessTokens,
     deleteAllAccessTokens,
   };
   };
-
 };
 };

+ 50 - 28
apps/app/src/stores/remote-latest-page.ts

@@ -1,55 +1,77 @@
-import { useMemo, useCallback } from 'react';
-
+import { useCallback, useMemo } from 'react';
 import type { IUserHasId } from '@growi/core';
 import type { IUserHasId } from '@growi/core';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 
 
-
-export const useRemoteRevisionId = (initialData?: string): SWRResponse<string, Error> => {
+export const useRemoteRevisionId = (
+  initialData?: string,
+): SWRResponse<string, Error> => {
   return useSWRStatic<string, Error>('remoteRevisionId', initialData);
   return useSWRStatic<string, Error>('remoteRevisionId', initialData);
 };
 };
 
 
-export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string, Error> => {
+export const useRemoteRevisionBody = (
+  initialData?: string,
+): SWRResponse<string, Error> => {
   return useSWRStatic<string, Error>('remoteRevisionBody', initialData);
   return useSWRStatic<string, Error>('remoteRevisionBody', initialData);
 };
 };
 
 
-export const useRemoteRevisionLastUpdateUser = (initialData?: IUserHasId): SWRResponse<IUserHasId, Error> => {
-  return useSWRStatic<IUserHasId, Error>('remoteRevisionLastUpdateUser', initialData);
+export const useRemoteRevisionLastUpdateUser = (
+  initialData?: IUserHasId,
+): SWRResponse<IUserHasId, Error> => {
+  return useSWRStatic<IUserHasId, Error>(
+    'remoteRevisionLastUpdateUser',
+    initialData,
+  );
 };
 };
 
 
-export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<Date, Error> => {
+export const useRemoteRevisionLastUpdatedAt = (
+  initialData?: Date,
+): SWRResponse<Date, Error> => {
   return useSWRStatic<Date, Error>('remoteRevisionLastUpdatedAt', initialData);
   return useSWRStatic<Date, Error>('remoteRevisionLastUpdatedAt', initialData);
 };
 };
 
 
 export type RemoteRevisionData = {
 export type RemoteRevisionData = {
-  remoteRevisionId: string,
-  remoteRevisionBody: string,
-  remoteRevisionLastUpdateUser?: IUserHasId,
-  remoteRevisionLastUpdatedAt: Date,
-}
-
+  remoteRevisionId: string;
+  remoteRevisionBody: string;
+  remoteRevisionLastUpdateUser?: IUserHasId;
+  remoteRevisionLastUpdatedAt: Date;
+};
 
 
 // set remote data all at once
 // set remote data all at once
-export const useSetRemoteLatestPageData = (): { setRemoteLatestPageData: (pageData: RemoteRevisionData) => void } => {
+export const useSetRemoteLatestPageData = (): {
+  setRemoteLatestPageData: (pageData: RemoteRevisionData) => void;
+} => {
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
   const { mutate: mutateRemoteRevisionBody } = useRemoteRevisionBody();
   const { mutate: mutateRemoteRevisionBody } = useRemoteRevisionBody();
-  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
-  const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
-
-  const setRemoteLatestPageData = useCallback((remoteRevisionData: RemoteRevisionData) => {
-    const {
-      remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt,
-    } = remoteRevisionData;
-    mutateRemoteRevisionId(remoteRevisionId);
-    mutateRemoteRevisionBody(remoteRevisionBody);
-    mutateRemoteRevisionLastUpdateUser(remoteRevisionLastUpdateUser);
-    mutateRemoteRevisionLastUpdatedAt(remoteRevisionLastUpdatedAt);
-  }, [mutateRemoteRevisionBody, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser, mutateRemoteRevisionLastUpdatedAt]);
+  const { mutate: mutateRemoteRevisionLastUpdateUser } =
+    useRemoteRevisionLastUpdateUser();
+  const { mutate: mutateRemoteRevisionLastUpdatedAt } =
+    useRemoteRevisionLastUpdatedAt();
+
+  const setRemoteLatestPageData = useCallback(
+    (remoteRevisionData: RemoteRevisionData) => {
+      const {
+        remoteRevisionId,
+        remoteRevisionBody,
+        remoteRevisionLastUpdateUser,
+        remoteRevisionLastUpdatedAt,
+      } = remoteRevisionData;
+      mutateRemoteRevisionId(remoteRevisionId);
+      mutateRemoteRevisionBody(remoteRevisionBody);
+      mutateRemoteRevisionLastUpdateUser(remoteRevisionLastUpdateUser);
+      mutateRemoteRevisionLastUpdatedAt(remoteRevisionLastUpdatedAt);
+    },
+    [
+      mutateRemoteRevisionBody,
+      mutateRemoteRevisionId,
+      mutateRemoteRevisionLastUpdateUser,
+      mutateRemoteRevisionLastUpdatedAt,
+    ],
+  );
 
 
   return useMemo(() => {
   return useMemo(() => {
     return {
     return {
       setRemoteLatestPageData,
       setRemoteLatestPageData,
     };
     };
   }, [setRemoteLatestPageData]);
   }, [setRemoteLatestPageData]);
-
 };
 };

+ 85 - 37
apps/app/src/stores/renderer.tsx

@@ -1,5 +1,4 @@
 import { useCallback, useEffect } from 'react';
 import { useCallback, useEffect } from 'react';
-
 import type { HtmlElementNode } from 'rehype-toc';
 import type { HtmlElementNode } from 'rehype-toc';
 import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr';
 import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr';
 
 
@@ -19,36 +18,52 @@ const useRendererConfigExt = (): RendererConfigExt | null => {
   const { data: rendererConfig } = useRendererConfig();
   const { data: rendererConfig } = useRendererConfig();
   const { isDarkMode } = useNextThemes();
   const { isDarkMode } = useNextThemes();
 
 
-  return rendererConfig == null ? null : {
-    ...rendererConfig,
-    isDarkMode,
-  } satisfies RendererConfigExt;
+  return rendererConfig == null
+    ? null
+    : ({
+        ...rendererConfig,
+        isDarkMode,
+      } satisfies RendererConfigExt);
 };
 };
 
 
-
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const rendererConfig = useRendererConfigExt();
   const rendererConfig = useRendererConfigExt();
   const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
   const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
 
 
-  const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
-    mutateCurrentPageTocNode(toc, { revalidate: false });
-  }, [mutateCurrentPageTocNode]);
+  const storeTocNodeHandler = useCallback(
+    (toc: HtmlElementNode) => {
+      mutateCurrentPageTocNode(toc, { revalidate: false });
+    },
+    [mutateCurrentPageTocNode],
+  );
 
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
-  const customGenerater = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGenerateViewOptions;
+  const customGenerater =
+    getGrowiFacade().markdownRenderer?.optionsGenerators
+      ?.customGenerateViewOptions;
 
 
   return useSWR(
   return useSWR(
     isAllDataValid
     isAllDataValid
       ? ['viewOptions', currentPagePath, rendererConfig, customGenerater]
       ? ['viewOptions', currentPagePath, rendererConfig, customGenerater]
       : null,
       : null,
-    async([, currentPagePath, rendererConfig]) => {
+    async ([, currentPagePath, rendererConfig]) => {
       if (customGenerater != null) {
       if (customGenerater != null) {
-        return customGenerater(currentPagePath, rendererConfig, storeTocNodeHandler);
+        return customGenerater(
+          currentPagePath,
+          rendererConfig,
+          storeTocNodeHandler,
+        );
       }
       }
 
 
-      const { generateViewOptions } = await import('~/client/services/renderer/renderer');
-      return generateViewOptions(currentPagePath, rendererConfig, storeTocNodeHandler);
+      const { generateViewOptions } = await import(
+        '~/client/services/renderer/renderer'
+      );
+      return generateViewOptions(
+        currentPagePath,
+        rendererConfig,
+        storeTocNodeHandler,
+      );
     },
     },
     {
     {
       keepPreviousData: true,
       keepPreviousData: true,
@@ -63,14 +78,17 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
   const rendererConfig = useRendererConfigExt();
   const rendererConfig = useRendererConfigExt();
   const { data: tocNode } = useCurrentPageTocNode();
   const { data: tocNode } = useCurrentPageTocNode();
 
 
-  const isAllDataValid = currentPagePath != null && rendererConfig != null && tocNode != null;
+  const isAllDataValid =
+    currentPagePath != null && rendererConfig != null && tocNode != null;
 
 
   return useSWR(
   return useSWR(
     isAllDataValid
     isAllDataValid
       ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
       ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
       : null,
       : null,
-    async([, , tocNode, rendererConfig]) => {
-      const { generateTocOptions } = await import('~/client/services/renderer/renderer');
+    async ([, , tocNode, rendererConfig]) => {
+      const { generateTocOptions } = await import(
+        '~/client/services/renderer/renderer'
+      );
       return generateTocOptions(rendererConfig, tocNode);
       return generateTocOptions(rendererConfig, tocNode);
     },
     },
     {
     {
@@ -86,18 +104,22 @@ export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
   const rendererConfig = useRendererConfigExt();
   const rendererConfig = useRendererConfigExt();
 
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
-  const customGenerater = getGrowiFacade().markdownRenderer?.optionsGenerators?.customGeneratePreviewOptions;
+  const customGenerater =
+    getGrowiFacade().markdownRenderer?.optionsGenerators
+      ?.customGeneratePreviewOptions;
 
 
   return useSWR(
   return useSWR(
     isAllDataValid
     isAllDataValid
       ? ['previewOptions', rendererConfig, currentPagePath, customGenerater]
       ? ['previewOptions', rendererConfig, currentPagePath, customGenerater]
       : null,
       : null,
-    async([, rendererConfig, pagePath]) => {
+    async ([, rendererConfig, pagePath]) => {
       if (customGenerater != null) {
       if (customGenerater != null) {
         return customGenerater(rendererConfig, pagePath);
         return customGenerater(rendererConfig, pagePath);
       }
       }
 
 
-      const { generatePreviewOptions } = await import('~/client/services/renderer/renderer');
+      const { generatePreviewOptions } = await import(
+        '~/client/services/renderer/renderer'
+      );
       return generatePreviewOptions(rendererConfig, pagePath);
       return generatePreviewOptions(rendererConfig, pagePath);
     },
     },
     {
     {
@@ -108,7 +130,10 @@ export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
   );
   );
 };
 };
 
 
-export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions, Error> => {
+export const useCommentForCurrentPageOptions = (): SWRResponse<
+  RendererOptions,
+  Error
+> => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const rendererConfig = useRendererConfigExt();
   const rendererConfig = useRendererConfigExt();
 
 
@@ -118,8 +143,10 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
     isAllDataValid
     isAllDataValid
       ? ['commentPreviewOptions', rendererConfig, currentPagePath]
       ? ['commentPreviewOptions', rendererConfig, currentPagePath]
       : null,
       : null,
-    async([, rendererConfig, currentPagePath]) => {
-      const { generateSimpleViewOptions } = await import('~/client/services/renderer/renderer');
+    async ([, rendererConfig, currentPagePath]) => {
+      const { generateSimpleViewOptions } = await import(
+        '~/client/services/renderer/renderer'
+      );
       return generateSimpleViewOptions(
       return generateSimpleViewOptions(
         rendererConfig,
         rendererConfig,
         currentPagePath,
         currentPagePath,
@@ -136,18 +163,32 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
 };
 };
 export const useCommentPreviewOptions = useCommentForCurrentPageOptions;
 export const useCommentPreviewOptions = useCommentForCurrentPageOptions;
 
 
-export const useSelectedPagePreviewOptions = (pagePath: string, highlightKeywords?: string | string[]): SWRResponse<RendererOptions, Error> => {
+export const useSelectedPagePreviewOptions = (
+  pagePath: string,
+  highlightKeywords?: string | string[],
+): SWRResponse<RendererOptions, Error> => {
   const rendererConfig = useRendererConfigExt();
   const rendererConfig = useRendererConfigExt();
 
 
   const isAllDataValid = rendererConfig != null;
   const isAllDataValid = rendererConfig != null;
 
 
   return useSWR(
   return useSWR(
     isAllDataValid
     isAllDataValid
-      ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
+      ? [
+          'selectedPagePreviewOptions',
+          rendererConfig,
+          pagePath,
+          highlightKeywords,
+        ]
       : null,
       : null,
-    async([, rendererConfig, pagePath, highlightKeywords]) => {
-      const { generateSimpleViewOptions } = await import('~/client/services/renderer/renderer');
-      return generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords);
+    async ([, rendererConfig, pagePath, highlightKeywords]) => {
+      const { generateSimpleViewOptions } = await import(
+        '~/client/services/renderer/renderer'
+      );
+      return generateSimpleViewOptions(
+        rendererConfig,
+        pagePath,
+        highlightKeywords,
+      );
     },
     },
     {
     {
       revalidateOnFocus: false,
       revalidateOnFocus: false,
@@ -159,17 +200,19 @@ export const useSearchResultOptions = useSelectedPagePreviewOptions;
 
 
 export const useTimelineOptions = useSelectedPagePreviewOptions;
 export const useTimelineOptions = useSelectedPagePreviewOptions;
 
 
-export const useCustomSidebarOptions = (config?: SWRConfiguration): SWRResponse<RendererOptions, Error> => {
+export const useCustomSidebarOptions = (
+  config?: SWRConfiguration,
+): SWRResponse<RendererOptions, Error> => {
   const rendererConfig = useRendererConfigExt();
   const rendererConfig = useRendererConfigExt();
 
 
   const isAllDataValid = rendererConfig != null;
   const isAllDataValid = rendererConfig != null;
 
 
   return useSWR(
   return useSWR(
-    isAllDataValid
-      ? ['customSidebarOptions', rendererConfig]
-      : null,
-    async([, rendererConfig]) => {
-      const { generateSimpleViewOptions } = await import('~/client/services/renderer/renderer');
+    isAllDataValid ? ['customSidebarOptions', rendererConfig] : null,
+    async ([, rendererConfig]) => {
+      const { generateSimpleViewOptions } = await import(
+        '~/client/services/renderer/renderer'
+      );
       return generateSimpleViewOptions(rendererConfig, '/');
       return generateSimpleViewOptions(rendererConfig, '/');
     },
     },
     {
     {
@@ -181,7 +224,10 @@ export const useCustomSidebarOptions = (config?: SWRConfiguration): SWRResponse<
   );
   );
 };
 };
 
 
-export const usePresentationViewOptions = (): SWRResponse<RendererOptions, Error> => {
+export const usePresentationViewOptions = (): SWRResponse<
+  RendererOptions,
+  Error
+> => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const rendererConfig = useRendererConfigExt();
   const rendererConfig = useRendererConfigExt();
 
 
@@ -197,8 +243,10 @@ export const usePresentationViewOptions = (): SWRResponse<RendererOptions, Error
     isAllDataValid
     isAllDataValid
       ? ['presentationViewOptions', currentPagePath, rendererConfig]
       ? ['presentationViewOptions', currentPagePath, rendererConfig]
       : null,
       : null,
-    async([, currentPagePath, rendererConfig]) => {
-      const { generatePresentationViewOptions } = await import('~/client/services/renderer/renderer');
+    async ([, currentPagePath, rendererConfig]) => {
+      const { generatePresentationViewOptions } = await import(
+        '~/client/services/renderer/renderer'
+      );
       return generatePresentationViewOptions(rendererConfig, currentPagePath);
       return generatePresentationViewOptions(rendererConfig, currentPagePath);
     },
     },
     {
     {

+ 42 - 35
apps/app/src/stores/search.tsx

@@ -5,31 +5,34 @@ import { apiGet } from '~/client/util/apiv1-client';
 import type { IFormattedSearchResult } from '~/interfaces/search';
 import type { IFormattedSearchResult } from '~/interfaces/search';
 import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
 
 
-
 export type ISearchConfigurations = {
 export type ISearchConfigurations = {
-  limit: number,
-  offset?: number,
-  sort?: SORT_AXIS,
-  order?: SORT_ORDER,
-  includeTrashPages?: boolean,
-  includeUserPages?: boolean,
-}
+  limit: number;
+  offset?: number;
+  sort?: SORT_AXIS;
+  order?: SORT_ORDER;
+  includeTrashPages?: boolean;
+  includeUserPages?: boolean;
+};
 
 
 type ISearchConfigurationsFixed = {
 type ISearchConfigurationsFixed = {
-  limit: number,
-  offset: number,
-  sort: SORT_AXIS,
-  order: SORT_ORDER,
-  includeTrashPages: boolean,
-  includeUserPages: boolean,
-}
+  limit: number;
+  offset: number;
+  sort: SORT_AXIS;
+  order: SORT_ORDER;
+  includeTrashPages: boolean;
+  includeUserPages: boolean;
+};
 
 
 export type ISearchConditions = ISearchConfigurationsFixed & {
 export type ISearchConditions = ISearchConfigurationsFixed & {
-  keyword: string | null,
-  rawQuery: string,
-}
+  keyword: string | null;
+  rawQuery: string;
+};
 
 
-const createSearchQuery = (keyword: string, includeTrashPages: boolean, includeUserPages: boolean): string => {
+const createSearchQuery = (
+  keyword: string,
+  includeTrashPages: boolean,
+  includeUserPages: boolean,
+): string => {
   let query = keyword;
   let query = keyword;
 
 
   // pages included in specific path are not retrived when prefix is added
   // pages included in specific path are not retrived when prefix is added
@@ -43,18 +46,19 @@ const createSearchQuery = (keyword: string, includeTrashPages: boolean, includeU
   return query;
   return query;
 };
 };
 
 
-export const mutateSearching = async(): Promise<void[]> => {
-  return mutate(
-    key => Array.isArray(key) && key[0] === '/search',
-  );
+export const mutateSearching = async (): Promise<void[]> => {
+  return mutate((key) => Array.isArray(key) && key[0] === '/search');
 };
 };
 
 
 export const useSWRxSearch = (
 export const useSWRxSearch = (
-    keyword: string | null, nqName: string | null, configurations: ISearchConfigurations,
-): SWRResponse<IFormattedSearchResult, Error> & { conditions: ISearchConditions } => {
-  const {
-    limit, offset, sort, order, includeTrashPages, includeUserPages,
-  } = configurations;
+  keyword: string | null,
+  nqName: string | null,
+  configurations: ISearchConfigurations,
+): SWRResponse<IFormattedSearchResult, Error> & {
+  conditions: ISearchConditions;
+} => {
+  const { limit, offset, sort, order, includeTrashPages, includeUserPages } =
+    configurations;
 
 
   const fixedConfigurations: ISearchConfigurationsFixed = {
   const fixedConfigurations: ISearchConfigurationsFixed = {
     limit,
     limit,
@@ -64,19 +68,22 @@ export const useSWRxSearch = (
     includeTrashPages: includeTrashPages ?? false,
     includeTrashPages: includeTrashPages ?? false,
     includeUserPages: includeUserPages ?? false,
     includeUserPages: includeUserPages ?? false,
   };
   };
-  const rawQuery = createSearchQuery(keyword ?? '', fixedConfigurations.includeTrashPages, fixedConfigurations.includeUserPages);
+  const rawQuery = createSearchQuery(
+    keyword ?? '',
+    fixedConfigurations.includeTrashPages,
+    fixedConfigurations.includeUserPages,
+  );
 
 
   const isKeywordValid = keyword != null && keyword.length > 0;
   const isKeywordValid = keyword != null && keyword.length > 0;
 
 
   const swrResult = useSWR(
   const swrResult = useSWR(
     isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
     isKeywordValid ? ['/search', keyword, fixedConfigurations] : null,
     ([endpoint, , fixedConfigurations]) => {
     ([endpoint, , fixedConfigurations]) => {
-      const {
-        limit, offset, sort, order,
-      } = fixedConfigurations;
+      const { limit, offset, sort, order } = fixedConfigurations;
 
 
       return apiGet(
       return apiGet(
-        endpoint, {
+        endpoint,
+        {
           q: encodeURIComponent(rawQuery),
           q: encodeURIComponent(rawQuery),
           nq: typeof nqName === 'string' ? encodeURIComponent(nqName) : null,
           nq: typeof nqName === 'string' ? encodeURIComponent(nqName) : null,
           limit,
           limit,
@@ -84,8 +91,8 @@ export const useSWRxSearch = (
           sort,
           sort,
           order,
           order,
         },
         },
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      ).then(result => result as IFormattedSearchResult);
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      ).then((result) => result as IFormattedSearchResult);
     },
     },
     {
     {
       keepPreviousData: true,
       keepPreviousData: true,

+ 7 - 3
apps/app/src/stores/share-link.tsx

@@ -5,12 +5,16 @@ import useSWR from 'swr';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import type { IResShareLinkList } from '~/interfaces/share-link';
 import type { IResShareLinkList } from '~/interfaces/share-link';
 
 
-const fetchShareLinks = async(endpoint, pageId) => {
-  const res = await apiv3Get<IResShareLinkList>(endpoint, { relatedPage: pageId });
+const fetchShareLinks = async (endpoint, pageId) => {
+  const res = await apiv3Get<IResShareLinkList>(endpoint, {
+    relatedPage: pageId,
+  });
   return res.data.shareLinksResult;
   return res.data.shareLinksResult;
 };
 };
 
 
-export const useSWRxSharelink = (currentPageId: Nullable<string>): SWRResponse<IResShareLinkList['shareLinksResult'], Error> => {
+export const useSWRxSharelink = (
+  currentPageId: Nullable<string>,
+): SWRResponse<IResShareLinkList['shareLinksResult'], Error> => {
   return useSWR(
   return useSWR(
     currentPageId == null ? null : ['/share-links/', currentPageId],
     currentPageId == null ? null : ['/share-links/', currentPageId],
     ([endpoint]) => fetchShareLinks(endpoint, currentPageId),
     ([endpoint]) => fetchShareLinks(endpoint, currentPageId),

+ 0 - 1
apps/app/src/stores/socket-io.ts

@@ -7,7 +7,6 @@ import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:cli:stores:socket-io');
 const logger = loggerFactory('growi:cli:stores:socket-io');
 
 
-
 const socketFactory = (namespace: string): Socket => {
 const socketFactory = (namespace: string): Socket => {
   const socket = io(namespace, {
   const socket = io(namespace, {
     transports: ['websocket'],
     transports: ['websocket'],

+ 2 - 3
apps/app/src/stores/staff.tsx

@@ -4,9 +4,8 @@ import useSWR from 'swr';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 
 
 export const useSWRxStaffs = (): SWRResponse<any, Error> => {
 export const useSWRxStaffs = (): SWRResponse<any, Error> => {
-  return useSWR(
-    '/staffs',
-    endpoint => apiv3Get(endpoint).then((response) => {
+  return useSWR('/staffs', (endpoint) =>
+    apiv3Get(endpoint).then((response) => {
       return response.data.contributors;
       return response.data.contributors;
     }),
     }),
   );
   );

+ 15 - 4
apps/app/src/stores/tag.tsx

@@ -4,10 +4,16 @@ import useSWR from 'swr';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 import type { IResTagsListApiv1, IResTagsSearchApiv1 } from '~/interfaces/tag';
 import type { IResTagsListApiv1, IResTagsSearchApiv1 } from '~/interfaces/tag';
 
 
-export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IResTagsListApiv1, Error> => {
+export const useSWRxTagsList = (
+  limit?: number,
+  offset?: number,
+): SWRResponse<IResTagsListApiv1, Error> => {
   return useSWR(
   return useSWR(
     ['/tags.list', limit, offset],
     ['/tags.list', limit, offset],
-    ([endpoint, limit, offset]) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
+    ([endpoint, limit, offset]) =>
+      apiGet(endpoint, { limit, offset }).then(
+        (result: IResTagsListApiv1) => result,
+      ),
     {
     {
       keepPreviousData: true,
       keepPreviousData: true,
       revalidateOnFocus: false,
       revalidateOnFocus: false,
@@ -16,10 +22,15 @@ export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IR
   );
   );
 };
 };
 
 
-export const useSWRxTagsSearch = (query: string): SWRResponse<IResTagsSearchApiv1, Error> => {
+export const useSWRxTagsSearch = (
+  query: string,
+): SWRResponse<IResTagsSearchApiv1, Error> => {
   return useSWR(
   return useSWR(
     ['/tags.search', query],
     ['/tags.search', query],
-    ([endpoint, query]) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
+    ([endpoint, query]) =>
+      apiGet(endpoint, { q: query }).then(
+        (result: IResTagsSearchApiv1) => result,
+      ),
     {
     {
       keepPreviousData: true,
       keepPreviousData: true,
       revalidateOnFocus: false,
       revalidateOnFocus: false,

+ 287 - 134
apps/app/src/stores/ui.tsx

@@ -1,19 +1,20 @@
+import { type RefObject, useCallback, useEffect, useLayoutEffect } from 'react';
+import { useRouter } from 'next/router';
+import { type Nullable, PageGrant } from '@growi/core';
 import {
 import {
-  type RefObject, useCallback, useEffect,
-  useLayoutEffect,
-} from 'react';
-
-import { PageGrant, type Nullable } from '@growi/core';
-import { type SWRResponseWithUtils, useSWRStatic, withUtils } from '@growi/core/dist/swr';
-import { pagePathUtils, isClient } from '@growi/core/dist/utils';
+  type SWRResponseWithUtils,
+  useSWRStatic,
+  withUtils,
+} from '@growi/core/dist/swr';
+import { isClient, pagePathUtils } from '@growi/core/dist/utils';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
 import { Breakpoint } from '@growi/ui/dist/interfaces';
-import { addBreakpointListener, cleanupBreakpointListener } from '@growi/ui/dist/utils';
-import { useRouter } from 'next/router';
+import {
+  addBreakpointListener,
+  cleanupBreakpointListener,
+} from '@growi/ui/dist/utils';
 import type { HtmlElementNode } from 'rehype-toc';
 import type { HtmlElementNode } from 'rehype-toc';
 import type { MutatorOptions } from 'swr';
 import type { MutatorOptions } from 'swr';
-import {
-  useSWRConfig, type SWRResponse, type Key,
-} from 'swr';
+import { type Key, type SWRResponse, useSWRConfig } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
 import { scheduleToPut } from '~/client/services/user-ui-settings';
 import { scheduleToPut } from '~/client/services/user-ui-settings';
@@ -21,13 +22,20 @@ import type { IPageSelectedGrant } from '~/interfaces/page';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import { SidebarContentsType, SidebarMode } from '~/interfaces/ui';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
 import {
 import {
-  useIsEditable, useIsReadOnlyUser,
-  useIsSharedUser, useIsIdenticalPath, useCurrentUser, useShareLinkId,
+  useCurrentPageId,
+  useCurrentPagePath,
+  useIsNotFound,
+  useIsTrashPage,
+} from '~/stores/page';
+import {
+  useCurrentUser,
+  useIsEditable,
+  useIsIdenticalPath,
+  useIsReadOnlyUser,
+  useIsSharedUser,
+  useShareLinkId,
 } from '~/stores-universal/context';
 } from '~/stores-universal/context';
 import { EditorMode, useEditorMode } from '~/stores-universal/ui';
 import { EditorMode, useEditorMode } from '~/stores-universal/ui';
-import {
-  useIsNotFound, useCurrentPagePath, useIsTrashPage, useCurrentPageId,
-} from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
@@ -36,7 +44,6 @@ const { isTrashTopPage, isUsersTopPage } = pagePathUtils;
 
 
 const logger = loggerFactory('growi:stores:ui');
 const logger = loggerFactory('growi:stores:ui');
 
 
-
 /** **********************************************************
 /** **********************************************************
  *                     Storing objects to ref
  *                     Storing objects to ref
  *********************************************************** */
  *********************************************************** */
@@ -52,8 +59,13 @@ export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
  *                      for switching UI
  *                      for switching UI
  *********************************************************** */
  *********************************************************** */
 
 
-export const useSidebarScrollerRef = (initialData?: RefObject<HTMLDivElement | null>): SWRResponse<RefObject<HTMLDivElement | null>, Error> => {
-  return useSWRStatic<RefObject<HTMLDivElement | null>, Error>('sidebarScrollerRef', initialData);
+export const useSidebarScrollerRef = (
+  initialData?: RefObject<HTMLDivElement | null>,
+): SWRResponse<RefObject<HTMLDivElement | null>, Error> => {
+  return useSWRStatic<RefObject<HTMLDivElement | null>, Error>(
+    'sidebarScrollerRef',
+    initialData,
+  );
 };
 };
 
 
 //
 //
@@ -65,21 +77,21 @@ export const useIsMobile = (): SWRResponse<boolean, Error> => {
   };
   };
 
 
   if (isClient()) {
   if (isClient()) {
-
     // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_device_detection
     // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_device_detection
     let hasTouchScreen = false;
     let hasTouchScreen = false;
-    hasTouchScreen = ('maxTouchPoints' in navigator) ? navigator?.maxTouchPoints > 0 : false;
+    hasTouchScreen =
+      'maxTouchPoints' in navigator ? navigator?.maxTouchPoints > 0 : false;
 
 
     if (!hasTouchScreen) {
     if (!hasTouchScreen) {
       const mQ = matchMedia?.('(pointer:coarse)');
       const mQ = matchMedia?.('(pointer:coarse)');
       if (mQ?.media === '(pointer:coarse)') {
       if (mQ?.media === '(pointer:coarse)') {
         hasTouchScreen = !!mQ.matches;
         hasTouchScreen = !!mQ.matches;
-      }
-      else {
-      // Only as a last resort, fall back to user agent sniffing
+      } else {
+        // Only as a last resort, fall back to user agent sniffing
         const UA = navigator.userAgent;
         const UA = navigator.userAgent;
-        hasTouchScreen = /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA)
-      || /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
+        hasTouchScreen =
+          /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
+          /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
       }
       }
     }
     }
 
 
@@ -98,7 +110,7 @@ export const useIsDeviceLargerThanMd = (): SWRResponse<boolean, Error> => {
 
 
   useEffect(() => {
   useEffect(() => {
     if (key != null) {
     if (key != null) {
-      const mdOrAvobeHandler = function(this: MediaQueryList): void {
+      const mdOrAvobeHandler = function (this: MediaQueryList): void {
         // sm -> md: matches will be true
         // sm -> md: matches will be true
         // md -> sm: matches will be false
         // md -> sm: matches will be false
         mutate(key, this.matches);
         mutate(key, this.matches);
@@ -126,7 +138,7 @@ export const useIsDeviceLargerThanLg = (): SWRResponse<boolean, Error> => {
 
 
   useEffect(() => {
   useEffect(() => {
     if (key != null) {
     if (key != null) {
-      const lgOrAvobeHandler = function(this: MediaQueryList): void {
+      const lgOrAvobeHandler = function (this: MediaQueryList): void {
         // md -> lg: matches will be true
         // md -> lg: matches will be true
         // lg -> md: matches will be false
         // lg -> md: matches will be false
         mutate(key, this.matches);
         mutate(key, this.matches);
@@ -154,7 +166,7 @@ export const useIsDeviceLargerThanXl = (): SWRResponse<boolean, Error> => {
 
 
   useEffect(() => {
   useEffect(() => {
     if (key != null) {
     if (key != null) {
-      const xlOrAvobeHandler = function(this: MediaQueryList): void {
+      const xlOrAvobeHandler = function (this: MediaQueryList): void {
         // lg -> xl: matches will be true
         // lg -> xl: matches will be true
         // xl -> lg: matches will be false
         // xl -> lg: matches will be false
         mutate(key, this.matches);
         mutate(key, this.matches);
@@ -175,23 +187,34 @@ export const useIsDeviceLargerThanXl = (): SWRResponse<boolean, Error> => {
   return useSWRStatic(key);
   return useSWRStatic(key);
 };
 };
 
 
-
-type MutateAndSaveUserUISettings<Data> = (data: Data, opts?: boolean | MutatorOptions<Data>) => Promise<Data | undefined>;
+type MutateAndSaveUserUISettings<Data> = (
+  data: Data,
+  opts?: boolean | MutatorOptions<Data>,
+) => Promise<Data | undefined>;
 type MutateAndSaveUserUISettingsUtils<Data> = {
 type MutateAndSaveUserUISettingsUtils<Data> = {
   mutateAndSave: MutateAndSaveUserUISettings<Data>;
   mutateAndSave: MutateAndSaveUserUISettings<Data>;
-}
+};
 
 
 export const useCurrentSidebarContents = (
 export const useCurrentSidebarContents = (
-    initialData?: SidebarContentsType,
-): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<SidebarContentsType>, SidebarContentsType> => {
-  const swrResponse = useSWRStatic('sidebarContents', initialData, { fallbackData: SidebarContentsType.TREE });
+  initialData?: SidebarContentsType,
+): SWRResponseWithUtils<
+  MutateAndSaveUserUISettingsUtils<SidebarContentsType>,
+  SidebarContentsType
+> => {
+  const swrResponse = useSWRStatic('sidebarContents', initialData, {
+    fallbackData: SidebarContentsType.TREE,
+  });
 
 
   const { mutate } = swrResponse;
   const { mutate } = swrResponse;
 
 
-  const mutateAndSave: MutateAndSaveUserUISettings<SidebarContentsType> = useCallback((data, opts?) => {
-    scheduleToPut({ currentSidebarContents: data });
-    return mutate(data, opts);
-  }, [mutate]);
+  const mutateAndSave: MutateAndSaveUserUISettings<SidebarContentsType> =
+    useCallback(
+      (data, opts?) => {
+        scheduleToPut({ currentSidebarContents: data });
+        return mutate(data, opts);
+      },
+      [mutate],
+    );
 
 
   return withUtils(swrResponse, { mutateAndSave });
   return withUtils(swrResponse, { mutateAndSave });
 };
 };
@@ -200,74 +223,131 @@ export const usePageControlsX = (initialData?: number): SWRResponse<number> => {
   return useSWRStatic('pageControlsX', initialData);
   return useSWRStatic('pageControlsX', initialData);
 };
 };
 
 
-export const useCurrentProductNavWidth = (initialData?: number): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<number>, number> => {
-  const swrResponse = useSWRStatic('productNavWidth', initialData, { fallbackData: 320 });
+export const useCurrentProductNavWidth = (
+  initialData?: number,
+): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<number>, number> => {
+  const swrResponse = useSWRStatic('productNavWidth', initialData, {
+    fallbackData: 320,
+  });
 
 
   const { mutate } = swrResponse;
   const { mutate } = swrResponse;
 
 
-  const mutateAndSave: MutateAndSaveUserUISettings<number> = useCallback((data, opts?) => {
-    scheduleToPut({ currentProductNavWidth: data });
-    return mutate(data, opts);
-  }, [mutate]);
+  const mutateAndSave: MutateAndSaveUserUISettings<number> = useCallback(
+    (data, opts?) => {
+      scheduleToPut({ currentProductNavWidth: data });
+      return mutate(data, opts);
+    },
+    [mutate],
+  );
 
 
   return withUtils(swrResponse, { mutateAndSave });
   return withUtils(swrResponse, { mutateAndSave });
 };
 };
 
 
-export const usePreferCollapsedMode = (initialData?: boolean): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<boolean>, boolean> => {
-  const swrResponse = useSWRStatic('isPreferCollapsedMode', initialData, { fallbackData: false });
+export const usePreferCollapsedMode = (
+  initialData?: boolean,
+): SWRResponseWithUtils<MutateAndSaveUserUISettingsUtils<boolean>, boolean> => {
+  const swrResponse = useSWRStatic('isPreferCollapsedMode', initialData, {
+    fallbackData: false,
+  });
 
 
   const { mutate } = swrResponse;
   const { mutate } = swrResponse;
 
 
-  const mutateAndSave: MutateAndSaveUserUISettings<boolean> = useCallback((data, opts?) => {
-    scheduleToPut({ preferCollapsedModeByUser: data });
-    return mutate(data, opts);
-  }, [mutate]);
+  const mutateAndSave: MutateAndSaveUserUISettings<boolean> = useCallback(
+    (data, opts?) => {
+      scheduleToPut({ preferCollapsedModeByUser: data });
+      return mutate(data, opts);
+    },
+    [mutate],
+  );
 
 
   return withUtils(swrResponse, { mutateAndSave });
   return withUtils(swrResponse, { mutateAndSave });
 };
 };
 
 
-export const useCollapsedContentsOpened = (initialData?: boolean): SWRResponse<boolean> => {
-  return useSWRStatic('isCollapsedContentsOpened', initialData, { fallbackData: false });
+export const useCollapsedContentsOpened = (
+  initialData?: boolean,
+): SWRResponse<boolean> => {
+  return useSWRStatic('isCollapsedContentsOpened', initialData, {
+    fallbackData: false,
+  });
 };
 };
 
 
-export const useDrawerOpened = (isOpened?: boolean): SWRResponse<boolean, Error> => {
+export const useDrawerOpened = (
+  isOpened?: boolean,
+): SWRResponse<boolean, Error> => {
   return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
   return useSWRStatic('isDrawerOpened', isOpened, { fallbackData: false });
 };
 };
 
 
 type DetectSidebarModeUtils = {
 type DetectSidebarModeUtils = {
-  isDrawerMode(): boolean
-  isCollapsedMode(): boolean
-  isDockMode(): boolean
-}
+  isDrawerMode(): boolean;
+  isCollapsedMode(): boolean;
+  isDockMode(): boolean;
+};
 
 
-export const useSidebarMode = (): SWRResponseWithUtils<DetectSidebarModeUtils, SidebarMode> => {
+export const useSidebarMode = (): SWRResponseWithUtils<
+  DetectSidebarModeUtils,
+  SidebarMode
+> => {
   const { data: isDeviceLargerThanXl } = useIsDeviceLargerThanXl();
   const { data: isDeviceLargerThanXl } = useIsDeviceLargerThanXl();
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
   const { data: isCollapsedModeUnderDockMode } = usePreferCollapsedMode();
   const { data: isCollapsedModeUnderDockMode } = usePreferCollapsedMode();
 
 
-  const condition = isDeviceLargerThanXl != null && editorMode != null && isCollapsedModeUnderDockMode != null;
+  const condition =
+    isDeviceLargerThanXl != null &&
+    editorMode != null &&
+    isCollapsedModeUnderDockMode != null;
 
 
   const isEditorMode = editorMode === EditorMode.Editor;
   const isEditorMode = editorMode === EditorMode.Editor;
 
 
-  const fetcher = useCallback((
-      [, isDeviceLargerThanXl, isEditorMode, isCollapsedModeUnderDockMode]: [Key, boolean|undefined, boolean|undefined, boolean|undefined],
-  ) => {
-    if (!isDeviceLargerThanXl) {
-      return SidebarMode.DRAWER;
-    }
-    return isEditorMode || isCollapsedModeUnderDockMode ? SidebarMode.COLLAPSED : SidebarMode.DOCK;
-  }, []);
+  const fetcher = useCallback(
+    ([, isDeviceLargerThanXl, isEditorMode, isCollapsedModeUnderDockMode]: [
+      Key,
+      boolean | undefined,
+      boolean | undefined,
+      boolean | undefined,
+    ]) => {
+      if (!isDeviceLargerThanXl) {
+        return SidebarMode.DRAWER;
+      }
+      return isEditorMode || isCollapsedModeUnderDockMode
+        ? SidebarMode.COLLAPSED
+        : SidebarMode.DOCK;
+    },
+    [],
+  );
 
 
   const swrResponse = useSWRImmutable(
   const swrResponse = useSWRImmutable(
-    condition ? ['sidebarMode', isDeviceLargerThanXl, isEditorMode, isCollapsedModeUnderDockMode] : null,
+    condition
+      ? [
+          'sidebarMode',
+          isDeviceLargerThanXl,
+          isEditorMode,
+          isCollapsedModeUnderDockMode,
+        ]
+      : null,
     // calcDrawerMode,
     // calcDrawerMode,
     fetcher,
     fetcher,
-    { fallbackData: fetcher(['sidebarMode', isDeviceLargerThanXl, isEditorMode, isCollapsedModeUnderDockMode]) },
+    {
+      fallbackData: fetcher([
+        'sidebarMode',
+        isDeviceLargerThanXl,
+        isEditorMode,
+        isCollapsedModeUnderDockMode,
+      ]),
+    },
   );
   );
 
 
-  const _isDrawerMode = useCallback(() => swrResponse.data === SidebarMode.DRAWER, [swrResponse.data]);
-  const _isCollapsedMode = useCallback(() => swrResponse.data === SidebarMode.COLLAPSED, [swrResponse.data]);
-  const _isDockMode = useCallback(() => swrResponse.data === SidebarMode.DOCK, [swrResponse.data]);
+  const _isDrawerMode = useCallback(
+    () => swrResponse.data === SidebarMode.DRAWER,
+    [swrResponse.data],
+  );
+  const _isCollapsedMode = useCallback(
+    () => swrResponse.data === SidebarMode.COLLAPSED,
+    [swrResponse.data],
+  );
+  const _isDockMode = useCallback(
+    () => swrResponse.data === SidebarMode.DOCK,
+    [swrResponse.data],
+  );
 
 
   return {
   return {
     ...swrResponse,
     ...swrResponse,
@@ -277,63 +357,93 @@ export const useSidebarMode = (): SWRResponseWithUtils<DetectSidebarModeUtils, S
   };
   };
 };
 };
 
 
-export const useSelectedGrant = (initialData?: Nullable<IPageSelectedGrant>): SWRResponse<Nullable<IPageSelectedGrant>, Error> => {
-  return useSWRStatic<Nullable<IPageSelectedGrant>, Error>('selectedGrant', initialData, { fallbackData: { grant: PageGrant.GRANT_PUBLIC } });
+export const useSelectedGrant = (
+  initialData?: Nullable<IPageSelectedGrant>,
+): SWRResponse<Nullable<IPageSelectedGrant>, Error> => {
+  return useSWRStatic<Nullable<IPageSelectedGrant>, Error>(
+    'selectedGrant',
+    initialData,
+    { fallbackData: { grant: PageGrant.GRANT_PUBLIC } },
+  );
 };
 };
 
 
 type PageTreeDescCountMapUtils = {
 type PageTreeDescCountMapUtils = {
-  update(newData?: UpdateDescCountData): Promise<UpdateDescCountData | undefined>
-  getDescCount(pageId?: string): number | null | undefined
-}
+  update(
+    newData?: UpdateDescCountData,
+  ): Promise<UpdateDescCountData | undefined>;
+  getDescCount(pageId?: string): number | null | undefined;
+};
 
 
-export const usePageTreeDescCountMap = (initialData?: UpdateDescCountData): SWRResponse<UpdateDescCountData, Error> & PageTreeDescCountMapUtils => {
+export const usePageTreeDescCountMap = (
+  initialData?: UpdateDescCountData,
+): SWRResponse<UpdateDescCountData, Error> & PageTreeDescCountMapUtils => {
   const key = 'pageTreeDescCountMap';
   const key = 'pageTreeDescCountMap';
 
 
-  const swrResponse = useStaticSWR<UpdateDescCountData, Error>(key, initialData, { fallbackData: new Map() });
+  const swrResponse = useStaticSWR<UpdateDescCountData, Error>(
+    key,
+    initialData,
+    { fallbackData: new Map() },
+  );
 
 
   return {
   return {
     ...swrResponse,
     ...swrResponse,
-    getDescCount: (pageId?: string) => (pageId != null ? swrResponse.data?.get(pageId) : null),
-    update: (newData: UpdateDescCountData) => swrResponse.mutate(new Map([...(swrResponse.data || new Map()), ...newData])),
+    getDescCount: (pageId?: string) =>
+      pageId != null ? swrResponse.data?.get(pageId) : null,
+    update: (newData: UpdateDescCountData) =>
+      swrResponse.mutate(
+        new Map([...(swrResponse.data || new Map()), ...newData]),
+      ),
   };
   };
 };
 };
 
 
-
 type UseCommentEditorDirtyMapOperation = {
 type UseCommentEditorDirtyMapOperation = {
-  evaluate(key: string, commentBody: string): Promise<number>,
-  clean(key: string): Promise<number>,
-}
+  evaluate(key: string, commentBody: string): Promise<number>;
+  clean(key: string): Promise<number>;
+};
 
 
-export const useCommentEditorDirtyMap = (): SWRResponse<Map<string, boolean>, Error> & UseCommentEditorDirtyMapOperation => {
+export const useCommentEditorDirtyMap = (): SWRResponse<
+  Map<string, boolean>,
+  Error
+> &
+  UseCommentEditorDirtyMapOperation => {
   const router = useRouter();
   const router = useRouter();
 
 
-  const swrResponse = useSWRStatic<Map<string, boolean>, Error>('editingCommentsNum', undefined, { fallbackData: new Map() });
+  const swrResponse = useSWRStatic<Map<string, boolean>, Error>(
+    'editingCommentsNum',
+    undefined,
+    { fallbackData: new Map() },
+  );
 
 
   const { mutate } = swrResponse;
   const { mutate } = swrResponse;
 
 
-  const evaluate = useCallback(async(key: string, commentBody: string) => {
-    const newMap = await mutate((map) => {
-      if (map == null) return new Map();
-
-      if (commentBody.length === 0) {
+  const evaluate = useCallback(
+    async (key: string, commentBody: string) => {
+      const newMap = await mutate((map) => {
+        if (map == null) return new Map();
+
+        if (commentBody.length === 0) {
+          map.delete(key);
+        } else {
+          map.set(key, true);
+        }
+
+        return map;
+      });
+      return newMap?.size ?? 0;
+    },
+    [mutate],
+  );
+  const clean = useCallback(
+    async (key: string) => {
+      const newMap = await mutate((map) => {
+        if (map == null) return new Map();
         map.delete(key);
         map.delete(key);
-      }
-      else {
-        map.set(key, true);
-      }
-
-      return map;
-    });
-    return newMap?.size ?? 0;
-  }, [mutate]);
-  const clean = useCallback(async(key: string) => {
-    const newMap = await mutate((map) => {
-      if (map == null) return new Map();
-      map.delete(key);
-      return map;
-    });
-    return newMap?.size ?? 0;
-  }, [mutate]);
+        return map;
+      });
+      return newMap?.size ?? 0;
+    },
+    [mutate],
+  );
 
 
   const reset = useCallback(() => mutate(new Map()), [mutate]);
   const reset = useCallback(() => mutate(new Map()), [mutate]);
 
 
@@ -351,13 +461,15 @@ export const useCommentEditorDirtyMap = (): SWRResponse<Map<string, boolean>, Er
   };
   };
 };
 };
 
 
-
 /** **********************************************************
 /** **********************************************************
  *                          SWR Hooks
  *                          SWR Hooks
  *                Determined value by context
  *                Determined value by context
  *********************************************************** */
  *********************************************************** */
 
 
-export const useIsAbleToShowTrashPageManagementButtons = (): SWRResponse<boolean, Error> => {
+export const useIsAbleToShowTrashPageManagementButtons = (): SWRResponse<
+  boolean,
+  Error
+> => {
   const key = 'isAbleToShowTrashPageManagementButtons';
   const key = 'isAbleToShowTrashPageManagementButtons';
 
 
   const { data: _currentUser } = useCurrentUser();
   const { data: _currentUser } = useCurrentUser();
@@ -371,15 +483,27 @@ export const useIsAbleToShowTrashPageManagementButtons = (): SWRResponse<boolean
   const isTrashPage = isPageExist && _isTrashPage === true;
   const isTrashPage = isPageExist && _isTrashPage === true;
   const isReadOnlyUser = isPageExist && _isReadOnlyUser === true;
   const isReadOnlyUser = isPageExist && _isReadOnlyUser === true;
 
 
-  const includesUndefined = [_currentUser, _currentPageId, _isNotFound, _isReadOnlyUser, _isTrashPage].some(v => v === undefined);
+  const includesUndefined = [
+    _currentUser,
+    _currentPageId,
+    _isNotFound,
+    _isReadOnlyUser,
+    _isTrashPage,
+  ].some((v) => v === undefined);
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : [key, isTrashPage, isCurrentUserExist, isReadOnlyUser],
-    ([, isTrashPage, isCurrentUserExist, isReadOnlyUser]) => isTrashPage && isCurrentUserExist && !isReadOnlyUser,
+    includesUndefined
+      ? null
+      : [key, isTrashPage, isCurrentUserExist, isReadOnlyUser],
+    ([, isTrashPage, isCurrentUserExist, isReadOnlyUser]) =>
+      isTrashPage && isCurrentUserExist && !isReadOnlyUser,
   );
   );
 };
 };
 
 
-export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> => {
+export const useIsAbleToShowPageManagement = (): SWRResponse<
+  boolean,
+  Error
+> => {
   const key = 'isAbleToShowPageManagement';
   const key = 'isAbleToShowPageManagement';
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
   const { data: _isTrashPage } = useIsTrashPage();
   const { data: _isTrashPage } = useIsTrashPage();
@@ -387,15 +511,23 @@ export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> =>
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
 
 
   const pageId = currentPageId;
   const pageId = currentPageId;
-  const includesUndefined = [pageId, _isTrashPage, _isSharedUser, isNotFound].some(v => v === undefined);
-  const isPageExist = (pageId != null) && isNotFound === false;
-  const isEmptyPage = (pageId != null) && isNotFound === true;
+  const includesUndefined = [
+    pageId,
+    _isTrashPage,
+    _isSharedUser,
+    isNotFound,
+  ].some((v) => v === undefined);
+  const isPageExist = pageId != null && isNotFound === false;
+  const isEmptyPage = pageId != null && isNotFound === true;
   const isTrashPage = isPageExist && _isTrashPage === true;
   const isTrashPage = isPageExist && _isTrashPage === true;
   const isSharedUser = isPageExist && _isSharedUser === true;
   const isSharedUser = isPageExist && _isSharedUser === true;
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : [key, pageId, isPageExist, isEmptyPage, isTrashPage, isSharedUser],
-    ([, , isPageExist, isEmptyPage, isTrashPage, isSharedUser]) => (isPageExist && !isTrashPage && !isSharedUser) || isEmptyPage,
+    includesUndefined
+      ? null
+      : [key, pageId, isPageExist, isEmptyPage, isTrashPage, isSharedUser],
+    ([, , isPageExist, isEmptyPage, isTrashPage, isSharedUser]) =>
+      (isPageExist && !isTrashPage && !isSharedUser) || isEmptyPage,
   );
   );
 };
 };
 
 
@@ -408,15 +540,35 @@ export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
 
 
-  const includesUndefined = [currentPagePath, isIdenticalPath, isNotFound, editorMode].some(v => v === undefined);
+  const includesUndefined = [
+    currentPagePath,
+    isIdenticalPath,
+    isNotFound,
+    editorMode,
+  ].some((v) => v === undefined);
 
 
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : [key, pageId, currentPagePath, isIdenticalPath, isNotFound, editorMode, shareLinkId],
+    includesUndefined
+      ? null
+      : [
+          key,
+          pageId,
+          currentPagePath,
+          isIdenticalPath,
+          isNotFound,
+          editorMode,
+          shareLinkId,
+        ],
     // "/trash" page does not exist on page collection and unable to add tags
     // "/trash" page does not exist on page collection and unable to add tags
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    () => !isUsersTopPage(currentPagePath!) && !isTrashTopPage(currentPagePath!) && shareLinkId == null && !isIdenticalPath && !(isViewMode && isNotFound),
+    () =>
+      !isUsersTopPage(currentPagePath!) &&
+      !isTrashTopPage(currentPagePath!) &&
+      shareLinkId == null &&
+      !isIdenticalPath &&
+      !(isViewMode && isNotFound),
   );
   );
 };
 };
 
 
@@ -425,7 +577,9 @@ export const useIsAbleToChangeEditorMode = (): SWRResponse<boolean, Error> => {
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
 
 
-  const includesUndefined = [isEditable, isSharedUser].some(v => v === undefined);
+  const includesUndefined = [isEditable, isSharedUser].some(
+    (v) => v === undefined,
+  );
 
 
   return useSWRImmutable(
   return useSWRImmutable(
     includesUndefined ? null : [key, isEditable, isSharedUser],
     includesUndefined ? null : [key, isEditable, isSharedUser],
@@ -439,8 +593,10 @@ export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
   const { data: pagePath } = useCurrentPagePath();
   const { data: pagePath } = useCurrentPagePath();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
 
 
-  const includesUndefined = [pageId, pagePath, isNotFound].some(v => v === undefined);
-  const isPageExist = (pageId != null) && !isNotFound;
+  const includesUndefined = [pageId, pagePath, isNotFound].some(
+    (v) => v === undefined,
+  );
+  const isPageExist = pageId != null && !isNotFound;
   const isUsersTopPagePath = pagePath != null && isUsersTopPage(pagePath);
   const isUsersTopPagePath = pagePath != null && isUsersTopPage(pagePath);
 
 
   return useSWRImmutable(
   return useSWRImmutable(
@@ -454,10 +610,7 @@ export const useIsUntitledPage = (): SWRResponse<boolean> => {
 
 
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
 
 
-  return useSWRStatic(
-    pageId == null ? null : [key, pageId],
-    undefined,
-    { fallbackData: false },
-  );
-
+  return useSWRStatic(pageId == null ? null : [key, pageId], undefined, {
+    fallbackData: false,
+  });
 };
 };

+ 6 - 2
apps/app/src/stores/use-editing-clients.ts

@@ -2,6 +2,10 @@ import { useSWRStatic } from '@growi/core/dist/swr';
 import type { EditingClient } from '@growi/editor';
 import type { EditingClient } from '@growi/editor';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 
 
-export const useEditingClients = (status?: EditingClient[]): SWRResponse<EditingClient[], Error> => {
-  return useSWRStatic<EditingClient[], Error>('editingUsers', status, { fallbackData: [] });
+export const useEditingClients = (
+  status?: EditingClient[],
+): SWRResponse<EditingClient[], Error> => {
+  return useSWRStatic<EditingClient[], Error>('editingUsers', status, {
+    fallbackData: [],
+  });
 };
 };

+ 93 - 31
apps/app/src/stores/user-group.tsx

@@ -1,5 +1,7 @@
 import type {
 import type {
-  IPageHasId, IUserGroupHasId, IUserGroupRelationHasId,
+  IPageHasId,
+  IUserGroupHasId,
+  IUserGroupRelationHasId,
 } from '@growi/core';
 } from '@growi/core';
 import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
 import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
@@ -7,23 +9,41 @@ import useSWRImmutable from 'swr/immutable';
 
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import type {
 import type {
+  AncestorUserGroupsResult,
+  ChildUserGroupListResult,
   IUserGroupRelationHasIdPopulatedUser,
   IUserGroupRelationHasIdPopulatedUser,
-  UserGroupResult, UserGroupListResult, ChildUserGroupListResult, UserGroupRelationListResult, UserGroupRelationsResult,
-  UserGroupPagesResult, SelectableParentUserGroupsResult, SelectableUserChildGroupsResult, AncestorUserGroupsResult,
+  SelectableParentUserGroupsResult,
+  SelectableUserChildGroupsResult,
+  UserGroupListResult,
+  UserGroupPagesResult,
+  UserGroupRelationListResult,
+  UserGroupRelationsResult,
+  UserGroupResult,
 } from '~/interfaces/user-group-response';
 } from '~/interfaces/user-group-response';
 
 
-export const useSWRxUserGroup = (groupId: string | null): SWRResponse<IUserGroupHasId, Error> => {
+export const useSWRxUserGroup = (
+  groupId: string | null,
+): SWRResponse<IUserGroupHasId, Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     groupId != null ? `/user-groups/${groupId}` : null,
     groupId != null ? `/user-groups/${groupId}` : null,
-    endpoint => apiv3Get<UserGroupResult>(endpoint).then(result => result.data.userGroup),
+    (endpoint) =>
+      apiv3Get<UserGroupResult>(endpoint).then(
+        (result) => result.data.userGroup,
+      ),
   );
   );
 };
 };
 
 
-export const useSWRxUserGroupList = (initialData?: IUserGroupHasId[], isExternalGroup = false): SWRResponse<IUserGroupHasId[], Error> => {
+export const useSWRxUserGroupList = (
+  initialData?: IUserGroupHasId[],
+  isExternalGroup = false,
+): SWRResponse<IUserGroupHasId[], Error> => {
   const url = isExternalGroup ? '/external-user-groups' : '/user-groups';
   const url = isExternalGroup ? '/external-user-groups' : '/user-groups';
   return useSWRImmutable(
   return useSWRImmutable(
     url,
     url,
-    endpoint => apiv3Get<UserGroupListResult>(endpoint, { pagination: false }).then(result => result.data.userGroups),
+    (endpoint) =>
+      apiv3Get<UserGroupListResult>(endpoint, { pagination: false }).then(
+        (result) => result.data.userGroups,
+      ),
     {
     {
       fallbackData: initialData,
       fallbackData: initialData,
     },
     },
@@ -31,21 +51,30 @@ export const useSWRxUserGroupList = (initialData?: IUserGroupHasId[], isExternal
 };
 };
 
 
 type ChildUserGroupListUtils = {
 type ChildUserGroupListUtils = {
-  updateChild(childGroupData: IUserGroupHasId): Promise<void>, // update one child and refresh list
-}
+  updateChild(childGroupData: IUserGroupHasId): Promise<void>; // update one child and refresh list
+};
 export const useSWRxChildUserGroupList = (
 export const useSWRxChildUserGroupList = (
-    parentIds?: string[], includeGrandChildren?: boolean,
-): SWRResponseWithUtils<ChildUserGroupListUtils, ChildUserGroupListResult, Error> => {
+  parentIds?: string[],
+  includeGrandChildren?: boolean,
+): SWRResponseWithUtils<
+  ChildUserGroupListUtils,
+  ChildUserGroupListResult,
+  Error
+> => {
   const shouldFetch = parentIds != null && parentIds.length > 0;
   const shouldFetch = parentIds != null && parentIds.length > 0;
 
 
   const swrResponse = useSWRImmutable(
   const swrResponse = useSWRImmutable(
-    shouldFetch ? ['/user-groups/children', parentIds, includeGrandChildren] : null,
-    ([endpoint, parentIds, includeGrandChildren]) => apiv3Get<ChildUserGroupListResult>(
-      endpoint, { parentIds, includeGrandChildren },
-    ).then((result => result.data)),
+    shouldFetch
+      ? ['/user-groups/children', parentIds, includeGrandChildren]
+      : null,
+    ([endpoint, parentIds, includeGrandChildren]) =>
+      apiv3Get<ChildUserGroupListResult>(endpoint, {
+        parentIds,
+        includeGrandChildren,
+      }).then((result) => result.data),
   );
   );
 
 
-  const updateChild = async(childGroupData: IUserGroupHasId) => {
+  const updateChild = async (childGroupData: IUserGroupHasId) => {
     await apiv3Put(`/user-groups/${childGroupData._id}`, {
     await apiv3Put(`/user-groups/${childGroupData._id}`, {
       name: childGroupData.name,
       name: childGroupData.name,
       description: childGroupData.description,
       description: childGroupData.description,
@@ -57,51 +86,84 @@ export const useSWRxChildUserGroupList = (
   return withUtils(swrResponse, { updateChild });
   return withUtils(swrResponse, { updateChild });
 };
 };
 
 
-export const useSWRxUserGroupRelations = (groupId: string | null): SWRResponse<IUserGroupRelationHasIdPopulatedUser[], Error> => {
+export const useSWRxUserGroupRelations = (
+  groupId: string | null,
+): SWRResponse<IUserGroupRelationHasIdPopulatedUser[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     groupId != null ? `/user-groups/${groupId}/user-group-relations` : null,
     groupId != null ? `/user-groups/${groupId}/user-group-relations` : null,
-    endpoint => apiv3Get<UserGroupRelationsResult>(endpoint).then(result => result.data.userGroupRelations),
+    (endpoint) =>
+      apiv3Get<UserGroupRelationsResult>(endpoint).then(
+        (result) => result.data.userGroupRelations,
+      ),
   );
   );
 };
 };
 
 
 export const useSWRxUserGroupRelationList = (
 export const useSWRxUserGroupRelationList = (
-    groupIds: string[] | null, childGroupIds?: string[], initialData?: IUserGroupRelationHasId[],
+  groupIds: string[] | null,
+  childGroupIds?: string[],
+  initialData?: IUserGroupRelationHasId[],
 ): SWRResponse<IUserGroupRelationHasId[], Error> => {
 ): SWRResponse<IUserGroupRelationHasId[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
-    groupIds != null ? ['/user-group-relations', groupIds, childGroupIds] : null,
-    ([endpoint, groupIds, childGroupIds]) => apiv3Get<UserGroupRelationListResult>(
-      endpoint, { groupIds, childGroupIds },
-    ).then(result => result.data.userGroupRelations),
+    groupIds != null
+      ? ['/user-group-relations', groupIds, childGroupIds]
+      : null,
+    ([endpoint, groupIds, childGroupIds]) =>
+      apiv3Get<UserGroupRelationListResult>(endpoint, {
+        groupIds,
+        childGroupIds,
+      }).then((result) => result.data.userGroupRelations),
     {
     {
       fallbackData: initialData,
       fallbackData: initialData,
     },
     },
   );
   );
 };
 };
 
 
-export const useSWRxUserGroupPages = (groupId: string | undefined, limit: number, offset: number): SWRResponse<IPageHasId[], Error> => {
+export const useSWRxUserGroupPages = (
+  groupId: string | undefined,
+  limit: number,
+  offset: number,
+): SWRResponse<IPageHasId[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     groupId != null ? [`/user-groups/${groupId}/pages`, limit, offset] : null,
     groupId != null ? [`/user-groups/${groupId}/pages`, limit, offset] : null,
-    ([endpoint, limit, offset]) => apiv3Get<UserGroupPagesResult>(endpoint, { limit, offset }).then(result => result.data.pages),
+    ([endpoint, limit, offset]) =>
+      apiv3Get<UserGroupPagesResult>(endpoint, { limit, offset }).then(
+        (result) => result.data.pages,
+      ),
   );
   );
 };
 };
 
 
-export const useSWRxSelectableParentUserGroups = (groupId: string | null): SWRResponse<IUserGroupHasId[], Error> => {
+export const useSWRxSelectableParentUserGroups = (
+  groupId: string | null,
+): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     groupId != null ? ['/user-groups/selectable-parent-groups', groupId] : null,
     groupId != null ? ['/user-groups/selectable-parent-groups', groupId] : null,
-    ([endpoint, groupId]) => apiv3Get<SelectableParentUserGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableParentGroups),
+    ([endpoint, groupId]) =>
+      apiv3Get<SelectableParentUserGroupsResult>(endpoint, { groupId }).then(
+        (result) => result.data.selectableParentGroups,
+      ),
   );
   );
 };
 };
 
 
-export const useSWRxSelectableChildUserGroups = (groupId: string | null): SWRResponse<IUserGroupHasId[], Error> => {
+export const useSWRxSelectableChildUserGroups = (
+  groupId: string | null,
+): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     groupId != null ? ['/user-groups/selectable-child-groups', groupId] : null,
     groupId != null ? ['/user-groups/selectable-child-groups', groupId] : null,
-    ([endpoint, groupId]) => apiv3Get<SelectableUserChildGroupsResult>(endpoint, { groupId }).then(result => result.data.selectableChildGroups),
+    ([endpoint, groupId]) =>
+      apiv3Get<SelectableUserChildGroupsResult>(endpoint, { groupId }).then(
+        (result) => result.data.selectableChildGroups,
+      ),
   );
   );
 };
 };
 
 
-export const useSWRxAncestorUserGroups = (groupId: string | null): SWRResponse<IUserGroupHasId[], Error> => {
+export const useSWRxAncestorUserGroups = (
+  groupId: string | null,
+): SWRResponse<IUserGroupHasId[], Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
     groupId != null ? ['/user-groups/ancestors', groupId] : null,
     groupId != null ? ['/user-groups/ancestors', groupId] : null,
-    ([endpoint, groupId]) => apiv3Get<AncestorUserGroupsResult>(endpoint, { groupId }).then(result => result.data.ancestorUserGroups),
+    ([endpoint, groupId]) =>
+      apiv3Get<AncestorUserGroupsResult>(endpoint, { groupId }).then(
+        (result) => result.data.ancestorUserGroups,
+      ),
   );
   );
 };
 };

+ 45 - 28
apps/app/src/stores/user.tsx

@@ -7,13 +7,17 @@ import { apiv3Get } from '~/client/util/apiv3-client';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import type { PopulatedGrantedGroup } from '~/interfaces/page-grant';
 import { checkAndUpdateImageUrlCached } from '~/stores/middlewares/user';
 import { checkAndUpdateImageUrlCached } from '~/stores/middlewares/user';
 
 
-export const useSWRxUsersList = (userIds: string[]): SWRResponse<IUserHasId[], Error> => {
-  const distinctUserIds = userIds.length > 0 ? Array.from(new Set(userIds)).sort() : [];
+export const useSWRxUsersList = (
+  userIds: string[],
+): SWRResponse<IUserHasId[], Error> => {
+  const distinctUserIds =
+    userIds.length > 0 ? Array.from(new Set(userIds)).sort() : [];
   return useSWR(
   return useSWR(
     distinctUserIds.length > 0 ? ['/users/list', distinctUserIds] : null,
     distinctUserIds.length > 0 ? ['/users/list', distinctUserIds] : null,
-    ([endpoint, userIds]) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
-      return response.data.users;
-    }),
+    ([endpoint, userIds]) =>
+      apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
+        return response.data.users;
+      }),
     {
     {
       use: [checkAndUpdateImageUrlCached],
       use: [checkAndUpdateImageUrlCached],
       revalidateOnFocus: false,
       revalidateOnFocus: false,
@@ -22,42 +26,55 @@ export const useSWRxUsersList = (userIds: string[]): SWRResponse<IUserHasId[], E
   );
   );
 };
 };
 
 
-
 type usernameRequestOptions = {
 type usernameRequestOptions = {
-  isIncludeActiveUser?: boolean,
-  isIncludeInactiveUser?: boolean,
-  isIncludeActivitySnapshotUser?: boolean,
-  isIncludeMixedUsernames?: boolean,
-}
+  isIncludeActiveUser?: boolean;
+  isIncludeInactiveUser?: boolean;
+  isIncludeActivitySnapshotUser?: boolean;
+  isIncludeMixedUsernames?: boolean;
+};
 
 
 type userData = {
 type userData = {
-  usernames: string[]
-  totalCount: number
-}
+  usernames: string[];
+  totalCount: number;
+};
 
 
 type usernameResult = {
 type usernameResult = {
-  activeUser?: userData
-  inactiveUser?: userData
-  activitySnapshotUser?: userData
-  mixedUsernames?: string[]
-}
+  activeUser?: userData;
+  inactiveUser?: userData;
+  activitySnapshotUser?: userData;
+  mixedUsernames?: string[];
+};
 
 
-export const useSWRxUsernames = (q: string, offset?: number, limit?: number, options?: usernameRequestOptions): SWRResponse<usernameResult, Error> => {
+export const useSWRxUsernames = (
+  q: string,
+  offset?: number,
+  limit?: number,
+  options?: usernameRequestOptions,
+): SWRResponse<usernameResult, Error> => {
   return useSWRImmutable(
   return useSWRImmutable(
-    (q != null && q.trim() !== '') ? ['/users/usernames', q, offset, limit, JSON.stringify(options)] : null,
-    ([endpoint, q, offset, limit, options]) => apiv3Get(endpoint, {
-      q, offset, limit, options,
-    }).then(result => result.data),
+    q != null && q.trim() !== ''
+      ? ['/users/usernames', q, offset, limit, JSON.stringify(options)]
+      : null,
+    ([endpoint, q, offset, limit, options]) =>
+      apiv3Get(endpoint, {
+        q,
+        offset,
+        limit,
+        options,
+      }).then((result) => result.data),
   );
   );
 };
 };
 
 
 type RelatedGroupsResponse = {
 type RelatedGroupsResponse = {
-  relatedGroups: PopulatedGrantedGroup[]
-}
+  relatedGroups: PopulatedGrantedGroup[];
+};
 
 
-export const useSWRxUserRelatedGroups = (): SWRResponse<RelatedGroupsResponse, Error> => {
+export const useSWRxUserRelatedGroups = (): SWRResponse<
+  RelatedGroupsResponse,
+  Error
+> => {
   return useSWRImmutable<RelatedGroupsResponse>(
   return useSWRImmutable<RelatedGroupsResponse>(
     ['/user/related-groups'],
     ['/user/related-groups'],
-    ([endpoint]) => apiv3Get(endpoint).then(response => response.data),
+    ([endpoint]) => apiv3Get(endpoint).then((response) => response.data),
   );
   );
 };
 };

+ 21 - 9
apps/app/src/stores/websocket.tsx

@@ -1,7 +1,8 @@
 import { useEffect } from 'react';
 import { useEffect } from 'react';
-
 import {
 import {
-  useGlobalSocket, GLOBAL_SOCKET_NS, useSWRStatic,
+  GLOBAL_SOCKET_NS,
+  useGlobalSocket,
+  useSWRStatic,
 } from '@growi/core/dist/swr';
 } from '@growi/core/dist/swr';
 import type { Socket } from 'socket.io-client';
 import type { Socket } from 'socket.io-client';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
@@ -19,7 +20,6 @@ export const GLOBAL_ADMIN_SOCKET_KEY = 'globalAdminSocket';
  * Global Socket
  * Global Socket
  */
  */
 export const useSetupGlobalSocket = (): void => {
 export const useSetupGlobalSocket = (): void => {
-
   const { data: socket, mutate } = useGlobalSocket();
   const { data: socket, mutate } = useGlobalSocket();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
@@ -35,21 +35,29 @@ export const useSetupGlobalSocket = (): void => {
       return;
       return;
     }
     }
 
 
-    mutate(async() => {
+    mutate(async () => {
       const { io } = await import('socket.io-client');
       const { io } = await import('socket.io-client');
       const newSocket = io(GLOBAL_SOCKET_NS, {
       const newSocket = io(GLOBAL_SOCKET_NS, {
         transports: ['websocket'],
         transports: ['websocket'],
       });
       });
 
 
-      newSocket.on('error', (err) => { logger.error(err) });
-      newSocket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
+      newSocket.on('error', (err) => {
+        logger.error(err);
+      });
+      newSocket.on('connect_error', (err) => {
+        logger.error('Failed to connect with websocket.', err);
+      });
 
 
       return newSocket;
       return newSocket;
     });
     });
 
 
     // Cleanup function to disconnect socket when component unmounts or user logs out
     // Cleanup function to disconnect socket when component unmounts or user logs out
     return () => {
     return () => {
-      if (socket != null && typeof socket === 'object' && 'disconnect' in socket) {
+      if (
+        socket != null &&
+        typeof socket === 'object' &&
+        'disconnect' in socket
+      ) {
         logger.debug('Disconnecting Socket.IO connection');
         logger.debug('Disconnecting Socket.IO connection');
         (socket as Socket).disconnect();
         (socket as Socket).disconnect();
         mutate(undefined, false); // Clear the SWR cache without revalidation
         mutate(undefined, false); // Clear the SWR cache without revalidation
@@ -81,11 +89,15 @@ export const useGlobalAdminSocket = (): SWRResponse<Socket, Error> => {
   return useSWRStatic(GLOBAL_ADMIN_SOCKET_KEY);
   return useSWRStatic(GLOBAL_ADMIN_SOCKET_KEY);
 };
 };
 
 
-export const useSetupGlobalSocketForPage = (pageId: string | undefined): void => {
+export const useSetupGlobalSocketForPage = (
+  pageId: string | undefined,
+): void => {
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
 
 
   useEffect(() => {
   useEffect(() => {
-    if (socket == null || pageId == null) { return }
+    if (socket == null || pageId == null) {
+      return;
+    }
 
 
     socket.emit(SocketEventName.JoinPage, { pageId });
     socket.emit(SocketEventName.JoinPage, { pageId });
 
 

+ 42 - 20
apps/app/src/stores/yjs.ts

@@ -1,5 +1,4 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
-
 import { useSWRStatic } from '@growi/core/dist/swr';
 import { useSWRStatic } from '@growi/core/dist/swr';
 import type { SWRResponse } from 'swr';
 import type { SWRResponse } from 'swr';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
@@ -11,41 +10,64 @@ import { useIsGuestUser } from '~/stores-universal/context';
 import { useCurrentPageId } from './page';
 import { useCurrentPageId } from './page';
 
 
 type CurrentPageYjsDataUtils = {
 type CurrentPageYjsDataUtils = {
-  updateHasYdocsNewerThanLatestRevision(hasYdocsNewerThanLatestRevision: boolean): void
-  updateAwarenessStateSize(awarenessStateSize: number): void
-}
+  updateHasYdocsNewerThanLatestRevision(
+    hasYdocsNewerThanLatestRevision: boolean,
+  ): void;
+  updateAwarenessStateSize(awarenessStateSize: number): void;
+};
 
 
-export const useCurrentPageYjsData = (): SWRResponse<CurrentPageYjsData, Error> & CurrentPageYjsDataUtils => {
+export const useCurrentPageYjsData = (): SWRResponse<
+  CurrentPageYjsData,
+  Error
+> &
+  CurrentPageYjsDataUtils => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
-  const key = !isGuestUser && currentPageId != null
-    ? `/page/${currentPageId}/yjs-data`
-    : null;
+  const key =
+    !isGuestUser && currentPageId != null
+      ? `/page/${currentPageId}/yjs-data`
+      : null;
 
 
   const swrResponse = useSWRStatic<CurrentPageYjsData, Error>(key, undefined);
   const swrResponse = useSWRStatic<CurrentPageYjsData, Error>(key, undefined);
 
 
-  const updateHasYdocsNewerThanLatestRevision = useCallback((hasYdocsNewerThanLatestRevision: boolean) => {
-    swrResponse.mutate({ ...swrResponse.data, hasYdocsNewerThanLatestRevision });
-  }, [swrResponse]);
+  const updateHasYdocsNewerThanLatestRevision = useCallback(
+    (hasYdocsNewerThanLatestRevision: boolean) => {
+      swrResponse.mutate({
+        ...swrResponse.data,
+        hasYdocsNewerThanLatestRevision,
+      });
+    },
+    [swrResponse],
+  );
 
 
-  const updateAwarenessStateSize = useCallback((awarenessStateSize: number) => {
-    swrResponse.mutate({ ...swrResponse.data, awarenessStateSize });
-  }, [swrResponse]);
+  const updateAwarenessStateSize = useCallback(
+    (awarenessStateSize: number) => {
+      swrResponse.mutate({ ...swrResponse.data, awarenessStateSize });
+    },
+    [swrResponse],
+  );
 
 
-  return Object.assign(swrResponse, { updateHasYdocsNewerThanLatestRevision, updateAwarenessStateSize });
+  return Object.assign(swrResponse, {
+    updateHasYdocsNewerThanLatestRevision,
+    updateAwarenessStateSize,
+  });
 };
 };
 
 
-export const useSWRMUTxCurrentPageYjsData = (): SWRMutationResponse<CurrentPageYjsData, Error> => {
+export const useSWRMUTxCurrentPageYjsData = (): SWRMutationResponse<
+  CurrentPageYjsData,
+  Error
+> => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
 
 
-  const key = currentPageId != null
-    ? `/page/${currentPageId}/yjs-data`
-    : null;
+  const key = currentPageId != null ? `/page/${currentPageId}/yjs-data` : null;
 
 
   return useSWRMutation(
   return useSWRMutation(
     key,
     key,
-    endpoint => apiv3Get<{ yjsData: CurrentPageYjsData }>(endpoint).then(result => result.data.yjsData),
+    (endpoint) =>
+      apiv3Get<{ yjsData: CurrentPageYjsData }>(endpoint).then(
+        (result) => result.data.yjsData,
+      ),
     { populateCache: true, revalidate: false },
     { populateCache: true, revalidate: false },
   );
   );
 };
 };

+ 4 - 3
biome.json

@@ -29,9 +29,10 @@
       "!packages/pdf-converter-client/specs",
       "!packages/pdf-converter-client/specs",
       "!apps/app/playwright",
       "!apps/app/playwright",
       "!apps/app/src/client",
       "!apps/app/src/client",
-      "!apps/app/src/server",
-      "!apps/app/src/services",
-      "!apps/app/src/stores"
+      "!apps/app/src/server/middlewares",
+      "!apps/app/src/server/models",
+      "!apps/app/src/server/routes",
+      "!apps/app/src/server/service"
     ]
     ]
   },
   },
   "formatter": {
   "formatter": {

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