Просмотр исходного кода

Merge branch 'dev/7.4.x' into imprv/173835-new-help-button

satof3 3 месяцев назад
Родитель
Сommit
96d0d9b71e
58 измененных файлов с 1534 добавлено и 1010 удалено
  1. 2 0
      apps/app/.eslintrc.js
  2. 1 0
      apps/app/config/logger/config.dev.js
  3. 0 4
      apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx
  4. 0 4
      apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  5. 4 0
      apps/app/src/client/components/PageAccessoriesModal/dynamic.tsx
  6. 0 5
      apps/app/src/client/services/AdminAppContainer.js
  7. 26 27
      apps/app/src/client/services/AdminCustomizeContainer.js
  8. 9 10
      apps/app/src/client/services/AdminExternalAccountsContainer.js
  9. 123 70
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  10. 27 18
      apps/app/src/client/services/AdminGitHubSecurityContainer.js
  11. 27 20
      apps/app/src/client/services/AdminGoogleSecurityContainer.js
  12. 4 8
      apps/app/src/client/services/AdminHomeContainer.js
  13. 0 2
      apps/app/src/client/services/AdminImportContainer.js
  14. 41 36
      apps/app/src/client/services/AdminLdapSecurityContainer.js
  15. 28 23
      apps/app/src/client/services/AdminLocalSecurityContainer.js
  16. 8 9
      apps/app/src/client/services/AdminMarkDownContainer.js
  17. 49 24
      apps/app/src/client/services/AdminNotificationContainer.js
  18. 67 52
      apps/app/src/client/services/AdminOidcSecurityContainer.js
  19. 51 38
      apps/app/src/client/services/AdminSamlSecurityContainer.js
  20. 5 5
      apps/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  21. 0 1
      apps/app/src/client/services/AdminSocketIoContainer.js
  22. 20 16
      apps/app/src/client/services/AdminUsersContainer.js
  23. 7 2
      apps/app/src/client/services/create-page/create-page.ts
  24. 95 81
      apps/app/src/client/services/create-page/use-create-page.tsx
  25. 21 18
      apps/app/src/client/services/create-page/use-create-template-page.ts
  26. 8 3
      apps/app/src/client/services/g2g-transfer.ts
  27. 0 1
      apps/app/src/client/services/maintenance-mode.ts
  28. 91 49
      apps/app/src/client/services/page-operation.ts
  29. 111 83
      apps/app/src/client/services/renderer/renderer.tsx
  30. 107 62
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  31. 117 64
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  32. 4 6
      apps/app/src/client/services/side-effects/hash-changed.ts
  33. 65 41
      apps/app/src/client/services/side-effects/page-updated.ts
  34. 1 1
      apps/app/src/client/services/side-effects/use-sticky.ts
  35. 3 2
      apps/app/src/client/services/update-page/conflict.tsx
  36. 7 2
      apps/app/src/client/services/update-page/update-page.ts
  37. 16 10
      apps/app/src/client/services/update-page/use-update-page.tsx
  38. 25 12
      apps/app/src/client/services/upload-attachments/upload-attachments.ts
  39. 4 4
      apps/app/src/client/services/use-print-mode.ts
  40. 23 22
      apps/app/src/client/services/use-start-editing.tsx
  41. 13 10
      apps/app/src/client/services/use-toastr-on-error.tsx
  42. 16 5
      apps/app/src/client/services/user-ui-settings.ts
  43. 26 9
      apps/app/src/client/util/apiv1-client.ts
  44. 29 10
      apps/app/src/client/util/apiv3-client.ts
  45. 48 12
      apps/app/src/client/util/bookmark-utils.ts
  46. 14 7
      apps/app/src/client/util/scope-util.test.ts
  47. 26 16
      apps/app/src/client/util/scope-util.ts
  48. 10 9
      apps/app/src/client/util/t-with-opt.ts
  49. 13 5
      apps/app/src/client/util/toastr.ts
  50. 36 30
      apps/app/src/client/util/use-input-validator.ts
  51. 11 15
      apps/app/src/pages/[[...path]]/page-data-props.ts
  52. 36 2
      apps/app/src/pages/[[...path]]/server-side-props.ts
  53. 30 0
      apps/app/src/server/service/page/index.ts
  54. 11 11
      apps/app/src/states/page/hooks.ts
  55. 0 2
      apps/app/src/states/page/use-fetch-current-page.ts
  56. 1 6
      apps/app/src/states/ui/page-abilities.ts
  57. 16 23
      apps/app/src/states/ui/sidebar/hydrate.ts
  58. 1 3
      biome.json

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

@@ -88,6 +88,8 @@ module.exports = {
     'src/server/service/page/**',
     'src/client/interfaces/**',
     'src/client/models/**',
+    'src/client/services/**',
+    'src/client/util/**',
   ],
   settings: {
     // resolve path aliases by eslint-import-resolver-typescript

+ 1 - 0
apps/app/config/logger/config.dev.js

@@ -15,6 +15,7 @@ module.exports = {
   'growi:routes:login': 'debug',
   'growi:routes:login-passport': 'debug',
   'growi:middleware:safe-redirect': 'debug',
+  'growi:services:page': 'debug',
   'growi:service:PassportService': 'debug',
   'growi:service:s2s-messaging:*': 'debug',
   'growi:service:yjs': 'debug',

+ 0 - 4
apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -292,8 +292,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const [isStickyActive, setStickyActive] = useState(false);
 
   const path = currentPage?.path ?? currentPathname;
-  // const grant = currentPage?.grant ?? grantData?.grant;
-  // const grantUserGroupId = currentPage?.grantedGroup?._id ?? grantData?.grantedGroup?.id;
 
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
 
@@ -425,8 +423,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
                 editorMode={editorMode}
                 isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
                 path={path}
-              // grant={grant}
-              // grantUserGroupId={grantUserGroupId}
               />
             )}
 

+ 0 - 4
apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.tsx

@@ -18,8 +18,6 @@ import { CustomNavDropdown, CustomNavTab } from '../CustomNavigation/CustomNav';
 import CustomTabContent from '../CustomNavigation/CustomTabContent';
 import ExpandOrContractButton from '../ExpandOrContractButton';
 
-import { useAutoOpenModalByQueryParam } from './hooks';
-
 import styles from './PageAccessoriesModal.module.scss';
 
 
@@ -45,8 +43,6 @@ const PageAccessoriesModalSubstance = ({ isWindowExpanded, setIsWindowExpanded }
   const status = usePageAccessoriesModalStatus();
   const { close, selectContents } = usePageAccessoriesModalActions();
 
-  useAutoOpenModalByQueryParam();
-
   // Memoize heavy navTabMapping calculation
   const navTabMapping = useMemo(() => {
     return {

+ 4 - 0
apps/app/src/client/components/PageAccessoriesModal/dynamic.tsx

@@ -3,11 +3,15 @@ import type { JSX } from 'react';
 import { useLazyLoader } from '~/components/utils/use-lazy-loader';
 import { usePageAccessoriesModalStatus } from '~/states/ui/modal/page-accessories';
 
+import { useAutoOpenModalByQueryParam } from './hooks';
+
 type PageAccessoriesModalProps = Record<string, unknown>;
 
 export const PageAccessoriesModalLazyLoaded = (): JSX.Element => {
   const status = usePageAccessoriesModalStatus();
 
+  useAutoOpenModalByQueryParam();
+
   const PageAccessoriesModal = useLazyLoader<PageAccessoriesModalProps>(
     'page-accessories-modal',
     () => import('./PageAccessoriesModal').then(mod => ({ default: mod.PageAccessoriesModal })),

+ 0 - 5
apps/app/src/client/services/AdminAppContainer.js

@@ -8,7 +8,6 @@ import { apiv3Get, apiv3Post, apiv3Put } from '../util/apiv3-client';
  * @extends {Container} unstated Container
  */
 export default class AdminAppContainer extends Container {
-
   constructor() {
     super();
 
@@ -42,7 +41,6 @@ export default class AdminAppContainer extends Container {
 
       isMaintenanceMode: false,
     };
-
   }
 
   /**
@@ -133,7 +131,6 @@ export default class AdminAppContainer extends Container {
     this.setState({ siteUrl });
   }
 
-
   /**
    * Change from address
    */
@@ -207,7 +204,6 @@ export default class AdminAppContainer extends Container {
     return appSettingParams;
   }
 
-
   /**
    * Update site url setting
    * @memberOf AdminAppContainer
@@ -294,5 +290,4 @@ export default class AdminAppContainer extends Container {
   async endMaintenanceMode() {
     await apiv3Post('/app-settings/maintenance-mode', { flag: false });
   }
-
 }

+ 26 - 27
apps/app/src/client/services/AdminCustomizeContainer.js

@@ -14,7 +14,6 @@ const logger = loggerFactory('growi:services:AdminCustomizeContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminCustomizeContainer extends Container {
-
   constructor() {
     super();
 
@@ -45,9 +44,9 @@ export default class AdminCustomizeContainer extends Container {
     this.switchPageListLimitationS = this.switchPageListLimitationS.bind(this);
     this.switchPageListLimitationM = this.switchPageListLimitationM.bind(this);
     this.switchPageListLimitationL = this.switchPageListLimitationL.bind(this);
-    this.switchPageListLimitationXL = this.switchPageListLimitationXL.bind(this);
+    this.switchPageListLimitationXL =
+      this.switchPageListLimitationXL.bind(this);
     this.switchShowPageSideAuthors = this.switchShowPageSideAuthors.bind(this);
-
   }
 
   /**
@@ -74,7 +73,8 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: customizeParams.pageLimitationXL,
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
         isAllReplyShown: customizeParams.isAllReplyShown,
-        isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
+        isSearchScopeChildrenAsDefault:
+          customizeParams.isSearchScopeChildrenAsDefault,
         isEnabledMarp: customizeParams.isEnabledMarp,
         currentCustomizeTitle: customizeParams.customizeTitle,
         currentCustomizeNoscript: customizeParams.customizeNoscript,
@@ -82,30 +82,29 @@ export default class AdminCustomizeContainer extends Container {
         currentCustomizeScript: customizeParams.customizeScript,
         showPageSideAuthors: customizeParams.showPageSideAuthors,
       });
-    }
-    catch (err) {
+    } catch (err) {
       this.setState({ retrieveError: err });
       logger.error(err);
       throw new Error('Failed to fetch data');
     }
   }
 
-
   /**
    * Switch enabledTimeLine
    */
   switchEnableTimeline() {
-    this.setState({ isEnabledTimeline:  !this.state.isEnabledTimeline });
+    this.setState({ isEnabledTimeline: !this.state.isEnabledTimeline });
   }
 
   /**
    * Switch enabledAttachTitleHeader
    */
   switchEnabledAttachTitleHeader() {
-    this.setState({ isEnabledAttachTitleHeader:  !this.state.isEnabledAttachTitleHeader });
+    this.setState({
+      isEnabledAttachTitleHeader: !this.state.isEnabledAttachTitleHeader,
+    });
   }
 
-
   /**
    * S: Switch pageListLimitationS
    */
@@ -138,7 +137,9 @@ export default class AdminCustomizeContainer extends Container {
    * Switch enabledStaleNotification
    */
   switchEnableStaleNotification() {
-    this.setState({ isEnabledStaleNotification:  !this.state.isEnabledStaleNotification });
+    this.setState({
+      isEnabledStaleNotification: !this.state.isEnabledStaleNotification,
+    });
   }
 
   /**
@@ -152,7 +153,10 @@ export default class AdminCustomizeContainer extends Container {
    * Switch isSearchScopeChildrenAsDefault
    */
   switchIsSearchScopeChildrenAsDefault() {
-    this.setState({ isSearchScopeChildrenAsDefault: !this.state.isSearchScopeChildrenAsDefault });
+    this.setState({
+      isSearchScopeChildrenAsDefault:
+        !this.state.isSearchScopeChildrenAsDefault,
+    });
   }
 
   /**
@@ -212,7 +216,8 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: this.state.pageLimitationXL,
         isEnabledStaleNotification: this.state.isEnabledStaleNotification,
         isAllReplyShown: this.state.isAllReplyShown,
-        isSearchScopeChildrenAsDefault: this.state.isSearchScopeChildrenAsDefault,
+        isSearchScopeChildrenAsDefault:
+          this.state.isSearchScopeChildrenAsDefault,
         showPageSideAuthors: this.state.showPageSideAuthors,
       });
       const { customizedParams } = response.data;
@@ -225,11 +230,11 @@ export default class AdminCustomizeContainer extends Container {
         pageLimitationXL: customizedParams.pageLimitationXL,
         isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
         isAllReplyShown: customizedParams.isAllReplyShown,
-        isSearchScopeChildrenAsDefault: customizedParams.isSearchScopeChildrenAsDefault,
+        isSearchScopeChildrenAsDefault:
+          customizedParams.isSearchScopeChildrenAsDefault,
         showPageSideAuthors: customizedParams.showPageSideAuthors,
       });
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error('Failed to update data');
     }
@@ -248,8 +253,7 @@ export default class AdminCustomizeContainer extends Container {
       this.setState({
         isEnabledMarp: customizedParams.isEnabledMarp,
       });
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error('Failed to update data');
     }
@@ -268,8 +272,7 @@ export default class AdminCustomizeContainer extends Container {
       this.setState({
         customizeTitle: customizedParams.customizeTitle,
       });
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error('Failed to update data');
     }
@@ -284,8 +287,7 @@ export default class AdminCustomizeContainer extends Container {
       this.setState({
         currentCustomizeNoscript: customizedParams.customizeNoscript,
       });
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error('Failed to update data');
     }
@@ -304,8 +306,7 @@ export default class AdminCustomizeContainer extends Container {
       this.setState({
         currentCustomizeCss: customizedParams.customizeCss,
       });
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error('Failed to update data');
     }
@@ -325,11 +326,9 @@ export default class AdminCustomizeContainer extends Container {
       this.setState({
         currentCustomizeScript: customizedParams.customizeScript,
       });
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error('Failed to update data');
     }
   }
-
 }

+ 9 - 10
apps/app/src/client/services/AdminExternalAccountsContainer.js

@@ -5,7 +5,6 @@ import loggerFactory from '~/utils/logger';
 
 import { apiv3Delete, apiv3Get } from '../util/apiv3-client';
 
-
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
 
@@ -14,7 +13,6 @@ const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminExternalAccountsContainer extends Container {
-
   constructor() {
     super();
 
@@ -28,7 +26,6 @@ export default class AdminExternalAccountsContainer extends Container {
       activePage: 1,
       pagingLimit: Infinity,
     };
-
   }
 
   /**
@@ -38,28 +35,29 @@ export default class AdminExternalAccountsContainer extends Container {
     return 'AdminExternalAccountsContainer';
   }
 
-
   /**
    * syncExternalAccounts of selectedPage
    * @memberOf AdminExternalAccountsContainer
    * @param {number} selectedPage
    */
   async retrieveExternalAccountsByPagingNum(selectedPage) {
-
     const params = { page: selectedPage };
     const { data } = await apiv3Get('/users/external-accounts', params);
 
     if (data.paginateResult == null) {
-      throw new Error('data must conclude \'paginateResult\' property.');
+      throw new Error("data must conclude 'paginateResult' property.");
     }
-    const { docs: externalAccounts, totalDocs: totalAccounts, limit: pagingLimit } = data.paginateResult;
+    const {
+      docs: externalAccounts,
+      totalDocs: totalAccounts,
+      limit: pagingLimit,
+    } = data.paginateResult;
     this.setState({
       externalAccounts,
       totalAccounts,
       pagingLimit,
       activePage: selectedPage,
     });
-
   }
 
   /**
@@ -69,10 +67,11 @@ export default class AdminExternalAccountsContainer extends Container {
    * @param {string} externalAccountId id of the External Account to be removed
    */
   async removeExternalAccountById(externalAccountId) {
-    const res = await apiv3Delete(`/users/external-accounts/${externalAccountId}/remove`);
+    const res = await apiv3Delete(
+      `/users/external-accounts/${externalAccountId}/remove`,
+    );
     const deletedUserData = res.data.externalAccount;
     await this.retrieveExternalAccountsByPagingNum(this.state.activePage);
     return deletedUserData.accountId;
   }
-
 }

+ 123 - 70
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -2,8 +2,10 @@ import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import {
-  PageSingleDeleteConfigValue, PageSingleDeleteCompConfigValue,
-  PageRecursiveDeleteConfigValue, PageRecursiveDeleteCompConfigValue,
+  PageRecursiveDeleteCompConfigValue,
+  PageRecursiveDeleteConfigValue,
+  PageSingleDeleteCompConfigValue,
+  PageSingleDeleteConfigValue,
 } from '~/interfaces/page-delete-config';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
@@ -15,7 +17,6 @@ import { toastError } from '../util/toastr';
  * @extends {Container} unstated Container
  */
 export default class AdminGeneralSecurityContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -29,9 +30,12 @@ export default class AdminGeneralSecurityContainer extends Container {
       wikiMode: '',
       currentRestrictGuestMode: '',
       currentPageDeletionAuthority: PageSingleDeleteConfigValue.AdminOnly,
-      currentPageRecursiveDeletionAuthority: PageRecursiveDeleteConfigValue.Inherit,
-      currentPageCompleteDeletionAuthority: PageSingleDeleteCompConfigValue.AdminOnly,
-      currentPageRecursiveCompleteDeletionAuthority: PageRecursiveDeleteCompConfigValue.Inherit,
+      currentPageRecursiveDeletionAuthority:
+        PageRecursiveDeleteConfigValue.Inherit,
+      currentPageCompleteDeletionAuthority:
+        PageSingleDeleteCompConfigValue.AdminOnly,
+      currentPageRecursiveCompleteDeletionAuthority:
+        PageRecursiveDeleteCompConfigValue.Inherit,
       currentGroupRestrictionDisplayMode: 'Hidden',
       currentOwnerRestrictionDisplayMode: 'Hidden',
       isAllGroupMembershipRequiredForPageCompleteDeletion: true,
@@ -57,33 +61,49 @@ export default class AdminGeneralSecurityContainer extends Container {
       shareLinksActivePage: 1,
     };
 
-    this.changeOwnerRestrictionDisplayMode = this.changeOwnerRestrictionDisplayMode.bind(this);
-    this.changeGroupRestrictionDisplayMode = this.changeGroupRestrictionDisplayMode.bind(this);
-    this.changePageDeletionAuthority = this.changePageDeletionAuthority.bind(this);
-    this.changePageCompleteDeletionAuthority = this.changePageCompleteDeletionAuthority.bind(this);
-    this.changePageRecursiveDeletionAuthority = this.changePageRecursiveDeletionAuthority.bind(this);
-    this.changePageRecursiveCompleteDeletionAuthority = this.changePageRecursiveCompleteDeletionAuthority.bind(this);
-    this.changePreviousPageRecursiveDeletionAuthority = this.changePreviousPageRecursiveDeletionAuthority.bind(this);
-    this.changePreviousPageRecursiveCompleteDeletionAuthority = this.changePreviousPageRecursiveCompleteDeletionAuthority.bind(this);
-
+    this.changeOwnerRestrictionDisplayMode =
+      this.changeOwnerRestrictionDisplayMode.bind(this);
+    this.changeGroupRestrictionDisplayMode =
+      this.changeGroupRestrictionDisplayMode.bind(this);
+    this.changePageDeletionAuthority =
+      this.changePageDeletionAuthority.bind(this);
+    this.changePageCompleteDeletionAuthority =
+      this.changePageCompleteDeletionAuthority.bind(this);
+    this.changePageRecursiveDeletionAuthority =
+      this.changePageRecursiveDeletionAuthority.bind(this);
+    this.changePageRecursiveCompleteDeletionAuthority =
+      this.changePageRecursiveCompleteDeletionAuthority.bind(this);
+    this.changePreviousPageRecursiveDeletionAuthority =
+      this.changePreviousPageRecursiveDeletionAuthority.bind(this);
+    this.changePreviousPageRecursiveCompleteDeletionAuthority =
+      this.changePreviousPageRecursiveCompleteDeletionAuthority.bind(this);
   }
 
   async retrieveSecurityData() {
     await this.retrieveSetupStratedies();
     const response = await apiv3Get('/security-setting/');
-    const { generalSetting, shareLinkSetting, generalAuth } = response.data.securityParams;
+    const { generalSetting, shareLinkSetting, generalAuth } =
+      response.data.securityParams;
     this.setState({
       currentRestrictGuestMode: generalSetting.restrictGuestMode,
       currentPageDeletionAuthority: generalSetting.pageDeletionAuthority,
-      currentPageCompleteDeletionAuthority: generalSetting.pageCompleteDeletionAuthority,
-      currentPageRecursiveDeletionAuthority: generalSetting.pageRecursiveDeletionAuthority,
-      currentPageRecursiveCompleteDeletionAuthority: generalSetting.pageRecursiveCompleteDeletionAuthority,
-      isAllGroupMembershipRequiredForPageCompleteDeletion: generalSetting.isAllGroupMembershipRequiredForPageCompleteDeletion,
+      currentPageCompleteDeletionAuthority:
+        generalSetting.pageCompleteDeletionAuthority,
+      currentPageRecursiveDeletionAuthority:
+        generalSetting.pageRecursiveDeletionAuthority,
+      currentPageRecursiveCompleteDeletionAuthority:
+        generalSetting.pageRecursiveCompleteDeletionAuthority,
+      isAllGroupMembershipRequiredForPageCompleteDeletion:
+        generalSetting.isAllGroupMembershipRequiredForPageCompleteDeletion,
       // Set display to 'Hidden' if hideRestrictedByOwner is anything but false.
-      currentOwnerRestrictionDisplayMode: generalSetting.hideRestrictedByOwner === false ? 'Displayed' : 'Hidden',
-      currentGroupRestrictionDisplayMode: generalSetting.hideRestrictedByGroup === false ? 'Displayed' : 'Hidden',
-      isUsersHomepageDeletionEnabled: generalSetting.isUsersHomepageDeletionEnabled,
-      isForceDeleteUserHomepageOnUserDeletion: generalSetting.isForceDeleteUserHomepageOnUserDeletion,
+      currentOwnerRestrictionDisplayMode:
+        generalSetting.hideRestrictedByOwner === false ? 'Displayed' : 'Hidden',
+      currentGroupRestrictionDisplayMode:
+        generalSetting.hideRestrictedByGroup === false ? 'Displayed' : 'Hidden',
+      isUsersHomepageDeletionEnabled:
+        generalSetting.isUsersHomepageDeletionEnabled,
+      isForceDeleteUserHomepageOnUserDeletion:
+        generalSetting.isForceDeleteUserHomepageOnUserDeletion,
       isRomUserAllowedToComment: generalSetting.isRomUserAllowedToComment,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
@@ -97,7 +117,6 @@ export default class AdminGeneralSecurityContainer extends Container {
     });
   }
 
-
   /**
    * Workaround for the mangling in production build to break constructor.name
    */
@@ -110,7 +129,9 @@ export default class AdminGeneralSecurityContainer extends Container {
    * @return {bool} isWikiModeForced
    */
   get isWikiModeForced() {
-    return this.state.wikiMode === 'public' || this.state.wikiMode === 'private';
+    return (
+      this.state.wikiMode === 'public' || this.state.wikiMode === 'private'
+    );
   }
 
   /**
@@ -180,7 +201,10 @@ export default class AdminGeneralSecurityContainer extends Container {
    * Switch isAllGroupMembershipRequiredForPageCompleteDeletion
    */
   switchIsAllGroupMembershipRequiredForPageCompleteDeletion() {
-    this.setState({ isAllGroupMembershipRequiredForPageCompleteDeletion: !this.state.isAllGroupMembershipRequiredForPageCompleteDeletion });
+    this.setState({
+      isAllGroupMembershipRequiredForPageCompleteDeletion:
+        !this.state.isAllGroupMembershipRequiredForPageCompleteDeletion,
+    });
   }
 
   /**
@@ -190,7 +214,6 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ previousPageRecursiveDeletionAuthority: val });
   }
 
-
   /**
    * Change previousPageRecursiveCompleteDeletionAuthority
    */
@@ -216,14 +239,20 @@ export default class AdminGeneralSecurityContainer extends Container {
    * Switch isUsersHomepageDeletionEnabled
    */
   switchIsUsersHomepageDeletionEnabled() {
-    this.setState({ isUsersHomepageDeletionEnabled: !this.state.isUsersHomepageDeletionEnabled });
+    this.setState({
+      isUsersHomepageDeletionEnabled:
+        !this.state.isUsersHomepageDeletionEnabled,
+    });
   }
 
   /**
    * Switch isForceDeleteUserHomepageOnUserDeletion
    */
   switchIsForceDeleteUserHomepageOnUserDeletion() {
-    this.setState({ isForceDeleteUserHomepageOnUserDeletion: !this.state.isForceDeleteUserHomepageOnUserDeletion });
+    this.setState({
+      isForceDeleteUserHomepageOnUserDeletion:
+        !this.state.isForceDeleteUserHomepageOnUserDeletion,
+    });
   }
 
   /**
@@ -233,44 +262,62 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ isRomUserAllowedToComment: bool });
   }
 
-
   /**
    * Update restrictGuestMode
    * @memberOf AdminGeneralSecuritySContainer
    * @return {string} Appearance
    */
   async updateGeneralSecuritySetting(formData) {
-
-    let requestParams = formData != null ? {
-      sessionMaxAge: formData.sessionMaxAge,
-      restrictGuestMode: formData.restrictGuestMode,
-      pageDeletionAuthority: formData.pageDeletionAuthority,
-      pageCompleteDeletionAuthority: formData.pageCompleteDeletionAuthority,
-      pageRecursiveDeletionAuthority: formData.pageRecursiveDeletionAuthority,
-      pageRecursiveCompleteDeletionAuthority: formData.pageRecursiveCompleteDeletionAuthority,
-      isAllGroupMembershipRequiredForPageCompleteDeletion: formData.isAllGroupMembershipRequiredForPageCompleteDeletion,
-      hideRestrictedByGroup: formData.hideRestrictedByGroup,
-      hideRestrictedByOwner: formData.hideRestrictedByOwner,
-      isUsersHomepageDeletionEnabled: formData.isUsersHomepageDeletionEnabled,
-      isForceDeleteUserHomepageOnUserDeletion: formData.isForceDeleteUserHomepageOnUserDeletion,
-      isRomUserAllowedToComment: formData.isRomUserAllowedToComment,
-    } : {
-      sessionMaxAge: this.state.sessionMaxAge,
-      restrictGuestMode: this.state.currentRestrictGuestMode,
-      pageDeletionAuthority: this.state.currentPageDeletionAuthority,
-      pageCompleteDeletionAuthority: this.state.currentPageCompleteDeletionAuthority,
-      pageRecursiveDeletionAuthority: this.state.currentPageRecursiveDeletionAuthority,
-      pageRecursiveCompleteDeletionAuthority: this.state.currentPageRecursiveCompleteDeletionAuthority,
-      isAllGroupMembershipRequiredForPageCompleteDeletion: this.state.isAllGroupMembershipRequiredForPageCompleteDeletion,
-      hideRestrictedByGroup: this.state.currentGroupRestrictionDisplayMode === 'Hidden',
-      hideRestrictedByOwner: this.state.currentOwnerRestrictionDisplayMode === 'Hidden',
-      isUsersHomepageDeletionEnabled: this.state.isUsersHomepageDeletionEnabled,
-      isForceDeleteUserHomepageOnUserDeletion: this.state.isForceDeleteUserHomepageOnUserDeletion,
-      isRomUserAllowedToComment: this.state.isRomUserAllowedToComment,
-    };
+    let requestParams =
+      formData != null
+        ? {
+            sessionMaxAge: formData.sessionMaxAge,
+            restrictGuestMode: formData.restrictGuestMode,
+            pageDeletionAuthority: formData.pageDeletionAuthority,
+            pageCompleteDeletionAuthority:
+              formData.pageCompleteDeletionAuthority,
+            pageRecursiveDeletionAuthority:
+              formData.pageRecursiveDeletionAuthority,
+            pageRecursiveCompleteDeletionAuthority:
+              formData.pageRecursiveCompleteDeletionAuthority,
+            isAllGroupMembershipRequiredForPageCompleteDeletion:
+              formData.isAllGroupMembershipRequiredForPageCompleteDeletion,
+            hideRestrictedByGroup: formData.hideRestrictedByGroup,
+            hideRestrictedByOwner: formData.hideRestrictedByOwner,
+            isUsersHomepageDeletionEnabled:
+              formData.isUsersHomepageDeletionEnabled,
+            isForceDeleteUserHomepageOnUserDeletion:
+              formData.isForceDeleteUserHomepageOnUserDeletion,
+            isRomUserAllowedToComment: formData.isRomUserAllowedToComment,
+          }
+        : {
+            sessionMaxAge: this.state.sessionMaxAge,
+            restrictGuestMode: this.state.currentRestrictGuestMode,
+            pageDeletionAuthority: this.state.currentPageDeletionAuthority,
+            pageCompleteDeletionAuthority:
+              this.state.currentPageCompleteDeletionAuthority,
+            pageRecursiveDeletionAuthority:
+              this.state.currentPageRecursiveDeletionAuthority,
+            pageRecursiveCompleteDeletionAuthority:
+              this.state.currentPageRecursiveCompleteDeletionAuthority,
+            isAllGroupMembershipRequiredForPageCompleteDeletion:
+              this.state.isAllGroupMembershipRequiredForPageCompleteDeletion,
+            hideRestrictedByGroup:
+              this.state.currentGroupRestrictionDisplayMode === 'Hidden',
+            hideRestrictedByOwner:
+              this.state.currentOwnerRestrictionDisplayMode === 'Hidden',
+            isUsersHomepageDeletionEnabled:
+              this.state.isUsersHomepageDeletionEnabled,
+            isForceDeleteUserHomepageOnUserDeletion:
+              this.state.isForceDeleteUserHomepageOnUserDeletion,
+            isRomUserAllowedToComment: this.state.isRomUserAllowedToComment,
+          };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/general-setting', requestParams);
+    const response = await apiv3Put(
+      '/security-setting/general-setting',
+      requestParams,
+    );
     const { securitySettingParams } = response.data;
     return securitySettingParams;
   }
@@ -282,7 +329,10 @@ export default class AdminGeneralSecurityContainer extends Container {
     const requestParams = {
       disableLinkSharing: !this.state.disableLinkSharing,
     };
-    const response = await apiv3Put('/security-setting/share-link-setting', requestParams);
+    const response = await apiv3Put(
+      '/security-setting/share-link-setting',
+      requestParams,
+    );
     this.setDisableLinkSharing(!this.state.disableLinkSharing);
     return response;
   }
@@ -299,8 +349,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       });
       await this.retrieveSetupStratedies();
       this.setState({ [stateVariableName]: isEnabled });
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
     }
   }
@@ -313,8 +362,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       const response = await apiv3Get('/security-setting/authentication');
       const { setupStrategies } = response.data;
       this.setState({ setupStrategies });
-    }
-    catch (err) {
+    } catch (err) {
       toastError(err);
     }
   }
@@ -323,18 +371,24 @@ export default class AdminGeneralSecurityContainer extends Container {
    * Retrieve All Sharelinks
    */
   async retrieveShareLinksByPagingNum(page) {
-
     const params = {
       page,
     };
 
-    const { data } = await apiv3Get('/security-setting/all-share-links', params);
+    const { data } = await apiv3Get(
+      '/security-setting/all-share-links',
+      params,
+    );
 
     if (data.paginateResult == null) {
-      throw new Error('data must conclude \'paginateResult\' property.');
+      throw new Error("data must conclude 'paginateResult' property.");
     }
 
-    const { docs: shareLinks, totalDocs: totalshareLinks, limit: shareLinksPagingLimit } = data.paginateResult;
+    const {
+      docs: shareLinks,
+      totalDocs: totalshareLinks,
+      limit: shareLinksPagingLimit,
+    } = data.paginateResult;
 
     this.setState({
       shareLinks,
@@ -385,5 +439,4 @@ export default class AdminGeneralSecurityContainer extends Container {
   async switchIsGitHubOAuthEnabled() {
     this.switchAuthentication('isGitHubEnabled', 'github');
   }
-
 }

+ 27 - 18
apps/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:security:AdminGitHubSecurityContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminGitHubSecurityContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -31,7 +30,6 @@ export default class AdminGitHubSecurityContainer extends Container {
       githubClientSecret: '',
       isSameUsernameTreatedAsIdenticalUser: false,
     };
-
   }
 
   /**
@@ -44,10 +42,10 @@ export default class AdminGitHubSecurityContainer extends Container {
       this.setState({
         githubClientId: githubOAuth.githubClientId,
         githubClientSecret: githubOAuth.githubClientSecret,
-        isSameUsernameTreatedAsIdenticalUser: githubOAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameUsernameTreatedAsIdenticalUser:
+          githubOAuth.isSameUsernameTreatedAsIdenticalUser,
       });
-    }
-    catch (err) {
+    } catch (err) {
       this.setState({ retrieveError: err });
       logger.error(err);
       throw new Error('Failed to fetch data');
@@ -65,33 +63,44 @@ export default class AdminGitHubSecurityContainer extends Container {
    * Switch isSameUsernameTreatedAsIdenticalUser
    */
   switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+    this.setState({
+      isSameUsernameTreatedAsIdenticalUser:
+        !this.state.isSameUsernameTreatedAsIdenticalUser,
+    });
   }
 
   /**
    * Update githubSetting
    */
   async updateGitHubSetting(formData) {
-    let requestParams = formData != null ? {
-      githubClientId: formData.githubClientId,
-      githubClientSecret: formData.githubClientSecret,
-      isSameUsernameTreatedAsIdenticalUser: formData.isSameUsernameTreatedAsIdenticalUser,
-    } : {
-      githubClientId: this.state.githubClientId,
-      githubClientSecret: this.state.githubClientSecret,
-      isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser,
-    };
+    let requestParams =
+      formData != null
+        ? {
+            githubClientId: formData.githubClientId,
+            githubClientSecret: formData.githubClientSecret,
+            isSameUsernameTreatedAsIdenticalUser:
+              formData.isSameUsernameTreatedAsIdenticalUser,
+          }
+        : {
+            githubClientId: this.state.githubClientId,
+            githubClientSecret: this.state.githubClientSecret,
+            isSameUsernameTreatedAsIdenticalUser:
+              this.state.isSameUsernameTreatedAsIdenticalUser,
+          };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/github-oauth', requestParams);
+    const response = await apiv3Put(
+      '/security-setting/github-oauth',
+      requestParams,
+    );
     const { securitySettingParams } = response.data;
 
     this.setState({
       githubClientId: securitySettingParams.githubClientId,
       githubClientSecret: securitySettingParams.githubClientSecret,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameUsernameTreatedAsIdenticalUser:
+        securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
     });
     return response;
   }
-
 }

+ 27 - 20
apps/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:security:AdminGoogleSecurityContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminGoogleSecurityContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -31,8 +30,6 @@ export default class AdminGoogleSecurityContainer extends Container {
       googleClientSecret: '',
       isSameEmailTreatedAsIdenticalUser: false,
     };
-
-
   }
 
   /**
@@ -45,10 +42,10 @@ export default class AdminGoogleSecurityContainer extends Container {
       this.setState({
         googleClientId: googleOAuth.googleClientId,
         googleClientSecret: googleOAuth.googleClientSecret,
-        isSameEmailTreatedAsIdenticalUser: googleOAuth.isSameEmailTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser:
+          googleOAuth.isSameEmailTreatedAsIdenticalUser,
       });
-    }
-    catch (err) {
+    } catch (err) {
       this.setState({ retrieveError: err });
       logger.error(err);
       throw new Error('Failed to fetch data');
@@ -66,34 +63,44 @@ export default class AdminGoogleSecurityContainer extends Container {
    * Switch isSameEmailTreatedAsIdenticalUser
    */
   switchIsSameEmailTreatedAsIdenticalUser() {
-    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
+    this.setState({
+      isSameEmailTreatedAsIdenticalUser:
+        !this.state.isSameEmailTreatedAsIdenticalUser,
+    });
   }
 
-
   /**
    * Update googleSetting
    */
   async updateGoogleSetting(formData) {
-    let requestParams = formData != null ? {
-      googleClientId: formData.googleClientId,
-      googleClientSecret: formData.googleClientSecret,
-      isSameEmailTreatedAsIdenticalUser: formData.isSameEmailTreatedAsIdenticalUser,
-    } : {
-      googleClientId: this.state.googleClientId,
-      googleClientSecret: this.state.googleClientSecret,
-      isSameEmailTreatedAsIdenticalUser: this.state.isSameEmailTreatedAsIdenticalUser,
-    };
+    let requestParams =
+      formData != null
+        ? {
+            googleClientId: formData.googleClientId,
+            googleClientSecret: formData.googleClientSecret,
+            isSameEmailTreatedAsIdenticalUser:
+              formData.isSameEmailTreatedAsIdenticalUser,
+          }
+        : {
+            googleClientId: this.state.googleClientId,
+            googleClientSecret: this.state.googleClientSecret,
+            isSameEmailTreatedAsIdenticalUser:
+              this.state.isSameEmailTreatedAsIdenticalUser,
+          };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/google-oauth', requestParams);
+    const response = await apiv3Put(
+      '/security-setting/google-oauth',
+      requestParams,
+    );
     const { securitySettingParams } = response.data;
 
     this.setState({
       googleClientId: securitySettingParams.googleClientId,
       googleClientSecret: securitySettingParams.googleClientSecret,
-      isSameEmailTreatedAsIdenticalUser: securitySettingParams.isSameEmailTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser:
+        securitySettingParams.isSameEmailTreatedAsIdenticalUser,
     });
     return response;
   }
-
 }

+ 4 - 8
apps/app/src/client/services/AdminHomeContainer.js

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:services:AdminHomeContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminHomeContainer extends Container {
-
   constructor() {
     super();
 
@@ -37,7 +36,6 @@ export default class AdminHomeContainer extends Container {
       isV5Compatible: null,
       isMaintenanceMode: null,
     };
-
   }
 
   /**
@@ -59,7 +57,7 @@ export default class AdminHomeContainer extends Container {
       const response = await apiv3Get('/admin-home/');
       const { adminHomeParams } = response.data;
 
-      this.setState(prevState => ({
+      this.setState((prevState) => ({
         ...prevState,
         growiVersion: adminHomeParams.growiVersion,
         nodeVersion: adminHomeParams.nodeVersion,
@@ -69,8 +67,7 @@ export default class AdminHomeContainer extends Container {
         isV5Compatible: adminHomeParams.isV5Compatible,
         isMaintenanceMode: adminHomeParams.isMaintenanceMode,
       }));
-    }
-    catch (err) {
+    } catch (err) {
       logger.error(err);
       throw new Error('Failed to retrive AdminHome data');
     }
@@ -80,13 +77,13 @@ export default class AdminHomeContainer extends Container {
    * sets button text when copying system information
    */
   onCopyPrefilledHostInformation() {
-    this.setState(prevState => ({
+    this.setState((prevState) => ({
       ...prevState,
       copyState: this.copyStateValues.DONE,
     }));
 
     this.timer = setTimeout(() => {
-      this.setState(prevState => ({
+      this.setState((prevState) => ({
         ...prevState,
         copyState: this.copyStateValues.DEFAULT,
       }));
@@ -111,5 +108,4 @@ export default class AdminHomeContainer extends Container {
 
 *(Accessing https://{GROWI_HOST}/admin helps you to fill in above versions)*`;
   }
-
 }

+ 0 - 2
apps/app/src/client/services/AdminImportContainer.js

@@ -6,7 +6,6 @@ import { Container } from 'unstated';
  * @extends {Container} unstated Container
  */
 export default class AdminImportContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -27,5 +26,4 @@ export default class AdminImportContainer extends Container {
   static getClassName() {
     return 'AdminImportContainer';
   }
-
 }

+ 41 - 36
apps/app/src/client/services/AdminLdapSecurityContainer.js

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminLdapSecurityContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -38,7 +37,6 @@ export default class AdminLdapSecurityContainer extends Container {
       ldapGroupSearchFilter: '',
       ldapGroupDnProperty: '',
     };
-
   }
 
   /**
@@ -55,22 +53,21 @@ export default class AdminLdapSecurityContainer extends Container {
         ldapBindDNPassword: ldapAuth.ldapBindDNPassword,
         ldapSearchFilter: ldapAuth.ldapSearchFilter,
         ldapAttrMapUsername: ldapAuth.ldapAttrMapUsername,
-        isSameUsernameTreatedAsIdenticalUser: ldapAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameUsernameTreatedAsIdenticalUser:
+          ldapAuth.isSameUsernameTreatedAsIdenticalUser,
         ldapAttrMapMail: ldapAuth.ldapAttrMapMail,
         ldapAttrMapName: ldapAuth.ldapAttrMapName,
         ldapGroupSearchBase: ldapAuth.ldapGroupSearchBase,
         ldapGroupSearchFilter: ldapAuth.ldapGroupSearchFilter,
         ldapGroupDnProperty: ldapAuth.ldapGroupDnProperty,
       });
-    }
-    catch (err) {
+    } catch (err) {
       this.setState({ retrieveError: err });
       logger.error(err);
       throw new Error('Failed to fetch data');
     }
   }
 
-
   /**
    * Workaround for the mangling in production build to break constructor.name
    */
@@ -90,40 +87,48 @@ export default class AdminLdapSecurityContainer extends Container {
    * Switch is same username treated as identical user
    */
   switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+    this.setState({
+      isSameUsernameTreatedAsIdenticalUser:
+        !this.state.isSameUsernameTreatedAsIdenticalUser,
+    });
   }
 
   /**
    * Update ldap option
    */
   async updateLdapSetting(formData) {
-    let requestParams = formData != null ? {
-      serverUrl: formData.serverUrl,
-      isUserBind: formData.isUserBind,
-      ldapBindDN: formData.ldapBindDN,
-      ldapBindDNPassword: formData.ldapBindDNPassword,
-      ldapSearchFilter: formData.ldapSearchFilter,
-      ldapAttrMapUsername: formData.ldapAttrMapUsername,
-      isSameUsernameTreatedAsIdenticalUser: formData.isSameUsernameTreatedAsIdenticalUser,
-      ldapAttrMapMail: formData.ldapAttrMapMail,
-      ldapAttrMapName: formData.ldapAttrMapName,
-      ldapGroupSearchBase: formData.ldapGroupSearchBase,
-      ldapGroupSearchFilter: formData.ldapGroupSearchFilter,
-      ldapGroupDnProperty: formData.ldapGroupDnProperty,
-    } : {
-      serverUrl: this.state.serverUrl,
-      isUserBind: this.state.isUserBind,
-      ldapBindDN: this.state.ldapBindDN,
-      ldapBindDNPassword: this.state.ldapBindDNPassword,
-      ldapSearchFilter: this.state.ldapSearchFilter,
-      ldapAttrMapUsername: this.state.ldapAttrMapUsername,
-      isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser,
-      ldapAttrMapMail: this.state.ldapAttrMapMail,
-      ldapAttrMapName: this.state.ldapAttrMapName,
-      ldapGroupSearchBase: this.state.ldapGroupSearchBase,
-      ldapGroupSearchFilter: this.state.ldapGroupSearchFilter,
-      ldapGroupDnProperty: this.state.ldapGroupDnProperty,
-    };
+    let requestParams =
+      formData != null
+        ? {
+            serverUrl: formData.serverUrl,
+            isUserBind: formData.isUserBind,
+            ldapBindDN: formData.ldapBindDN,
+            ldapBindDNPassword: formData.ldapBindDNPassword,
+            ldapSearchFilter: formData.ldapSearchFilter,
+            ldapAttrMapUsername: formData.ldapAttrMapUsername,
+            isSameUsernameTreatedAsIdenticalUser:
+              formData.isSameUsernameTreatedAsIdenticalUser,
+            ldapAttrMapMail: formData.ldapAttrMapMail,
+            ldapAttrMapName: formData.ldapAttrMapName,
+            ldapGroupSearchBase: formData.ldapGroupSearchBase,
+            ldapGroupSearchFilter: formData.ldapGroupSearchFilter,
+            ldapGroupDnProperty: formData.ldapGroupDnProperty,
+          }
+        : {
+            serverUrl: this.state.serverUrl,
+            isUserBind: this.state.isUserBind,
+            ldapBindDN: this.state.ldapBindDN,
+            ldapBindDNPassword: this.state.ldapBindDNPassword,
+            ldapSearchFilter: this.state.ldapSearchFilter,
+            ldapAttrMapUsername: this.state.ldapAttrMapUsername,
+            isSameUsernameTreatedAsIdenticalUser:
+              this.state.isSameUsernameTreatedAsIdenticalUser,
+            ldapAttrMapMail: this.state.ldapAttrMapMail,
+            ldapAttrMapName: this.state.ldapAttrMapName,
+            ldapGroupSearchBase: this.state.ldapGroupSearchBase,
+            ldapGroupSearchFilter: this.state.ldapGroupSearchFilter,
+            ldapGroupDnProperty: this.state.ldapGroupDnProperty,
+          };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     const response = await apiv3Put('/security-setting/ldap', requestParams);
@@ -136,7 +141,8 @@ export default class AdminLdapSecurityContainer extends Container {
       ldapBindDNPassword: securitySettingParams.ldapBindDNPassword,
       ldapSearchFilter: securitySettingParams.ldapSearchFilter,
       ldapAttrMapUsername: securitySettingParams.ldapAttrMapUsername,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameUsernameTreatedAsIdenticalUser:
+        securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
       ldapAttrMapMail: securitySettingParams.ldapAttrMapMail,
       ldapAttrMapName: securitySettingParams.ldapAttrMapName,
       ldapGroupSearchBase: securitySettingParams.ldapGroupSearchBase,
@@ -145,5 +151,4 @@ export default class AdminLdapSecurityContainer extends Container {
     });
     return response;
   }
-
 }

+ 28 - 23
apps/app/src/client/services/AdminLocalSecurityContainer.js

@@ -12,7 +12,6 @@ const logger = loggerFactory('growi:services:AdminLocalSecurityContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminLocalSecurityContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -33,7 +32,6 @@ export default class AdminLocalSecurityContainer extends Container {
       isPasswordResetEnabled: false,
       isEmailAuthenticationEnabled: false,
     };
-
   }
 
   async retrieveSecurityData() {
@@ -47,13 +45,11 @@ export default class AdminLocalSecurityContainer extends Container {
         isPasswordResetEnabled: localSetting.isPasswordResetEnabled,
         isEmailAuthenticationEnabled: localSetting.isEmailAuthenticationEnabled,
       });
-    }
-    catch (err) {
+    } catch (err) {
       this.setState({ retrieveError: err });
       logger.error(err);
       throw new Error('Failed to fetch data');
     }
-
   }
 
   /**
@@ -63,7 +59,6 @@ export default class AdminLocalSecurityContainer extends Container {
     return 'AdminLocalSecurityContainer';
   }
 
-
   /**
    * Change registration mode
    */
@@ -75,32 +70,43 @@ export default class AdminLocalSecurityContainer extends Container {
    * Switch password reset enabled
    */
   switchIsPasswordResetEnabled() {
-    this.setState({ isPasswordResetEnabled: !this.state.isPasswordResetEnabled });
+    this.setState({
+      isPasswordResetEnabled: !this.state.isPasswordResetEnabled,
+    });
   }
 
   /**
    * Switch email authentication enabled
    */
   switchIsEmailAuthenticationEnabled() {
-    this.setState({ isEmailAuthenticationEnabled: !this.state.isEmailAuthenticationEnabled });
+    this.setState({
+      isEmailAuthenticationEnabled: !this.state.isEmailAuthenticationEnabled,
+    });
   }
 
   /**
    * update local security setting
    */
   async updateLocalSecuritySetting(formData) {
-    const requestParams = formData != null ? {
-      registrationMode: formData.registrationMode,
-      registrationWhitelist: formData.registrationWhitelist,
-      isPasswordResetEnabled: formData.isPasswordResetEnabled,
-      isEmailAuthenticationEnabled: formData.isEmailAuthenticationEnabled,
-    } : {
-      registrationMode: this.state.registrationMode,
-      registrationWhitelist: this.state.registrationWhitelist,
-      isPasswordResetEnabled: this.state.isPasswordResetEnabled,
-      isEmailAuthenticationEnabled: this.state.isEmailAuthenticationEnabled,
-    };
-    const response = await apiv3Put('/security-setting/local-setting', requestParams);
+    const requestParams =
+      formData != null
+        ? {
+            registrationMode: formData.registrationMode,
+            registrationWhitelist: formData.registrationWhitelist,
+            isPasswordResetEnabled: formData.isPasswordResetEnabled,
+            isEmailAuthenticationEnabled: formData.isEmailAuthenticationEnabled,
+          }
+        : {
+            registrationMode: this.state.registrationMode,
+            registrationWhitelist: this.state.registrationWhitelist,
+            isPasswordResetEnabled: this.state.isPasswordResetEnabled,
+            isEmailAuthenticationEnabled:
+              this.state.isEmailAuthenticationEnabled,
+          };
+    const response = await apiv3Put(
+      '/security-setting/local-setting',
+      requestParams,
+    );
 
     const { localSettingParams } = response.data;
 
@@ -108,11 +114,10 @@ export default class AdminLocalSecurityContainer extends Container {
       registrationMode: localSettingParams.registrationMode,
       registrationWhitelist: localSettingParams.registrationWhitelist,
       isPasswordResetEnabled: localSettingParams.isPasswordResetEnabled,
-      isEmailAuthenticationEnabled: localSettingParams.isEmailAuthenticationEnabled,
+      isEmailAuthenticationEnabled:
+        localSettingParams.isEmailAuthenticationEnabled,
     });
 
     return localSettingParams;
   }
-
-
 }

+ 8 - 9
apps/app/src/client/services/AdminMarkDownContainer.js

@@ -8,7 +8,6 @@ import { apiv3Get, apiv3Put } from '../util/apiv3-client';
  * @extends {Container} unstated Container
  */
 export default class AdminMarkDownContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -31,7 +30,8 @@ export default class AdminMarkDownContainer extends Container {
     };
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
-    this.setAdminPreferredIndentSize = this.setAdminPreferredIndentSize.bind(this);
+    this.setAdminPreferredIndentSize =
+      this.setAdminPreferredIndentSize.bind(this);
   }
 
   /**
@@ -50,7 +50,8 @@ export default class AdminMarkDownContainer extends Container {
 
     this.setState({
       isEnabledLinebreaks: markdownParams.isEnabledLinebreaks,
-      isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
+      isEnabledLinebreaksInComments:
+        markdownParams.isEnabledLinebreaksInComments,
       adminPreferredIndentSize: markdownParams.adminPreferredIndentSize,
       isIndentSizeForced: markdownParams.isIndentSizeForced,
       isEnabledXss: markdownParams.isEnabledXss,
@@ -75,7 +76,6 @@ export default class AdminMarkDownContainer extends Container {
    * Update LineBreak Setting
    */
   async updateLineBreakSetting() {
-
     const response = await apiv3Put('/markdown-setting/lineBreak', {
       isEnabledLinebreaks: this.state.isEnabledLinebreaks,
       isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
@@ -88,7 +88,6 @@ export default class AdminMarkDownContainer extends Container {
    * Update
    */
   async updateIndentSetting() {
-
     const response = await apiv3Put('/markdown-setting/indent', {
       adminPreferredIndentSize: this.state.adminPreferredIndentSize,
       isIndentSizeForced: this.state.isIndentSizeForced,
@@ -104,13 +103,14 @@ export default class AdminMarkDownContainer extends Container {
     let { tagWhitelist = '' } = this.state;
     const { attrWhitelist = '{}' } = this.state;
 
-    tagWhitelist = Array.isArray(tagWhitelist) ? tagWhitelist : tagWhitelist.split(',');
+    tagWhitelist = Array.isArray(tagWhitelist)
+      ? tagWhitelist
+      : tagWhitelist.split(',');
 
     try {
       // Check if parsing is possible
       JSON.parse(attrWhitelist);
-    }
-    catch (err) {
+    } catch (err) {
       throw Error(`attrWhitelist parsing error occured: ${err.message}`);
     }
 
@@ -121,5 +121,4 @@ export default class AdminMarkDownContainer extends Container {
       attrWhitelist,
     });
   }
-
 }

+ 49 - 24
apps/app/src/client/services/AdminNotificationContainer.js

@@ -2,7 +2,10 @@ import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 
 import {
-  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+  apiv3Delete,
+  apiv3Get,
+  apiv3Post,
+  apiv3Put,
 } from '../util/apiv3-client';
 
 /**
@@ -10,7 +13,6 @@ import {
  * @extends {Container} unstated Container
  */
 export default class AdminNotificationContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -32,7 +34,6 @@ export default class AdminNotificationContainer extends Container {
       isNotificationForGroupPageEnabled: false,
       globalNotifications: [],
     };
-
   }
 
   /**
@@ -55,8 +56,10 @@ export default class AdminNotificationContainer extends Container {
       currentBotType: notificationParams.currentBotType,
 
       userNotifications: notificationParams.userNotifications,
-      isNotificationForOwnerPageEnabled: notificationParams.isNotificationForOwnerPageEnabled,
-      isNotificationForGroupPageEnabled: notificationParams.isNotificationForGroupPageEnabled,
+      isNotificationForOwnerPageEnabled:
+        notificationParams.isNotificationForOwnerPageEnabled,
+      isNotificationForGroupPageEnabled:
+        notificationParams.isNotificationForGroupPageEnabled,
       globalNotifications: notificationParams.globalNotifications,
     });
   }
@@ -66,11 +69,14 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    */
   async updateSlackAppConfiguration() {
-    const response = await apiv3Put('/notification-setting/slack-configuration', {
-      webhookUrl: this.state.webhookUrl,
-      isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
-      slackToken: this.state.slackToken,
-    });
+    const response = await apiv3Put(
+      '/notification-setting/slack-configuration',
+      {
+        webhookUrl: this.state.webhookUrl,
+        isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
+        slackToken: this.state.slackToken,
+      },
+    );
 
     return response;
   }
@@ -80,19 +86,26 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    */
   async addNotificationPattern(pathPattern, channel) {
-    const response = await apiv3Post('/notification-setting/user-notification', {
-      pathPattern,
-      channel,
-    });
+    const response = await apiv3Post(
+      '/notification-setting/user-notification',
+      {
+        pathPattern,
+        channel,
+      },
+    );
 
-    this.setState({ userNotifications: response.data.responseParams.userNotifications });
+    this.setState({
+      userNotifications: response.data.responseParams.userNotifications,
+    });
   }
 
   /**
    * Delete user trigger notification pattern
    */
   async deleteUserTriggerNotificationPattern(notificatiionId) {
-    const response = await apiv3Delete(`/notification-setting/user-notification/${notificatiionId}`);
+    const response = await apiv3Delete(
+      `/notification-setting/user-notification/${notificatiionId}`,
+    );
     const deletedNotificaton = response.data;
     await this.retrieveNotificationData();
     return deletedNotificaton;
@@ -102,14 +115,20 @@ export default class AdminNotificationContainer extends Container {
    * Switch isNotificationForOwnerPageEnabled
    */
   switchIsNotificationForOwnerPageEnabled() {
-    this.setState({ isNotificationForOwnerPageEnabled: !this.state.isNotificationForOwnerPageEnabled });
+    this.setState({
+      isNotificationForOwnerPageEnabled:
+        !this.state.isNotificationForOwnerPageEnabled,
+    });
   }
 
   /**
    * Switch isNotificationForGroupPageEnabled
    */
   switchIsNotificationForGroupPageEnabled() {
-    this.setState({ isNotificationForGroupPageEnabled: !this.state.isNotificationForGroupPageEnabled });
+    this.setState({
+      isNotificationForGroupPageEnabled:
+        !this.state.isNotificationForGroupPageEnabled,
+    });
   }
 
   /**
@@ -117,10 +136,15 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    */
   async updateGlobalNotificationForPages() {
-    const response = await apiv3Put('/notification-setting/notify-for-page-grant/', {
-      isNotificationForOwnerPageEnabled: this.state.isNotificationForOwnerPageEnabled,
-      isNotificationForGroupPageEnabled: this.state.isNotificationForGroupPageEnabled,
-    });
+    const response = await apiv3Put(
+      '/notification-setting/notify-for-page-grant/',
+      {
+        isNotificationForOwnerPageEnabled:
+          this.state.isNotificationForOwnerPageEnabled,
+        isNotificationForGroupPageEnabled:
+          this.state.isNotificationForGroupPageEnabled,
+      },
+    );
 
     return response;
   }
@@ -129,10 +153,11 @@ export default class AdminNotificationContainer extends Container {
    * Delete global notification pattern
    */
   async deleteGlobalNotificationPattern(notificatiionId) {
-    const response = await apiv3Delete(`/notification-setting/global-notification/${notificatiionId}`);
+    const response = await apiv3Delete(
+      `/notification-setting/global-notification/${notificatiionId}`,
+    );
     const deletedNotificaton = response.data;
     await this.retrieveNotificationData();
     return deletedNotificaton;
   }
-
 }

+ 67 - 52
apps/app/src/client/services/AdminOidcSecurityContainer.js

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminOidcSecurityContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -44,7 +43,6 @@ export default class AdminOidcSecurityContainer extends Container {
       isSameUsernameTreatedAsIdenticalUser: false,
       isSameEmailTreatedAsIdenticalUser: false,
     };
-
   }
 
   /**
@@ -71,11 +69,12 @@ export default class AdminOidcSecurityContainer extends Container {
         oidcAttrMapUserName: oidcAuth.oidcAttrMapUserName,
         oidcAttrMapName: oidcAuth.oidcAttrMapName,
         oidcAttrMapEmail: oidcAuth.oidcAttrMapEmail,
-        isSameUsernameTreatedAsIdenticalUser: oidcAuth.isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser: oidcAuth.isSameEmailTreatedAsIdenticalUser,
+        isSameUsernameTreatedAsIdenticalUser:
+          oidcAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser:
+          oidcAuth.isSameEmailTreatedAsIdenticalUser,
       });
-    }
-    catch (err) {
+    } catch (err) {
       this.setState({ retrieveError: err });
       logger.error(err);
       throw new Error('Failed to fetch data');
@@ -93,59 +92,72 @@ export default class AdminOidcSecurityContainer extends Container {
    * Switch sameUsernameTreatedAsIdenticalUser
    */
   switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+    this.setState({
+      isSameUsernameTreatedAsIdenticalUser:
+        !this.state.isSameUsernameTreatedAsIdenticalUser,
+    });
   }
 
   /**
    * Switch sameEmailTreatedAsIdenticalUser
    */
   switchIsSameEmailTreatedAsIdenticalUser() {
-    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
+    this.setState({
+      isSameEmailTreatedAsIdenticalUser:
+        !this.state.isSameEmailTreatedAsIdenticalUser,
+    });
   }
 
   /**
    * Update OpenID Connect
    */
   async updateOidcSetting(formData) {
-    let requestParams = formData != null ? {
-      oidcProviderName: formData.oidcProviderName,
-      oidcIssuerHost: formData.oidcIssuerHost,
-      oidcAuthorizationEndpoint: formData.oidcAuthorizationEndpoint,
-      oidcTokenEndpoint: formData.oidcTokenEndpoint,
-      oidcRevocationEndpoint: formData.oidcRevocationEndpoint,
-      oidcIntrospectionEndpoint: formData.oidcIntrospectionEndpoint,
-      oidcUserInfoEndpoint: formData.oidcUserInfoEndpoint,
-      oidcEndSessionEndpoint: formData.oidcEndSessionEndpoint,
-      oidcRegistrationEndpoint: formData.oidcRegistrationEndpoint,
-      oidcJWKSUri: formData.oidcJWKSUri,
-      oidcClientId: formData.oidcClientId,
-      oidcClientSecret: formData.oidcClientSecret,
-      oidcAttrMapId: formData.oidcAttrMapId,
-      oidcAttrMapUserName: formData.oidcAttrMapUserName,
-      oidcAttrMapName: formData.oidcAttrMapName,
-      oidcAttrMapEmail: formData.oidcAttrMapEmail,
-      isSameUsernameTreatedAsIdenticalUser: formData.isSameUsernameTreatedAsIdenticalUser,
-      isSameEmailTreatedAsIdenticalUser: formData.isSameEmailTreatedAsIdenticalUser,
-    } : {
-      oidcProviderName: this.state.oidcProviderName,
-      oidcIssuerHost: this.state.oidcIssuerHost,
-      oidcAuthorizationEndpoint: this.state.oidcAuthorizationEndpoint,
-      oidcTokenEndpoint: this.state.oidcTokenEndpoint,
-      oidcRevocationEndpoint: this.state.oidcRevocationEndpoint,
-      oidcIntrospectionEndpoint: this.state.oidcIntrospectionEndpoint,
-      oidcUserInfoEndpoint: this.state.oidcUserInfoEndpoint,
-      oidcEndSessionEndpoint: this.state.oidcEndSessionEndpoint,
-      oidcRegistrationEndpoint: this.state.oidcRegistrationEndpoint,
-      oidcJWKSUri: this.state.oidcJWKSUri,
-      oidcClientId: this.state.oidcClientId,
-      oidcClientSecret: this.state.oidcClientSecret,
-      oidcAttrMapId: this.state.oidcAttrMapId,
-      oidcAttrMapUserName: this.state.oidcAttrMapUserName,
-      oidcAttrMapName: this.state.oidcAttrMapName,
-      oidcAttrMapEmail: this.state.oidcAttrMapEmail,
-      isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser,
-      isSameEmailTreatedAsIdenticalUser: this.state.isSameEmailTreatedAsIdenticalUser,
-    };
+    let requestParams =
+      formData != null
+        ? {
+            oidcProviderName: formData.oidcProviderName,
+            oidcIssuerHost: formData.oidcIssuerHost,
+            oidcAuthorizationEndpoint: formData.oidcAuthorizationEndpoint,
+            oidcTokenEndpoint: formData.oidcTokenEndpoint,
+            oidcRevocationEndpoint: formData.oidcRevocationEndpoint,
+            oidcIntrospectionEndpoint: formData.oidcIntrospectionEndpoint,
+            oidcUserInfoEndpoint: formData.oidcUserInfoEndpoint,
+            oidcEndSessionEndpoint: formData.oidcEndSessionEndpoint,
+            oidcRegistrationEndpoint: formData.oidcRegistrationEndpoint,
+            oidcJWKSUri: formData.oidcJWKSUri,
+            oidcClientId: formData.oidcClientId,
+            oidcClientSecret: formData.oidcClientSecret,
+            oidcAttrMapId: formData.oidcAttrMapId,
+            oidcAttrMapUserName: formData.oidcAttrMapUserName,
+            oidcAttrMapName: formData.oidcAttrMapName,
+            oidcAttrMapEmail: formData.oidcAttrMapEmail,
+            isSameUsernameTreatedAsIdenticalUser:
+              formData.isSameUsernameTreatedAsIdenticalUser,
+            isSameEmailTreatedAsIdenticalUser:
+              formData.isSameEmailTreatedAsIdenticalUser,
+          }
+        : {
+            oidcProviderName: this.state.oidcProviderName,
+            oidcIssuerHost: this.state.oidcIssuerHost,
+            oidcAuthorizationEndpoint: this.state.oidcAuthorizationEndpoint,
+            oidcTokenEndpoint: this.state.oidcTokenEndpoint,
+            oidcRevocationEndpoint: this.state.oidcRevocationEndpoint,
+            oidcIntrospectionEndpoint: this.state.oidcIntrospectionEndpoint,
+            oidcUserInfoEndpoint: this.state.oidcUserInfoEndpoint,
+            oidcEndSessionEndpoint: this.state.oidcEndSessionEndpoint,
+            oidcRegistrationEndpoint: this.state.oidcRegistrationEndpoint,
+            oidcJWKSUri: this.state.oidcJWKSUri,
+            oidcClientId: this.state.oidcClientId,
+            oidcClientSecret: this.state.oidcClientSecret,
+            oidcAttrMapId: this.state.oidcAttrMapId,
+            oidcAttrMapUserName: this.state.oidcAttrMapUserName,
+            oidcAttrMapName: this.state.oidcAttrMapName,
+            oidcAttrMapEmail: this.state.oidcAttrMapEmail,
+            isSameUsernameTreatedAsIdenticalUser:
+              this.state.isSameUsernameTreatedAsIdenticalUser,
+            isSameEmailTreatedAsIdenticalUser:
+              this.state.isSameEmailTreatedAsIdenticalUser,
+          };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     const response = await apiv3Put('/security-setting/oidc', requestParams);
@@ -154,10 +166,12 @@ export default class AdminOidcSecurityContainer extends Container {
     this.setState({
       oidcProviderName: securitySettingParams.oidcProviderName,
       oidcIssuerHost: securitySettingParams.oidcIssuerHost,
-      oidcAuthorizationEndpoint: securitySettingParams.oidcAuthorizationEndpoint,
+      oidcAuthorizationEndpoint:
+        securitySettingParams.oidcAuthorizationEndpoint,
       oidcTokenEndpoint: securitySettingParams.oidcTokenEndpoint,
       oidcRevocationEndpoint: securitySettingParams.oidcRevocationEndpoint,
-      oidcIntrospectionEndpoint: securitySettingParams.oidcIntrospectionEndpoint,
+      oidcIntrospectionEndpoint:
+        securitySettingParams.oidcIntrospectionEndpoint,
       oidcUserInfoEndpoint: securitySettingParams.oidcUserInfoEndpoint,
       oidcEndSessionEndpoint: securitySettingParams.oidcEndSessionEndpoint,
       oidcRegistrationEndpoint: securitySettingParams.oidcRegistrationEndpoint,
@@ -168,10 +182,11 @@ export default class AdminOidcSecurityContainer extends Container {
       oidcAttrMapUserName: securitySettingParams.oidcAttrMapUserName,
       oidcAttrMapName: securitySettingParams.oidcAttrMapName,
       oidcAttrMapEmail: securitySettingParams.oidcAttrMapEmail,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
-      isSameEmailTreatedAsIdenticalUser: securitySettingParams.isSameEmailTreatedAsIdenticalUser,
+      isSameUsernameTreatedAsIdenticalUser:
+        securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser:
+        securitySettingParams.isSameEmailTreatedAsIdenticalUser,
     });
     return response;
   }
-
 }

+ 51 - 38
apps/app/src/client/services/AdminSamlSecurityContainer.js

@@ -13,7 +13,6 @@ const logger = loggerFactory('growi:security:AdminSamlSecurityContainer');
  * @extends {Container} unstated Container
  */
 export default class AdminSamlSecurityContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -49,7 +48,6 @@ export default class AdminSamlSecurityContainer extends Container {
       envAttrMapLastName: '',
       envABLCRule: '',
     };
-
   }
 
   /**
@@ -70,8 +68,10 @@ export default class AdminSamlSecurityContainer extends Container {
         samlAttrMapMail: samlAuth.samlAttrMapMail,
         samlAttrMapFirstName: samlAuth.samlAttrMapFirstName,
         samlAttrMapLastName: samlAuth.samlAttrMapLastName,
-        isSameUsernameTreatedAsIdenticalUser: samlAuth.isSameUsernameTreatedAsIdenticalUser,
-        isSameEmailTreatedAsIdenticalUser: samlAuth.isSameEmailTreatedAsIdenticalUser,
+        isSameUsernameTreatedAsIdenticalUser:
+          samlAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser:
+          samlAuth.isSameEmailTreatedAsIdenticalUser,
         samlABLCRule: samlAuth.samlABLCRule,
         envEntryPoint: samlAuth.samlEnvVarEntryPoint,
         envIssuer: samlAuth.samlEnvVarIssuer,
@@ -83,8 +83,7 @@ export default class AdminSamlSecurityContainer extends Container {
         envAttrMapLastName: samlAuth.samlEnvVarAttrMapLastName,
         envABLCRule: samlAuth.samlEnvVarABLCRule,
       });
-    }
-    catch (err) {
+    } catch (err) {
       this.setState({ retrieveError: err });
       logger.error(err);
       throw new Error('Failed to fetch data');
@@ -102,53 +101,66 @@ export default class AdminSamlSecurityContainer extends Container {
    * Switch isSameUsernameTreatedAsIdenticalUser
    */
   switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+    this.setState({
+      isSameUsernameTreatedAsIdenticalUser:
+        !this.state.isSameUsernameTreatedAsIdenticalUser,
+    });
   }
 
   /**
    * Switch isSameEmailTreatedAsIdenticalUser
    */
   switchIsSameEmailTreatedAsIdenticalUser() {
-    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
+    this.setState({
+      isSameEmailTreatedAsIdenticalUser:
+        !this.state.isSameEmailTreatedAsIdenticalUser,
+    });
   }
 
   /**
    * Update saml option
    */
   async updateSamlSetting(formData) {
-
-    let requestParams = formData != null ? {
-      entryPoint: formData.samlEntryPoint,
-      issuer: formData.samlIssuer,
-      cert: formData.samlCert,
-      attrMapId: formData.samlAttrMapId,
-      attrMapUsername: formData.samlAttrMapUsername,
-      attrMapMail: formData.samlAttrMapMail,
-      attrMapFirstName: formData.samlAttrMapFirstName,
-      attrMapLastName: formData.samlAttrMapLastName,
-      isSameUsernameTreatedAsIdenticalUser: formData.isSameUsernameTreatedAsIdenticalUser,
-      isSameEmailTreatedAsIdenticalUser: formData.isSameEmailTreatedAsIdenticalUser,
-      ABLCRule: formData.samlABLCRule,
-    } : {
-      entryPoint: this.state.samlEntryPoint,
-      issuer: this.state.samlIssuer,
-      cert: this.state.samlCert,
-      attrMapId: this.state.samlAttrMapId,
-      attrMapUsername: this.state.samlAttrMapUsername,
-      attrMapMail: this.state.samlAttrMapMail,
-      attrMapFirstName: this.state.samlAttrMapFirstName,
-      attrMapLastName: this.state.samlAttrMapLastName,
-      isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser,
-      isSameEmailTreatedAsIdenticalUser: this.state.isSameEmailTreatedAsIdenticalUser,
-      ABLCRule: this.state.samlABLCRule,
-    };
+    let requestParams =
+      formData != null
+        ? {
+            entryPoint: formData.samlEntryPoint,
+            issuer: formData.samlIssuer,
+            cert: formData.samlCert,
+            attrMapId: formData.samlAttrMapId,
+            attrMapUsername: formData.samlAttrMapUsername,
+            attrMapMail: formData.samlAttrMapMail,
+            attrMapFirstName: formData.samlAttrMapFirstName,
+            attrMapLastName: formData.samlAttrMapLastName,
+            isSameUsernameTreatedAsIdenticalUser:
+              formData.isSameUsernameTreatedAsIdenticalUser,
+            isSameEmailTreatedAsIdenticalUser:
+              formData.isSameEmailTreatedAsIdenticalUser,
+            ABLCRule: formData.samlABLCRule,
+          }
+        : {
+            entryPoint: this.state.samlEntryPoint,
+            issuer: this.state.samlIssuer,
+            cert: this.state.samlCert,
+            attrMapId: this.state.samlAttrMapId,
+            attrMapUsername: this.state.samlAttrMapUsername,
+            attrMapMail: this.state.samlAttrMapMail,
+            attrMapFirstName: this.state.samlAttrMapFirstName,
+            attrMapLastName: this.state.samlAttrMapLastName,
+            isSameUsernameTreatedAsIdenticalUser:
+              this.state.isSameUsernameTreatedAsIdenticalUser,
+            isSameEmailTreatedAsIdenticalUser:
+              this.state.isSameEmailTreatedAsIdenticalUser,
+            ABLCRule: this.state.samlABLCRule,
+          };
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     const response = await apiv3Put('/security-setting/saml', requestParams);
     const { securitySettingParams } = response.data;
 
     this.setState({
-      missingMandatoryConfigKeys: securitySettingParams.missingMandatoryConfigKeys,
+      missingMandatoryConfigKeys:
+        securitySettingParams.missingMandatoryConfigKeys,
       samlEntryPoint: securitySettingParams.samlEntryPoint,
       samlIssuer: securitySettingParams.samlIssuer,
       samlCert: securitySettingParams.samlCert,
@@ -157,11 +169,12 @@ export default class AdminSamlSecurityContainer extends Container {
       samlAttrMapMail: securitySettingParams.samlAttrMapMail,
       samlAttrMapFirstName: securitySettingParams.samlAttrMapFirstName,
       samlAttrMapLastName: securitySettingParams.samlAttrMapLastName,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
-      isSameEmailTreatedAsIdenticalUser: securitySettingParams.isSameEmailTreatedAsIdenticalUser,
+      isSameUsernameTreatedAsIdenticalUser:
+        securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser:
+        securitySettingParams.isSameEmailTreatedAsIdenticalUser,
       samlABLCRule: securitySettingParams.samlABLCRule,
     });
     return response;
   }
-
 }

+ 5 - 5
apps/app/src/client/services/AdminSlackIntegrationLegacyContainer.js

@@ -8,7 +8,6 @@ import { apiv3Get, apiv3Put } from '../util/apiv3-client';
  * @extends {Container} unstated Container
  */
 export default class AdminSlackIntegrationLegacyContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -26,7 +25,6 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
       isIncomingWebhookPrioritized: false,
       slackToken: '',
     };
-
   }
 
   /**
@@ -46,7 +44,8 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
     this.setState({
       isSlackbotConfigured: slackIntegrationParams.isSlackbotConfigured,
       webhookUrl: slackIntegrationParams.webhookUrl,
-      isIncomingWebhookPrioritized: slackIntegrationParams.isIncomingWebhookPrioritized,
+      isIncomingWebhookPrioritized:
+        slackIntegrationParams.isIncomingWebhookPrioritized,
       slackToken: slackIntegrationParams.slackToken,
     });
   }
@@ -69,7 +68,9 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
    * Switch incomingWebhookPrioritized
    */
   switchIsIncomingWebhookPrioritized() {
-    this.setState({ isIncomingWebhookPrioritized: !this.state.isIncomingWebhookPrioritized });
+    this.setState({
+      isIncomingWebhookPrioritized: !this.state.isIncomingWebhookPrioritized,
+    });
   }
 
   /**
@@ -92,5 +93,4 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
 
     return response;
   }
-
 }

+ 0 - 1
apps/app/src/client/services/AdminSocketIoContainer.js

@@ -1,2 +1 @@
-
 export default class AdminSocketIoContainer {}

+ 20 - 16
apps/app/src/client/services/AdminUsersContainer.js

@@ -3,16 +3,17 @@ import { debounce } from 'throttle-debounce';
 import { Container } from 'unstated';
 
 import {
-  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+  apiv3Delete,
+  apiv3Get,
+  apiv3Post,
+  apiv3Put,
 } from '../util/apiv3-client';
 
-
 /**
  * Service container for admin users page (Users.jsx)
  * @extends {Container} unstated Container
  */
 export default class AdminUsersContainer extends Container {
-
   constructor(appContainer) {
     super();
 
@@ -41,7 +42,9 @@ export default class AdminUsersContainer extends Container {
     this.hidePasswordResetModal = this.hidePasswordResetModal.bind(this);
     this.toggleUserInviteModal = this.toggleUserInviteModal.bind(this);
 
-    this.handleChangeSearchTextDebouce = debounce(3000, () => this.retrieveUsersByPagingNum(1));
+    this.handleChangeSearchTextDebouce = debounce(3000, () =>
+      this.retrieveUsersByPagingNum(1),
+    );
   }
 
   /**
@@ -62,12 +65,10 @@ export default class AdminUsersContainer extends Container {
     const all = 'all';
     if (this.isSelected(statusType)) {
       this.deleteStatusFromList(statusType);
-    }
-    else {
+    } else {
       if (statusType === all) {
         this.clearStatusList();
-      }
-      else {
+      } else {
         this.deleteStatusFromList(all);
       }
       this.addStatusToList(statusType);
@@ -132,7 +133,6 @@ export default class AdminUsersContainer extends Container {
    * @param {number} selectedPage
    */
   async retrieveUsersByPagingNum(selectedPage) {
-
     const params = {
       page: selectedPage,
       sort: this.state.sort,
@@ -145,10 +145,14 @@ export default class AdminUsersContainer extends Container {
     const { data } = await apiv3Get('/users', params);
 
     if (data.paginateResult == null) {
-      throw new Error('data must conclude \'paginateResult\' property.');
+      throw new Error("data must conclude 'paginateResult' property.");
     }
 
-    const { docs: users, totalDocs: totalUsers, limit: pagingLimit } = data.paginateResult;
+    const {
+      docs: users,
+      totalDocs: totalUsers,
+      limit: pagingLimit,
+    } = data.paginateResult;
 
     this.setState({
       users,
@@ -156,12 +160,11 @@ export default class AdminUsersContainer extends Container {
       pagingLimit,
       activePage: selectedPage,
     });
-
   }
 
   /**
- * retrieve user statistics
- */
+   * retrieve user statistics
+   */
   async retrieveUserStatistics() {
     const statsRes = await apiv3Get('/statistics/user');
     const userStatistics = statsRes.data.data;
@@ -211,7 +214,9 @@ export default class AdminUsersContainer extends Container {
    * @memberOf AdminUsersContainer
    */
   async toggleUserInviteModal() {
-    await this.setState({ isUserInviteModalShown: !this.state.isUserInviteModalShown });
+    await this.setState({
+      isUserInviteModalShown: !this.state.isUserInviteModalShown,
+    });
   }
 
   /**
@@ -304,5 +309,4 @@ export default class AdminUsersContainer extends Container {
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return removedUserData;
   }
-
 }

+ 7 - 2
apps/app/src/client/services/create-page/create-page.ts

@@ -1,7 +1,12 @@
 import { apiv3Post } from '~/client/util/apiv3-client';
-import type { IApiv3PageCreateParams, IApiv3PageCreateResponse } from '~/interfaces/apiv3';
+import type {
+  IApiv3PageCreateParams,
+  IApiv3PageCreateResponse,
+} from '~/interfaces/apiv3';
 
-export const createPage = async(params: IApiv3PageCreateParams): Promise<IApiv3PageCreateResponse> => {
+export const createPage = async (
+  params: IApiv3PageCreateParams,
+): Promise<IApiv3PageCreateResponse> => {
   const res = await apiv3Post<IApiv3PageCreateResponse>('/page', params);
   return res.data;
 };

+ 95 - 81
apps/app/src/client/services/create-page/use-create-page.tsx

@@ -1,13 +1,15 @@
 import { useCallback, useState } from 'react';
-
 import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 
-import { exist, getIsNonUserRelatedGroupsGranted } from '~/client/services/page-operation';
+import {
+  exist,
+  getIsNonUserRelatedGroupsGranted,
+} from '~/client/services/page-operation';
 import { toastWarning } from '~/client/util/toastr';
 import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
 import { useCurrentPagePath, useSetIsUntitledPage } from '~/states/page';
-import { useEditorMode, EditorMode } from '~/states/ui/editor';
+import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import { useGrantedGroupsInheritanceSelectModalActions } from '~/states/ui/modal/granted-groups-inheritance-select';
 
 import { createPage } from './create-page';
@@ -26,13 +28,13 @@ type OnAborted = () => void;
 type OnTerminated = () => void;
 
 export type CreatePageOpts = {
-  skipPageExistenceCheck?: boolean,
-  skipTransition?: boolean,
-  onCreationStart?: OnCreated,
-  onCreated?: OnCreated,
-  onAborted?: OnAborted,
-  onTerminated?: OnTerminated,
-}
+  skipPageExistenceCheck?: boolean;
+  skipTransition?: boolean;
+  onCreationStart?: OnCreated;
+  onCreated?: OnCreated;
+  onAborted?: OnAborted;
+  onTerminated?: OnTerminated;
+};
 
 type CreatePage = (
   params: IApiv3PageCreateParams,
@@ -40,101 +42,113 @@ type CreatePage = (
 ) => Promise<void>;
 
 type UseCreatePage = () => {
-  isCreating: boolean,
-  create: CreatePage,
+  isCreating: boolean;
+  create: CreatePage;
 };
 
 export const useCreatePage: UseCreatePage = () => {
-
   const router = useRouter();
   const { t } = useTranslation();
 
   const currentPagePath = useCurrentPagePath();
   const { setEditorMode } = useEditorMode();
   const setIsUntitledPage = useSetIsUntitledPage();
-  const { open: openGrantedGroupsInheritanceSelectModal, close: closeGrantedGroupsInheritanceSelectModal } = useGrantedGroupsInheritanceSelectModalActions();
+  const {
+    open: openGrantedGroupsInheritanceSelectModal,
+    close: closeGrantedGroupsInheritanceSelectModal,
+  } = useGrantedGroupsInheritanceSelectModalActions();
 
   const [isCreating, setCreating] = useState(false);
 
-  const create: CreatePage = useCallback(async (params, opts = {}) => {
-    const {
-      onCreationStart, onCreated, onAborted, onTerminated,
-    } = opts;
-    const skipPageExistenceCheck = opts.skipPageExistenceCheck ?? false;
-    const skipTransition = opts.skipTransition ?? false;
-
-    // check the page existence
-    if (!skipPageExistenceCheck && params.path != null) {
-      const pagePath = params.path;
-
-      try {
-        const { isExist } = await exist(pagePath);
-
-        if (isExist) {
-          if (!skipTransition) {
-            // routing
-            if (pagePath !== currentPagePath) {
-              await router.push(`${pagePath}#edit`);
+  const create: CreatePage = useCallback(
+    async (params, opts = {}) => {
+      const { onCreationStart, onCreated, onAborted, onTerminated } = opts;
+      const skipPageExistenceCheck = opts.skipPageExistenceCheck ?? false;
+      const skipTransition = opts.skipTransition ?? false;
+
+      // check the page existence
+      if (!skipPageExistenceCheck && params.path != null) {
+        const pagePath = params.path;
+
+        try {
+          const { isExist } = await exist(pagePath);
+
+          if (isExist) {
+            if (!skipTransition) {
+              // routing
+              if (pagePath !== currentPagePath) {
+                await router.push(`${pagePath}#edit`);
+              }
+              setEditorMode(EditorMode.Editor);
+            } else {
+              toastWarning(
+                t('duplicated_page_alert.same_page_name_exists', {
+                  pageName: pagePath,
+                }),
+              );
             }
-            setEditorMode(EditorMode.Editor);
-          }
-          else {
-            toastWarning(t('duplicated_page_alert.same_page_name_exists', { pageName: pagePath }));
+            onAborted?.();
+            return;
           }
-          onAborted?.();
-          return;
+        } catch (err) {
+          throw err;
+        } finally {
+          onTerminated?.();
         }
       }
-      catch (err) {
-        throw err;
-      }
-      finally {
-        onTerminated?.();
-      }
-    }
 
-    const _create = async (onlyInheritUserRelatedGrantedGroups?: boolean) => {
-      try {
-        setCreating(true);
-        onCreationStart?.();
+      const _create = async (onlyInheritUserRelatedGrantedGroups?: boolean) => {
+        try {
+          setCreating(true);
+          onCreationStart?.();
 
-        params.onlyInheritUserRelatedGrantedGroups = onlyInheritUserRelatedGrantedGroups;
-        const response = await createPage(params);
+          params.onlyInheritUserRelatedGrantedGroups =
+            onlyInheritUserRelatedGrantedGroups;
+          const response = await createPage(params);
 
-        closeGrantedGroupsInheritanceSelectModal();
+          closeGrantedGroupsInheritanceSelectModal();
 
-        if (!skipTransition) {
-          await router.push(`/${response.page._id}#edit`);
-          setEditorMode(EditorMode.Editor);
-        }
+          if (!skipTransition) {
+            await router.push(`/${response.page._id}#edit`);
+            setEditorMode(EditorMode.Editor);
+          }
 
-        if (params.path == null) {
-          setIsUntitledPage(true);
-        }
+          if (params.path == null) {
+            setIsUntitledPage(true);
+          }
 
-        onCreated?.();
-      }
-      catch (err) {
-        throw err;
-      }
-      finally {
-        onTerminated?.();
-        setCreating(false);
-      }
-    };
-
-    // If parent page is granted to non-user-related groups, let the user select whether or not to inherit them.
-    if (params.parentPath != null) {
-      const { isNonUserRelatedGroupsGranted } = await getIsNonUserRelatedGroupsGranted(params.parentPath);
-      if (isNonUserRelatedGroupsGranted) {
-        // create and transit request will be made from modal
-        openGrantedGroupsInheritanceSelectModal(_create);
-        return;
+          onCreated?.();
+        } catch (err) {
+          throw err;
+        } finally {
+          onTerminated?.();
+          setCreating(false);
+        }
+      };
+
+      // If parent page is granted to non-user-related groups, let the user select whether or not to inherit them.
+      if (params.parentPath != null) {
+        const { isNonUserRelatedGroupsGranted } =
+          await getIsNonUserRelatedGroupsGranted(params.parentPath);
+        if (isNonUserRelatedGroupsGranted) {
+          // create and transit request will be made from modal
+          openGrantedGroupsInheritanceSelectModal(_create);
+          return;
+        }
       }
-    }
 
-    await _create();
-  }, [currentPagePath, setEditorMode, router, t, closeGrantedGroupsInheritanceSelectModal, setIsUntitledPage, openGrantedGroupsInheritanceSelectModal]);
+      await _create();
+    },
+    [
+      currentPagePath,
+      setEditorMode,
+      router,
+      t,
+      closeGrantedGroupsInheritanceSelectModal,
+      setIsUntitledPage,
+      openGrantedGroupsInheritanceSelectModal,
+    ],
+  );
 
   return {
     isCreating,

+ 21 - 18
apps/app/src/client/services/create-page/use-create-template-page.ts

@@ -1,5 +1,4 @@
 import { useCallback } from 'react';
-
 import { Origin } from '@growi/core';
 import { isCreatablePage } from '@growi/core/dist/utils/page-path-utils';
 import { normalizePath } from '@growi/core/dist/utils/path-utils';
@@ -7,31 +6,35 @@ import { normalizePath } from '@growi/core/dist/utils/path-utils';
 import type { LabelType } from '~/interfaces/template';
 import { useCurrentPagePath } from '~/states/page';
 
-
 import { useCreatePage } from './use-create-page';
 
 type UseCreateTemplatePage = () => {
-  isCreatable: boolean,
-  isCreating: boolean,
-  createTemplate?: (label: LabelType) => Promise<void>,
-}
+  isCreatable: boolean;
+  isCreating: boolean;
+  createTemplate?: (label: LabelType) => Promise<void>;
+};
 
 export const useCreateTemplatePage: UseCreateTemplatePage = () => {
-
   const currentPagePath = useCurrentPagePath();
 
   const { isCreating, create } = useCreatePage();
-  const isCreatable = currentPagePath != null && isCreatablePage(normalizePath(`${currentPagePath}/_template`));
-
-  const createTemplate = useCallback(async(label: LabelType) => {
-    if (currentPagePath == null || !isCreatable) return;
-
-    return create(
-      {
-        path: normalizePath(`${currentPagePath}/${label}`), parentPath: currentPagePath, wip: false, origin: Origin.View,
-      },
-    );
-  }, [currentPagePath, isCreatable, create]);
+  const isCreatable =
+    currentPagePath != null &&
+    isCreatablePage(normalizePath(`${currentPagePath}/_template`));
+
+  const createTemplate = useCallback(
+    async (label: LabelType) => {
+      if (currentPagePath == null || !isCreatable) return;
+
+      return create({
+        path: normalizePath(`${currentPagePath}/${label}`),
+        parentPath: currentPagePath,
+        wip: false,
+        origin: Origin.View,
+      });
+    },
+    [currentPagePath, isCreatable, create],
+  );
 
   return {
     isCreatable,

+ 8 - 3
apps/app/src/client/services/g2g-transfer.ts

@@ -2,11 +2,16 @@ import { useCallback, useState } from 'react';
 
 import { apiv3Post } from '~/client/util/apiv3-client';
 
-export const useGenerateTransferKey = (): {transferKey: string, generateTransferKey: () => Promise<void>} => {
+export const useGenerateTransferKey = (): {
+  transferKey: string;
+  generateTransferKey: () => Promise<void>;
+} => {
   const [transferKey, setTransferKey] = useState('');
 
-  const generateTransferKey = useCallback(async() => {
-    const response = await apiv3Post('/g2g-transfer/generate-key', { appSiteUrl: window.location.origin });
+  const generateTransferKey = useCallback(async () => {
+    const response = await apiv3Post('/g2g-transfer/generate-key', {
+      appSiteUrl: window.location.origin,
+    });
     const { transferKey } = response.data;
     setTransferKey(transferKey);
   }, []);

+ 0 - 1
apps/app/src/client/services/maintenance-mode.ts

@@ -1,5 +1,4 @@
 import { useCallback } from 'react';
-
 import { useSetAtom } from 'jotai';
 
 import { _atomsForMaintenanceMode } from '../../states/global';

+ 91 - 49
apps/app/src/client/services/page-operation.ts

@@ -1,5 +1,4 @@
 import { useCallback } from 'react';
-
 import type { IPageHasId } from '@growi/core';
 import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
@@ -17,67 +16,81 @@ import { toastError } from '../util/toastr';
 
 const logger = loggerFactory('growi:services:page-operation');
 
-
-export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
+export const toggleSubscribe = async (
+  pageId: string,
+  currentStatus: SubscriptionStatusType | undefined,
+): Promise<void> => {
   try {
-    const newStatus = currentStatus === SubscriptionStatusType.SUBSCRIBE
-      ? SubscriptionStatusType.UNSUBSCRIBE
-      : SubscriptionStatusType.SUBSCRIBE;
+    const newStatus =
+      currentStatus === SubscriptionStatusType.SUBSCRIBE
+        ? SubscriptionStatusType.UNSUBSCRIBE
+        : SubscriptionStatusType.SUBSCRIBE;
 
     await apiv3Put('/page/subscribe', { pageId, status: newStatus });
-  }
-  catch (err) {
+  } catch (err) {
     toastError(err);
   }
 };
 
-export const toggleLike = async(pageId: string, currentValue?: boolean): Promise<void> => {
+export const toggleLike = async (
+  pageId: string,
+  currentValue?: boolean,
+): Promise<void> => {
   try {
     await apiv3Put('/page/likes', { pageId, bool: !currentValue });
-  }
-  catch (err) {
+  } catch (err) {
     toastError(err);
   }
 };
 
-export const toggleBookmark = async(pageId: string, currentValue?: boolean): Promise<void> => {
+export const toggleBookmark = async (
+  pageId: string,
+  currentValue?: boolean,
+): Promise<void> => {
   try {
     await apiv3Put('/bookmarks', { pageId, bool: !currentValue });
-  }
-  catch (err) {
+  } catch (err) {
     toastError(err);
   }
 };
 
-export const updateContentWidth = async(pageId: string, newValue: boolean): Promise<void> => {
+export const updateContentWidth = async (
+  pageId: string,
+  newValue: boolean,
+): Promise<void> => {
   try {
-    await apiv3Put(`/page/${pageId}/content-width`, { expandContentWidth: newValue });
-  }
-  catch (err) {
+    await apiv3Put(`/page/${pageId}/content-width`, {
+      expandContentWidth: newValue,
+    });
+  } catch (err) {
     toastError(err);
   }
 };
 
-export const bookmark = async(pageId: string): Promise<void> => {
+export const bookmark = async (pageId: string): Promise<void> => {
   try {
     await apiv3Put('/bookmarks', { pageId, bool: true });
-  }
-  catch (err) {
+  } catch (err) {
     toastError(err);
   }
 };
 
-export const unbookmark = async(pageId: string): Promise<void> => {
+export const unbookmark = async (pageId: string): Promise<void> => {
   try {
     await apiv3Put('/bookmarks', { pageId, bool: false });
-  }
-  catch (err) {
+  } catch (err) {
     toastError(err);
   }
 };
 
-export const exportAsMarkdown = (pageId: string, revisionId: string, format: string): void => {
-  const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
+export const exportAsMarkdown = (
+  pageId: string,
+  revisionId: string,
+  format: string,
+): void => {
+  const url = new URL(
+    urljoin(window.location.origin, '_api/v3/page/export', pageId),
+  );
   url.searchParams.append('format', format);
   url.searchParams.append('revisionId', revisionId);
   window.location.href = url.href;
@@ -86,34 +99,46 @@ export const exportAsMarkdown = (pageId: string, revisionId: string, format: str
 /**
  * send request to fix broken paths caused by unexpected events such as server shutdown while renaming page paths
  */
-export const resumeRenameOperation = async(pageId: string): Promise<void> => {
+export const resumeRenameOperation = async (pageId: string): Promise<void> => {
   await apiv3Post('/pages/resume-rename', { pageId });
 };
 
 export type UpdateStateAfterSaveOption = {
-  supressEditingMarkdownMutation: boolean,
-}
+  supressEditingMarkdownMutation: boolean;
+};
 
-export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: UpdateStateAfterSaveOption): (() => Promise<void>) | undefined => {
+export const useUpdateStateAfterSave = (
+  pageId: string | undefined | null,
+  opts?: UpdateStateAfterSaveOption,
+): (() => Promise<void>) | undefined => {
   const isGuestUser = useIsGuestUser();
   const { fetchCurrentPage } = useFetchCurrentPage();
   const setRemoteLatestPageData = useSetRemoteLatestPageData();
 
   const setEditingMarkdown = useSetEditingMarkdown();
-  const { mutate: mutateCurrentGrantData } = useSWRxCurrentGrantData(isGuestUser ? null : pageId);
-  const { mutate: mutateApplicableGrant } = useSWRxApplicableGrant(isGuestUser ? null : pageId);
+  const { mutate: mutateCurrentGrantData } = useSWRxCurrentGrantData(
+    isGuestUser ? null : pageId,
+  );
+  const { mutate: mutateApplicableGrant } = useSWRxApplicableGrant(
+    isGuestUser ? null : pageId,
+  );
 
   // update swr 'currentPageId', 'currentPage', remote states
-  return useCallback(async() => {
-    if (pageId == null) { return }
+  return useCallback(async () => {
+    if (pageId == null) {
+      return;
+    }
 
     const updatedPage = await fetchCurrentPage({ pageId, force: true });
 
-    if (updatedPage == null || updatedPage.revision == null) { return }
+    if (updatedPage == null || updatedPage.revision == null) {
+      return;
+    }
 
     // supress to mutate only when updated from built-in editor
     // and see: https://github.com/growilabs/growi/pull/7118
-    const supressEditingMarkdownMutation = opts?.supressEditingMarkdownMutation ?? false;
+    const supressEditingMarkdownMutation =
+      opts?.supressEditingMarkdownMutation ?? false;
     if (!supressEditingMarkdownMutation) {
       setEditingMarkdown(updatedPage.revision.body);
     }
@@ -129,44 +154,61 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
     };
 
     setRemoteLatestPageData(remoterevisionData);
-  },
-  [pageId, fetchCurrentPage, opts?.supressEditingMarkdownMutation, mutateCurrentGrantData, mutateApplicableGrant, setRemoteLatestPageData, setEditingMarkdown]);
+  }, [
+    pageId,
+    fetchCurrentPage,
+    opts?.supressEditingMarkdownMutation,
+    mutateCurrentGrantData,
+    mutateApplicableGrant,
+    setRemoteLatestPageData,
+    setEditingMarkdown,
+  ]);
 };
 
-export const unlink = async(path: string): Promise<void> => {
+export const unlink = async (path: string): Promise<void> => {
   await apiPost('/pages.unlink', { path });
 };
 
-
 interface PageExistResponse {
-  isExist: boolean,
+  isExist: boolean;
 }
 
-export const exist = async(path: string): Promise<PageExistResponse> => {
+export const exist = async (path: string): Promise<PageExistResponse> => {
   const res = await apiv3Get<PageExistResponse>('/page/exist', { path });
   return res.data;
 };
 
 interface NonUserRelatedGroupsGrantedResponse {
-  isNonUserRelatedGroupsGranted: boolean,
+  isNonUserRelatedGroupsGranted: boolean;
 }
 
-export const getIsNonUserRelatedGroupsGranted = async(path: string): Promise<NonUserRelatedGroupsGrantedResponse> => {
-  const res = await apiv3Get<NonUserRelatedGroupsGrantedResponse>('/page/non-user-related-groups-granted', { path });
+export const getIsNonUserRelatedGroupsGranted = async (
+  path: string,
+): Promise<NonUserRelatedGroupsGrantedResponse> => {
+  const res = await apiv3Get<NonUserRelatedGroupsGrantedResponse>(
+    '/page/non-user-related-groups-granted',
+    { path },
+  );
   return res.data;
 };
 
-export const publish = async(pageId: string): Promise<IPageHasId> => {
+export const publish = async (pageId: string): Promise<IPageHasId> => {
   const res = await apiv3Put(`/page/${pageId}/publish`);
   return res.data;
 };
 
-export const unpublish = async(pageId: string): Promise<IPageHasId> => {
+export const unpublish = async (pageId: string): Promise<IPageHasId> => {
   const res = await apiv3Put(`/page/${pageId}/unpublish`);
   return res.data;
 };
 
-export const syncLatestRevisionBody = async(pageId: string, editingMarkdownLength?: number): Promise<SyncLatestRevisionBody> => {
-  const res = await apiv3Put(`/page/${pageId}/sync-latest-revision-body-to-yjs-draft`, { editingMarkdownLength });
+export const syncLatestRevisionBody = async (
+  pageId: string,
+  editingMarkdownLength?: number,
+): Promise<SyncLatestRevisionBody> => {
+  const res = await apiv3Put(
+    `/page/${pageId}/sync-latest-revision-body-to-yjs-draft`,
+    { editingMarkdownLength },
+  );
   return res.data;
 };

+ 111 - 83
apps/app/src/client/services/renderer/renderer.tsx

@@ -1,10 +1,9 @@
-import assert from 'assert';
-
 import { isClient } from '@growi/core/dist/utils/browser-utils';
 import * as presentation from '@growi/presentation/dist/client/services/sanitize-option';
 import * as refsGrowiDirective from '@growi/remark-attachment-refs/dist/client';
 import * as drawio from '@growi/remark-drawio';
 import * as lsxGrowiDirective from '@growi/remark-lsx/dist/client';
+import assert from 'assert';
 import katex from 'rehype-katex';
 import sanitize from 'rehype-sanitize';
 import slug from 'rehype-slug';
@@ -24,7 +23,7 @@ import * as callout from '~/features/callout';
 import * as mermaid from '~/features/mermaid';
 import * as plantuml from '~/features/plantuml';
 import type { RendererOptions } from '~/interfaces/renderer-options';
-import { type RendererConfigExt } from '~/interfaces/services/renderer';
+import type { RendererConfigExt } from '~/interfaces/services/renderer';
 import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
 import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
@@ -32,29 +31,26 @@ import * as attachment from '~/services/renderer/remark-plugins/attachment';
 import * as codeBlock from '~/services/renderer/remark-plugins/codeblock';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
-  getCommonSanitizeOption, generateCommonOptions, verifySanitizePlugin,
+  generateCommonOptions,
+  getCommonSanitizeOption,
+  verifySanitizePlugin,
 } from '~/services/renderer/renderer';
 import loggerFactory from '~/utils/logger';
 
 // import EasyGrid from './PreProcessor/EasyGrid';
 
-
 import '@growi/remark-lsx/dist/client/style.css';
 import '@growi/remark-attachment-refs/dist/client/style.css';
 
-
 const logger = loggerFactory('growi:cli:services:renderer');
 
-
 assert(isClient(), 'This module must be loaded only from client modules.');
 
-
 export const generateViewOptions = (
-    pagePath: string,
-    config: RendererConfigExt,
-    storeTocNode: (toc: HtmlElementNode) => void,
+  pagePath: string,
+  config: RendererConfigExt,
+  storeTocNode: (toc: HtmlElementNode) => void,
 ): RendererOptions => {
-
   const options = generateCommonOptions(pagePath);
 
   const { remarkPlugins, rehypePlugins, components } = options;
@@ -62,7 +58,10 @@ export const generateViewOptions = (
   // add remark plugins
   remarkPlugins.push(
     math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode }],
+    [
+      plantuml.remarkPlugin,
+      { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode },
+    ],
     [drawio.remarkPlugin, { isDarkMode: config.isDarkMode }],
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
@@ -76,24 +75,31 @@ export const generateViewOptions = (
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      getCommonSanitizeOption(config),
-      presentation.sanitizeOption,
-      drawio.sanitizeOption,
-      mermaid.sanitizeOption,
-      callout.sanitizeOption,
-      attachment.sanitizeOption,
-      lsxGrowiDirective.sanitizeOption,
-      refsGrowiDirective.sanitizeOption,
-      codeBlock.sanitizeOption,
-    )]
-    : () => {};
+  const rehypeSanitizePlugin: Pluggable | (() => void) =
+    config.isEnabledXssPrevention
+      ? [
+          sanitize,
+          deepmerge(
+            getCommonSanitizeOption(config),
+            presentation.sanitizeOption,
+            drawio.sanitizeOption,
+            mermaid.sanitizeOption,
+            callout.sanitizeOption,
+            attachment.sanitizeOption,
+            lsxGrowiDirective.sanitizeOption,
+            refsGrowiDirective.sanitizeOption,
+            codeBlock.sanitizeOption,
+          ),
+        ]
+      : () => {};
 
   // add rehype plugins
   rehypePlugins.push(
     slug,
-    [lsxGrowiDirective.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
+    [
+      lsxGrowiDirective.rehypePlugin,
+      { pagePath, isSharedPage: config.isSharedPage },
+    ],
     [refsGrowiDirective.rehypePlugin, { pagePath }],
     rehypeSanitizePlugin,
     katex,
@@ -128,8 +134,10 @@ export const generateViewOptions = (
   return options;
 };
 
-export const generateTocOptions = (config: RendererConfigExt, tocNode: HtmlElementNode | undefined): RendererOptions => {
-
+export const generateTocOptions = (
+  config: RendererConfigExt,
+  tocNode: HtmlElementNode | undefined,
+): RendererOptions => {
   const options = generateCommonOptions(undefined);
 
   const { rehypePlugins } = options;
@@ -137,12 +145,13 @@ export const generateTocOptions = (config: RendererConfigExt, tocNode: HtmlEleme
   // add remark plugins
   // remarkPlugins.push();
 
-  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      getCommonSanitizeOption(config),
-      codeBlock.sanitizeOption,
-    )]
-    : () => {};
+  const rehypeSanitizePlugin: Pluggable | (() => void) =
+    config.isEnabledXssPrevention
+      ? [
+          sanitize,
+          deepmerge(getCommonSanitizeOption(config), codeBlock.sanitizeOption),
+        ]
+      : () => {};
 
   // add rehype plugins
   rehypePlugins.push(
@@ -158,10 +167,10 @@ export const generateTocOptions = (config: RendererConfigExt, tocNode: HtmlEleme
 };
 
 export const generateSimpleViewOptions = (
-    config: RendererConfigExt,
-    pagePath: string,
-    highlightKeywords?: string | string[],
-    overrideIsEnabledLinebreaks?: boolean,
+  config: RendererConfigExt,
+  pagePath: string,
+  highlightKeywords?: string | string[],
+  overrideIsEnabledLinebreaks?: boolean,
 ): RendererOptions => {
   const options = generateCommonOptions(pagePath);
 
@@ -170,7 +179,10 @@ export const generateSimpleViewOptions = (
   // add remark plugins
   remarkPlugins.push(
     math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode }],
+    [
+      plantuml.remarkPlugin,
+      { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode },
+    ],
     [drawio.remarkPlugin, { isDarkMode: config.isDarkMode }],
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
@@ -181,29 +193,37 @@ export const generateSimpleViewOptions = (
     refsGrowiDirective.remarkPlugin,
   );
 
-  const isEnabledLinebreaks = overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
+  const isEnabledLinebreaks =
+    overrideIsEnabledLinebreaks ?? config.isEnabledLinebreaks;
 
   if (isEnabledLinebreaks) {
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      getCommonSanitizeOption(config),
-      presentation.sanitizeOption,
-      drawio.sanitizeOption,
-      mermaid.sanitizeOption,
-      callout.sanitizeOption,
-      attachment.sanitizeOption,
-      lsxGrowiDirective.sanitizeOption,
-      refsGrowiDirective.sanitizeOption,
-      codeBlock.sanitizeOption,
-    )]
-    : () => {};
+  const rehypeSanitizePlugin: Pluggable | (() => void) =
+    config.isEnabledXssPrevention
+      ? [
+          sanitize,
+          deepmerge(
+            getCommonSanitizeOption(config),
+            presentation.sanitizeOption,
+            drawio.sanitizeOption,
+            mermaid.sanitizeOption,
+            callout.sanitizeOption,
+            attachment.sanitizeOption,
+            lsxGrowiDirective.sanitizeOption,
+            refsGrowiDirective.sanitizeOption,
+            codeBlock.sanitizeOption,
+          ),
+        ]
+      : () => {};
 
   // add rehype plugins
   rehypePlugins.push(
-    [lsxGrowiDirective.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
+    [
+      lsxGrowiDirective.rehypePlugin,
+      { pagePath, isSharedPage: config.isSharedPage },
+    ],
     [refsGrowiDirective.rehypePlugin, { pagePath }],
     [keywordHighlighter.rehypePlugin, { keywords: highlightKeywords }],
     rehypeSanitizePlugin,
@@ -232,26 +252,21 @@ export const generateSimpleViewOptions = (
 };
 
 export const generatePresentationViewOptions = (
-    config: RendererConfigExt,
-    pagePath: string,
+  config: RendererConfigExt,
+  pagePath: string,
 ): RendererOptions => {
   // based on simple view options
   const options = generateSimpleViewOptions(config, pagePath);
 
   const { rehypePlugins } = options;
 
-
-  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      addLineNumberAttribute.sanitizeOption,
-    )]
-    : () => {};
+  const rehypeSanitizePlugin: Pluggable | (() => void) =
+    config.isEnabledXssPrevention
+      ? [sanitize, deepmerge(addLineNumberAttribute.sanitizeOption)]
+      : () => {};
 
   // add rehype plugins
-  rehypePlugins.push(
-    addLineNumberAttribute.rehypePlugin,
-    rehypeSanitizePlugin,
-  );
+  rehypePlugins.push(addLineNumberAttribute.rehypePlugin, rehypeSanitizePlugin);
 
   if (config.isEnabledXssPrevention) {
     verifySanitizePlugin(options, false);
@@ -259,7 +274,10 @@ export const generatePresentationViewOptions = (
   return options;
 };
 
-export const generatePreviewOptions = (config: RendererConfigExt, pagePath: string): RendererOptions => {
+export const generatePreviewOptions = (
+  config: RendererConfigExt,
+  pagePath: string,
+): RendererOptions => {
   const options = generateCommonOptions(pagePath);
 
   const { remarkPlugins, rehypePlugins, components } = options;
@@ -267,7 +285,10 @@ export const generatePreviewOptions = (config: RendererConfigExt, pagePath: stri
   // add remark plugins
   remarkPlugins.push(
     math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode }],
+    [
+      plantuml.remarkPlugin,
+      { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode },
+    ],
     [drawio.remarkPlugin, { isDarkMode: config.isDarkMode }],
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
@@ -281,23 +302,30 @@ export const generatePreviewOptions = (config: RendererConfigExt, pagePath: stri
     remarkPlugins.push(breaks);
   }
 
-  const rehypeSanitizePlugin: Pluggable | (() => void) = config.isEnabledXssPrevention
-    ? [sanitize, deepmerge(
-      getCommonSanitizeOption(config),
-      drawio.sanitizeOption,
-      mermaid.sanitizeOption,
-      callout.sanitizeOption,
-      attachment.sanitizeOption,
-      lsxGrowiDirective.sanitizeOption,
-      refsGrowiDirective.sanitizeOption,
-      addLineNumberAttribute.sanitizeOption,
-      codeBlock.sanitizeOption,
-    )]
-    : () => {};
+  const rehypeSanitizePlugin: Pluggable | (() => void) =
+    config.isEnabledXssPrevention
+      ? [
+          sanitize,
+          deepmerge(
+            getCommonSanitizeOption(config),
+            drawio.sanitizeOption,
+            mermaid.sanitizeOption,
+            callout.sanitizeOption,
+            attachment.sanitizeOption,
+            lsxGrowiDirective.sanitizeOption,
+            refsGrowiDirective.sanitizeOption,
+            addLineNumberAttribute.sanitizeOption,
+            codeBlock.sanitizeOption,
+          ),
+        ]
+      : () => {};
 
   // add rehype plugins
   rehypePlugins.push(
-    [lsxGrowiDirective.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
+    [
+      lsxGrowiDirective.rehypePlugin,
+      { pagePath, isSharedPage: config.isSharedPage },
+    ],
     [refsGrowiDirective.rehypePlugin, { pagePath }],
     addLineNumberAttribute.rehypePlugin,
     rehypeSanitizePlugin,

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

@@ -1,98 +1,141 @@
 import { useCallback, useEffect } from 'react';
-
 import { Origin } from '@growi/core';
 import { globalEventTarget } from '@growi/core/dist/utils';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 import { replaceDrawioInMarkdown } from '~/client/components/Page/markdown-drawio-util-for-view';
-import { extractRemoteRevisionDataFromErrorObj, useUpdatePage } from '~/client/services/update-page';
-import { useCurrentPageData, useSetRemoteLatestPageData } from '~/states/page';
+import {
+  extractRemoteRevisionDataFromErrorObj,
+  useUpdatePage,
+} from '~/client/services/update-page';
 import type { RemoteRevisionData } from '~/states/page';
+import { useCurrentPageData, useSetRemoteLatestPageData } from '~/states/page';
 import { useShareLinkId } from '~/states/page/hooks';
 import { useConflictDiffModalActions } from '~/states/ui/modal/conflict-diff';
 import { useDrawioModalActions } from '~/states/ui/modal/drawio';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:cli:side-effects:useDrawioModalLauncherForView');
-
+const logger = loggerFactory(
+  'growi:cli:side-effects:useDrawioModalLauncherForView',
+);
 
 export const useDrawioModalLauncherForView = (opts?: {
-  onSaveSuccess?: () => void,
-  onSaveError?: (error: any) => void,
+  onSaveSuccess?: () => void;
+  onSaveError?: (error: any) => void;
 }): void => {
-
   const shareLinkId = useShareLinkId();
 
   const currentPage = useCurrentPageData();
 
   const { open: openDrawioModal } = useDrawioModalActions();
 
-  const { open: openConflictDiffModal, close: closeConflictDiffModal } = useConflictDiffModalActions();
+  const { open: openConflictDiffModal, close: closeConflictDiffModal } =
+    useConflictDiffModalActions();
 
   const _updatePage = useUpdatePage();
 
   const setRemoteLatestPageData = useSetRemoteLatestPageData();
 
   // eslint-disable-next-line max-len
-  const updatePage = useCallback(async(revisionId:string, newMarkdown: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {
-    if (currentPage == null || currentPage.revision == null || shareLinkId != null) {
-      return;
-    }
-
-    // There are cases where "revisionId" is not required for revision updates
-    // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
-    try {
-      await _updatePage({
-        pageId: currentPage._id,
-        revisionId,
-        body: newMarkdown,
-        origin: Origin.View,
-      });
-
-      closeConflictDiffModal();
-      opts?.onSaveSuccess?.();
-    }
-    catch (error) {
-      const remoteRevidsionData = extractRemoteRevisionDataFromErrorObj(error);
-      if (remoteRevidsionData != null) {
-        onConflict(remoteRevidsionData, newMarkdown);
+  const updatePage = useCallback(
+    async (
+      revisionId: string,
+      newMarkdown: string,
+      onConflict: (
+        conflictData: RemoteRevisionData,
+        newMarkdown: string,
+      ) => void,
+    ) => {
+      if (
+        currentPage == null ||
+        currentPage.revision == null ||
+        shareLinkId != null
+      ) {
+        return;
       }
 
-      logger.error('failed to save', error);
-      opts?.onSaveError?.(error);
-    }
-  }, [_updatePage, closeConflictDiffModal, currentPage, opts, shareLinkId]);
+      // There are cases where "revisionId" is not required for revision updates
+      // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
+      try {
+        await _updatePage({
+          pageId: currentPage._id,
+          revisionId,
+          body: newMarkdown,
+          origin: Origin.View,
+        });
+
+        closeConflictDiffModal();
+        opts?.onSaveSuccess?.();
+      } catch (error) {
+        const remoteRevidsionData =
+          extractRemoteRevisionDataFromErrorObj(error);
+        if (remoteRevidsionData != null) {
+          onConflict(remoteRevidsionData, newMarkdown);
+        }
+
+        logger.error('failed to save', error);
+        opts?.onSaveError?.(error);
+      }
+    },
+    [_updatePage, closeConflictDiffModal, currentPage, opts, shareLinkId],
+  );
 
   // eslint-disable-next-line max-len
-  const generateResolveConflictHandler = useCallback((revisionId: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {
-    return async(newMarkdown: string) => {
-      await updatePage(revisionId, newMarkdown, onConflict);
-    };
-  }, [updatePage]);
-
-  const onConflictHandler = useCallback((remoteRevidsionData: RemoteRevisionData, newMarkdown: string) => {
-    setRemoteLatestPageData(remoteRevidsionData);
-
-    const resolveConflictHandler = generateResolveConflictHandler(remoteRevidsionData.remoteRevisionId, onConflictHandler);
-    if (resolveConflictHandler == null) {
-      return;
-    }
-
-    openConflictDiffModal(newMarkdown, resolveConflictHandler);
-  }, [generateResolveConflictHandler, openConflictDiffModal, setRemoteLatestPageData]);
+  const generateResolveConflictHandler = useCallback(
+    (
+      revisionId: string,
+      onConflict: (
+        conflictData: RemoteRevisionData,
+        newMarkdown: string,
+      ) => void,
+    ) => {
+      return async (newMarkdown: string) => {
+        await updatePage(revisionId, newMarkdown, onConflict);
+      };
+    },
+    [updatePage],
+  );
+
+  const onConflictHandler = useCallback(
+    (remoteRevidsionData: RemoteRevisionData, newMarkdown: string) => {
+      setRemoteLatestPageData(remoteRevidsionData);
+
+      const resolveConflictHandler = generateResolveConflictHandler(
+        remoteRevidsionData.remoteRevisionId,
+        onConflictHandler,
+      );
+      if (resolveConflictHandler == null) {
+        return;
+      }
 
-  const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
-    if (currentPage == null || currentPage.revision == null) {
-      return;
-    }
+      openConflictDiffModal(newMarkdown, resolveConflictHandler);
+    },
+    [
+      generateResolveConflictHandler,
+      openConflictDiffModal,
+      setRemoteLatestPageData,
+    ],
+  );
+
+  const saveByDrawioModal = useCallback(
+    async (drawioMxFile: string, bol: number, eol: number) => {
+      if (currentPage == null || currentPage.revision == null) {
+        return;
+      }
 
-    const currentRevisionId = currentPage.revision._id;
-    const currentMarkdown = currentPage.revision.body;
-    const newMarkdown = replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
+      const currentRevisionId = currentPage.revision._id;
+      const currentMarkdown = currentPage.revision.body;
+      const newMarkdown = replaceDrawioInMarkdown(
+        drawioMxFile,
+        currentMarkdown,
+        bol,
+        eol,
+      );
 
-    await updatePage(currentRevisionId, newMarkdown, onConflictHandler);
-  }, [currentPage, onConflictHandler, updatePage]);
+      await updatePage(currentRevisionId, newMarkdown, onConflictHandler);
+    },
+    [currentPage, onConflictHandler, updatePage],
+  );
 
   // set handler to open DrawioModal
   useEffect(() => {
@@ -103,7 +146,9 @@ export const useDrawioModalLauncherForView = (opts?: {
 
     const handler = (evt: CustomEvent<DrawioEditByViewerProps>) => {
       const data = evt.detail;
-      openDrawioModal(data.drawioMxFile, drawioMxFile => saveByDrawioModal(drawioMxFile, data.bol, data.eol));
+      openDrawioModal(data.drawioMxFile, (drawioMxFile) =>
+        saveByDrawioModal(drawioMxFile, data.bol, data.eol),
+      );
     };
     globalEventTarget.addEventListener('launchDrawioModal', handler);
 

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

@@ -1,99 +1,145 @@
 import { useCallback, useEffect } from 'react';
-
 import { Origin } from '@growi/core';
 import { globalEventTarget } from '@growi/core/dist/utils';
 import type { MarkdownTable } from '@growi/editor';
 
-import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/client/components/Page/markdown-table-util-for-view';
+import {
+  getMarkdownTableFromLine,
+  replaceMarkdownTableInMarkdown,
+} from '~/client/components/Page/markdown-table-util-for-view';
 import type { LaunchHandsonTableModalEventDetail } from '~/client/interfaces/handsontable-modal';
-import { extractRemoteRevisionDataFromErrorObj, useUpdatePage } from '~/client/services/update-page';
-import { useCurrentPageData, useSetRemoteLatestPageData } from '~/states/page';
+import {
+  extractRemoteRevisionDataFromErrorObj,
+  useUpdatePage,
+} from '~/client/services/update-page';
 import type { RemoteRevisionData } from '~/states/page';
+import { useCurrentPageData, useSetRemoteLatestPageData } from '~/states/page';
 import { useShareLinkId } from '~/states/page/hooks';
 import { useConflictDiffModalActions } from '~/states/ui/modal/conflict-diff';
 import { useHandsontableModalActions } from '~/states/ui/modal/handsontable';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:cli:side-effects:useHandsontableModalLauncherForView');
-
+const logger = loggerFactory(
+  'growi:cli:side-effects:useHandsontableModalLauncherForView',
+);
 
 export const useHandsontableModalLauncherForView = (opts?: {
-  onSaveSuccess?: () => void,
-  onSaveError?: (error: any) => void,
+  onSaveSuccess?: () => void;
+  onSaveError?: (error: any) => void;
 }): void => {
-
   const shareLinkId = useShareLinkId();
 
   const currentPage = useCurrentPageData();
 
   const { open: openHandsontableModal } = useHandsontableModalActions();
 
-  const { open: openConflictDiffModal, close: closeConflictDiffModal } = useConflictDiffModalActions();
+  const { open: openConflictDiffModal, close: closeConflictDiffModal } =
+    useConflictDiffModalActions();
 
   const _updatePage = useUpdatePage();
 
   const setRemoteLatestPageData = useSetRemoteLatestPageData();
 
   // eslint-disable-next-line max-len
-  const updatePage = useCallback(async(revisionId:string, newMarkdown: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {
-    if (currentPage == null || currentPage.revision == null || shareLinkId != null) {
-      return;
-    }
-
-    try {
-      // There are cases where "revisionId" is not required for revision updates
-      // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
-      await _updatePage({
-        pageId: currentPage._id,
-        revisionId,
-        body: newMarkdown,
-        origin: Origin.View,
-      });
-
-      closeConflictDiffModal();
-      opts?.onSaveSuccess?.();
-    }
-    catch (error) {
-      const remoteRevidsionData = extractRemoteRevisionDataFromErrorObj(error);
-      if (remoteRevidsionData != null) {
-        onConflict?.(remoteRevidsionData, newMarkdown);
+  const updatePage = useCallback(
+    async (
+      revisionId: string,
+      newMarkdown: string,
+      onConflict: (
+        conflictData: RemoteRevisionData,
+        newMarkdown: string,
+      ) => void,
+    ) => {
+      if (
+        currentPage == null ||
+        currentPage.revision == null ||
+        shareLinkId != null
+      ) {
+        return;
       }
 
-      logger.error('failed to save', error);
-      opts?.onSaveError?.(error);
-    }
-  }, [_updatePage, closeConflictDiffModal, currentPage, opts, shareLinkId]);
+      try {
+        // There are cases where "revisionId" is not required for revision updates
+        // See: https://dev.growi.org/651a6f4a008fee2f99187431#origin-%E3%81%AE%E5%BC%B7%E5%BC%B1
+        await _updatePage({
+          pageId: currentPage._id,
+          revisionId,
+          body: newMarkdown,
+          origin: Origin.View,
+        });
+
+        closeConflictDiffModal();
+        opts?.onSaveSuccess?.();
+      } catch (error) {
+        const remoteRevidsionData =
+          extractRemoteRevisionDataFromErrorObj(error);
+        if (remoteRevidsionData != null) {
+          onConflict?.(remoteRevidsionData, newMarkdown);
+        }
+
+        logger.error('failed to save', error);
+        opts?.onSaveError?.(error);
+      }
+    },
+    [_updatePage, closeConflictDiffModal, currentPage, opts, shareLinkId],
+  );
 
   // eslint-disable-next-line max-len
-  const generateResolveConflictHandler = useCallback((revisionId: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {
-    return async(newMarkdown: string) => {
-      await updatePage(revisionId, newMarkdown, onConflict);
-    };
-  }, [updatePage]);
-
-  const onConflictHandler = useCallback((remoteRevidsionData: RemoteRevisionData, newMarkdown: string) => {
-    setRemoteLatestPageData(remoteRevidsionData);
-
-    const resolveConflictHandler = generateResolveConflictHandler(remoteRevidsionData.remoteRevisionId, onConflictHandler);
-    if (resolveConflictHandler == null) {
-      return;
-    }
-
-    openConflictDiffModal(newMarkdown, resolveConflictHandler);
-  }, [generateResolveConflictHandler, openConflictDiffModal, setRemoteLatestPageData]);
+  const generateResolveConflictHandler = useCallback(
+    (
+      revisionId: string,
+      onConflict: (
+        conflictData: RemoteRevisionData,
+        newMarkdown: string,
+      ) => void,
+    ) => {
+      return async (newMarkdown: string) => {
+        await updatePage(revisionId, newMarkdown, onConflict);
+      };
+    },
+    [updatePage],
+  );
+
+  const onConflictHandler = useCallback(
+    (remoteRevidsionData: RemoteRevisionData, newMarkdown: string) => {
+      setRemoteLatestPageData(remoteRevidsionData);
+
+      const resolveConflictHandler = generateResolveConflictHandler(
+        remoteRevidsionData.remoteRevisionId,
+        onConflictHandler,
+      );
+      if (resolveConflictHandler == null) {
+        return;
+      }
 
-  const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
-    if (currentPage == null || currentPage.revision == null) {
-      return;
-    }
+      openConflictDiffModal(newMarkdown, resolveConflictHandler);
+    },
+    [
+      generateResolveConflictHandler,
+      openConflictDiffModal,
+      setRemoteLatestPageData,
+    ],
+  );
+
+  const saveByHandsontableModal = useCallback(
+    async (table: MarkdownTable, bol: number, eol: number) => {
+      if (currentPage == null || currentPage.revision == null) {
+        return;
+      }
 
-    const currentRevisionId = currentPage.revision._id;
-    const currentMarkdown = currentPage.revision.body;
-    const newMarkdown = replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
+      const currentRevisionId = currentPage.revision._id;
+      const currentMarkdown = currentPage.revision.body;
+      const newMarkdown = replaceMarkdownTableInMarkdown(
+        table,
+        currentMarkdown,
+        bol,
+        eol,
+      );
 
-    await updatePage(currentRevisionId, newMarkdown, onConflictHandler);
-  }, [currentPage, onConflictHandler, updatePage]);
+      await updatePage(currentRevisionId, newMarkdown, onConflictHandler);
+    },
+    [currentPage, onConflictHandler, updatePage],
+  );
 
   // set handler to open HandsonTableModal
   useEffect(() => {
@@ -107,12 +153,19 @@ export const useHandsontableModalLauncherForView = (opts?: {
       const markdown = currentPage.revision.body;
       const { bol, eol } = evt.detail;
       const currentMarkdownTable = getMarkdownTableFromLine(markdown, bol, eol);
-      openHandsontableModal(currentMarkdownTable, false, table => saveByHandsontableModal(table, bol, eol));
+      openHandsontableModal(currentMarkdownTable, false, (table) =>
+        saveByHandsontableModal(table, bol, eol),
+      );
     };
     globalEventTarget.addEventListener('launchHandsonTableModal', handler);
 
     return function cleanup() {
       globalEventTarget.removeEventListener('launchHandsonTableModal', handler);
     };
-  }, [currentPage, openHandsontableModal, saveByHandsontableModal, shareLinkId]);
+  }, [
+    currentPage,
+    openHandsontableModal,
+    saveByHandsontableModal,
+    shareLinkId,
+  ]);
 };

+ 4 - 6
apps/app/src/client/services/side-effects/hash-changed.ts

@@ -1,9 +1,8 @@
 import { useCallback, useEffect } from 'react';
-
 import { useRouter } from 'next/router';
 
 import { useIsEditable } from '~/states/page';
-import { useEditorMode, determineEditorModeByHash } from '~/states/ui/editor';
+import { determineEditorModeByHash, useEditorMode } from '~/states/ui/editor';
 
 /**
  * Change editorMode by browser forward/back operation
@@ -34,13 +33,12 @@ export const useHashChangedEffect = (): void => {
     return function cleanup() {
       window.removeEventListener('hashchange', hashchangeHandler);
     };
-
   }, [hashchangeHandler, isEditable]);
 
   /*
-  * Route changes by Next Router
-  * https://nextjs.org/docs/api-reference/next/router
-  */
+   * Route changes by Next Router
+   * https://nextjs.org/docs/api-reference/next/router
+   */
   useEffect(() => {
     router.events.on('routeChangeComplete', hashchangeHandler);
 

+ 65 - 41
apps/app/src/client/services/side-effects/page-updated.ts

@@ -1,71 +1,95 @@
 import { useCallback, useEffect } from 'react';
 
 import { SocketEventName } from '~/interfaces/websocket';
-import { useCurrentPageData, useFetchCurrentPage, useSetRemoteLatestPageData } from '~/states/page';
 import type { RemoteRevisionData } from '~/states/page';
+import {
+  useCurrentPageData,
+  useFetchCurrentPage,
+  useSetRemoteLatestPageData,
+} from '~/states/page';
 import { useGlobalSocket } from '~/states/socket-io';
-import { useEditorMode, EditorMode } from '~/states/ui/editor';
+import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import { usePageStatusAlertActions } from '~/states/ui/modal/page-status-alert';
 import { useSWRxPageInfo } from '~/stores/page';
 
-
 export const usePageUpdatedEffect = (): void => {
-
   const setRemoteLatestPageData = useSetRemoteLatestPageData();
 
   const socket = useGlobalSocket();
   const { editorMode } = useEditorMode();
   const currentPage = useCurrentPageData();
   const { fetchCurrentPage } = useFetchCurrentPage();
-  const { open: openPageStatusAlert, close: closePageStatusAlert } = usePageStatusAlertActions();
+  const { open: openPageStatusAlert, close: closePageStatusAlert } =
+    usePageStatusAlertActions();
 
   const { mutate: mutatePageInfo } = useSWRxPageInfo(currentPage?._id);
 
-  const remotePageDataUpdateHandler = useCallback((data) => {
-    // Set remote page data
-    const { s2cMessagePageUpdated } = data;
-
-    const remoteData: RemoteRevisionData = {
-      remoteRevisionId: s2cMessagePageUpdated.revisionId,
-      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
-      remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
-      remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
-    };
-
-    if (currentPage?._id != null && currentPage._id === s2cMessagePageUpdated.pageId) {
-      setRemoteLatestPageData(remoteData);
-
-      // Update PageInfo cache
-      mutatePageInfo();
-
-      // Open PageStatusAlert
-      const currentRevisionId = currentPage?.revision?._id;
-      const remoteRevisionId = s2cMessagePageUpdated.revisionId;
-      const isRevisionOutdated = (currentRevisionId != null || remoteRevisionId != null) && currentRevisionId !== remoteRevisionId;
-
-      // !!CAUTION!! Timing of calling openPageStatusAlert may clash with components/PageEditor/conflict.tsx
-      if (isRevisionOutdated && editorMode === EditorMode.View) {
-        openPageStatusAlert({ hideEditorMode: EditorMode.Editor, onRefleshPage: () => fetchCurrentPage({ force: true }) });
-      }
-
-      // Clear cache
-      if (!isRevisionOutdated) {
-        closePageStatusAlert();
+  const remotePageDataUpdateHandler = useCallback(
+    (data) => {
+      // Set remote page data
+      const { s2cMessagePageUpdated } = data;
+
+      const remoteData: RemoteRevisionData = {
+        remoteRevisionId: s2cMessagePageUpdated.revisionId,
+        remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
+        remoteRevisionLastUpdateUser:
+          s2cMessagePageUpdated.remoteLastUpdateUser,
+        remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
+      };
+
+      if (
+        currentPage?._id != null &&
+        currentPage._id === s2cMessagePageUpdated.pageId
+      ) {
+        setRemoteLatestPageData(remoteData);
+
+        // Update PageInfo cache
+        mutatePageInfo();
+
+        // Open PageStatusAlert
+        const currentRevisionId = currentPage?.revision?._id;
+        const remoteRevisionId = s2cMessagePageUpdated.revisionId;
+        const isRevisionOutdated =
+          (currentRevisionId != null || remoteRevisionId != null) &&
+          currentRevisionId !== remoteRevisionId;
+
+        // !!CAUTION!! Timing of calling openPageStatusAlert may clash with components/PageEditor/conflict.tsx
+        if (isRevisionOutdated && editorMode === EditorMode.View) {
+          openPageStatusAlert({
+            hideEditorMode: EditorMode.Editor,
+            onRefleshPage: () => fetchCurrentPage({ force: true }),
+          });
+        }
+
+        // Clear cache
+        if (!isRevisionOutdated) {
+          closePageStatusAlert();
+        }
       }
-    }
-  // eslint-disable-next-line max-len
-  }, [currentPage?._id, currentPage?.revision?._id, setRemoteLatestPageData, mutatePageInfo, editorMode, openPageStatusAlert, fetchCurrentPage, closePageStatusAlert]);
+      // eslint-disable-next-line max-len
+    },
+    [
+      currentPage?._id,
+      currentPage?.revision?._id,
+      setRemoteLatestPageData,
+      mutatePageInfo,
+      editorMode,
+      openPageStatusAlert,
+      fetchCurrentPage,
+      closePageStatusAlert,
+    ],
+  );
 
   // listen socket for someone updating this page
   useEffect(() => {
-
-    if (socket == null) { return }
+    if (socket == null) {
+      return;
+    }
 
     socket.on(SocketEventName.PageUpdated, remotePageDataUpdateHandler);
 
     return () => {
       socket.off(SocketEventName.PageUpdated, remotePageDataUpdateHandler);
     };
-
   }, [remotePageDataUpdateHandler, socket]);
 };

+ 1 - 1
apps/app/src/client/services/side-effects/use-sticky.ts

@@ -1,4 +1,4 @@
-import { useState, useEffect } from 'react';
+import { useEffect, useState } from 'react';
 
 // Custom hook that accepts a selector string as an argument
 // and returns a boolean indicating whether the selected element is currently sticky.

+ 3 - 2
apps/app/src/client/services/update-page/conflict.tsx

@@ -3,10 +3,11 @@ import type { ErrorV3 } from '@growi/core/dist/models';
 import { PageUpdateErrorCode } from '~/interfaces/apiv3';
 import type { RemoteRevisionData } from '~/states/page';
 
-export const extractRemoteRevisionDataFromErrorObj = (errors: Array<ErrorV3>): RemoteRevisionData | undefined => {
+export const extractRemoteRevisionDataFromErrorObj = (
+  errors: Array<ErrorV3>,
+): RemoteRevisionData | undefined => {
   for (const error of errors) {
     if (error.code === PageUpdateErrorCode.CONFLICT) {
-
       const latestRevision = error.args.returnLatestRevision;
 
       const remoteRevidsionData = {

+ 7 - 2
apps/app/src/client/services/update-page/update-page.ts

@@ -1,7 +1,12 @@
 import { apiv3Put } from '~/client/util/apiv3-client';
-import type { IApiv3PageUpdateParams, IApiv3PageUpdateResponse } from '~/interfaces/apiv3';
+import type {
+  IApiv3PageUpdateParams,
+  IApiv3PageUpdateResponse,
+} from '~/interfaces/apiv3';
 
-export const updatePage = async(params: IApiv3PageUpdateParams): Promise<IApiv3PageUpdateResponse> => {
+export const updatePage = async (
+  params: IApiv3PageUpdateParams,
+): Promise<IApiv3PageUpdateResponse> => {
   const res = await apiv3Put<IApiv3PageUpdateResponse>('/page', params);
   return res.data;
 };

+ 16 - 10
apps/app/src/client/services/update-page/use-update-page.tsx

@@ -1,25 +1,31 @@
 import { useCallback } from 'react';
 
-import type { IApiv3PageUpdateParams, IApiv3PageUpdateResponse } from '~/interfaces/apiv3';
+import type {
+  IApiv3PageUpdateParams,
+  IApiv3PageUpdateResponse,
+} from '~/interfaces/apiv3';
 import { useSetIsUntitledPage } from '~/states/page';
 
 import { updatePage } from './update-page';
 
-
-type UseUpdatePage = (params: IApiv3PageUpdateParams) => Promise<IApiv3PageUpdateResponse>;
-
+type UseUpdatePage = (
+  params: IApiv3PageUpdateParams,
+) => Promise<IApiv3PageUpdateResponse>;
 
 export const useUpdatePage = (): UseUpdatePage => {
   const setIsUntitledPage = useSetIsUntitledPage();
 
-  const updatePageExt: UseUpdatePage = useCallback(async (params) => {
-    const result = await updatePage(params);
+  const updatePageExt: UseUpdatePage = useCallback(
+    async (params) => {
+      const result = await updatePage(params);
 
-    // set false to isUntitledPage
-    setIsUntitledPage(false);
+      // set false to isUntitledPage
+      setIsUntitledPage(false);
 
-    return result;
-  }, [setIsUntitledPage]);
+      return result;
+    },
+    [setIsUntitledPage],
+  );
 
   return updatePageExt;
 };

+ 25 - 12
apps/app/src/client/services/upload-attachments/upload-attachments.ts

@@ -1,23 +1,33 @@
 import type { IAttachment } from '@growi/core';
 
 import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
-import type { IApiv3GetAttachmentLimitParams, IApiv3GetAttachmentLimitResponse, IApiv3PostAttachmentResponse } from '~/interfaces/apiv3/attachment';
+import type {
+  IApiv3GetAttachmentLimitParams,
+  IApiv3GetAttachmentLimitResponse,
+  IApiv3PostAttachmentResponse,
+} from '~/interfaces/apiv3/attachment';
 import loggerFactory from '~/utils/logger';
 
-
 const logger = loggerFactory('growi:client:services:upload-attachment');
 
-
 type UploadOpts = {
-  onUploaded?: (attachment: IAttachment) => void,
-  onError?: (error: Error, file: File) => void,
-}
+  onUploaded?: (attachment: IAttachment) => void;
+  onError?: (error: Error, file: File) => void;
+};
 
-export const uploadAttachments = async(pageId: string, files: File[], opts?: UploadOpts): Promise<void> => {
-  files.forEach(async(file) => {
+export const uploadAttachments = async (
+  pageId: string,
+  files: File[],
+  opts?: UploadOpts,
+): Promise<void> => {
+  files.forEach(async (file) => {
     try {
       const params: IApiv3GetAttachmentLimitParams = { fileSize: file.size };
-      const { data: resLimit } = await apiv3Get<IApiv3GetAttachmentLimitResponse>('/attachment/limit', params);
+      const { data: resLimit } =
+        await apiv3Get<IApiv3GetAttachmentLimitResponse>(
+          '/attachment/limit',
+          params,
+        );
 
       if (!resLimit.isUploadable) {
         throw new Error(resLimit.errorMessage);
@@ -27,11 +37,14 @@ export const uploadAttachments = async(pageId: string, files: File[], opts?: Upl
       formData.append('file', file);
       formData.append('page_id', pageId);
 
-      const { data: resAdd } = await apiv3PostForm<IApiv3PostAttachmentResponse>('/attachment', formData);
+      const { data: resAdd } =
+        await apiv3PostForm<IApiv3PostAttachmentResponse>(
+          '/attachment',
+          formData,
+        );
 
       opts?.onUploaded?.(resAdd.attachment);
-    }
-    catch (e) {
+    } catch (e) {
       logger.error('failed to upload', e);
       opts?.onError?.(e, file);
     }

+ 4 - 4
apps/app/src/client/services/use-print-mode.ts

@@ -1,5 +1,4 @@
 import { useEffect, useState } from 'react';
-
 import { flushSync } from 'react-dom';
 
 export const usePrintMode = (): boolean => {
@@ -7,9 +6,10 @@ export const usePrintMode = (): boolean => {
 
   useEffect(() => {
     // force re-render on beforeprint
-    const handleBeforePrint = () => flushSync(() => {
-      setIsPrinting(true);
-    });
+    const handleBeforePrint = () =>
+      flushSync(() => {
+        setIsPrinting(true);
+      });
 
     const handleAfterPrint = () => {
       setIsPrinting(false);

+ 23 - 22
apps/app/src/client/services/use-start-editing.tsx

@@ -1,11 +1,10 @@
 import { useCallback } from 'react';
-
 import { Origin } from '@growi/core';
 import { getParentPath } from '@growi/core/dist/utils/path-utils';
 
 import { useCreatePage } from '~/client/services/create-page';
 import { usePageNotFound } from '~/states/page';
-import { useEditorMode, EditorMode } from '~/states/ui/editor';
+import { EditorMode, useEditorMode } from '~/states/ui/editor';
 
 import { shouldCreateWipPage } from '../../utils/should-create-wip-page';
 
@@ -14,25 +13,27 @@ export const useStartEditing = (): ((path?: string) => Promise<void>) => {
   const { setEditorMode } = useEditorMode();
   const { create } = useCreatePage();
 
-  return useCallback(async (path?: string) => {
-    if (!isNotFound) {
-      setEditorMode(EditorMode.Editor);
-      return;
-    }
-    // Create a new page if it does not exist and transit to the editor mode
-    try {
-      const parentPath = path != null ? getParentPath(path) : undefined; // does not have to exist
-      await create(
-        {
-          path, parentPath, wip: shouldCreateWipPage(path), origin: Origin.View,
-        },
-      );
-
-      setEditorMode(EditorMode.Editor);
-    }
-    catch (err) {
-      throw new Error(err);
-    }
-  }, [create, isNotFound, setEditorMode]);
+  return useCallback(
+    async (path?: string) => {
+      if (!isNotFound) {
+        setEditorMode(EditorMode.Editor);
+        return;
+      }
+      // Create a new page if it does not exist and transit to the editor mode
+      try {
+        const parentPath = path != null ? getParentPath(path) : undefined; // does not have to exist
+        await create({
+          path,
+          parentPath,
+          wip: shouldCreateWipPage(path),
+          origin: Origin.View,
+        });
 
+        setEditorMode(EditorMode.Editor);
+      } catch (err) {
+        throw new Error(err);
+      }
+    },
+    [create, isNotFound, setEditorMode],
+  );
 };

+ 13 - 10
apps/app/src/client/services/use-toastr-on-error.tsx

@@ -1,18 +1,21 @@
 import { useCallback } from 'react';
-
 import { useTranslation } from 'react-i18next';
 
 import { toastError } from '~/client/util/toastr';
 
-export const useToastrOnError = <P, R>(method?: (param?: P) => Promise<R|undefined>): (param?: P) => Promise<R|undefined> => {
+export const useToastrOnError = <P, R>(
+  method?: (param?: P) => Promise<R | undefined>,
+): ((param?: P) => Promise<R | undefined>) => {
   const { t } = useTranslation('commons');
 
-  return useCallback(async(param) => {
-    try {
-      return await method?.(param);
-    }
-    catch (err) {
-      toastError(t('toaster.create_failed', { target: 'a page' }));
-    }
-  }, [method, t]);
+  return useCallback(
+    async (param) => {
+      try {
+        return await method?.(param);
+      } catch (err) {
+        toastError(t('toaster.create_failed', { target: 'a page' }));
+      }
+    },
+    [method, t],
+  );
 };

+ 16 - 5
apps/app/src/client/services/user-ui-settings.ts

@@ -6,8 +6,12 @@ import { apiv3Put } from '~/client/util/apiv3-client';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 
 let settingsForBulk: Partial<IUserUISettings> = {};
-const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> => {
-  const result = apiv3Put<IUserUISettings>('/user-ui-settings', { settings: settingsForBulk });
+const _putUserUISettingsInBulk = (): Promise<
+  AxiosResponse<IUserUISettings>
+> => {
+  const result = apiv3Put<IUserUISettings>('/user-ui-settings', {
+    settings: settingsForBulk,
+  });
 
   // clear partial
   settingsForBulk = {};
@@ -15,7 +19,10 @@ const _putUserUISettingsInBulk = (): Promise<AxiosResponse<IUserUISettings>> =>
   return result;
 };
 
-const _putUserUISettingsInBulkDebounced = debounce(1500, _putUserUISettingsInBulk);
+const _putUserUISettingsInBulkDebounced = debounce(
+  1500,
+  _putUserUISettingsInBulk,
+);
 
 export const scheduleToPut = (settings: Partial<IUserUISettings>): void => {
   settingsForBulk = {
@@ -26,8 +33,12 @@ export const scheduleToPut = (settings: Partial<IUserUISettings>): void => {
   _putUserUISettingsInBulkDebounced();
 };
 
-export const updateUserUISettings = async(settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {
-  const result = await apiv3Put<IUserUISettings>('/user-ui-settings', { settings });
+export const updateUserUISettings = async (
+  settings: Partial<IUserUISettings>,
+): Promise<AxiosResponse<IUserUISettings>> => {
+  const result = await apiv3Put<IUserUISettings>('/user-ui-settings', {
+    settings,
+  });
 
   return result;
 };

+ 26 - 9
apps/app/src/client/util/apiv1-client.ts

@@ -5,7 +5,6 @@ import axios from '~/utils/axios';
 const apiv1Root = '/_api';
 
 class Apiv1ErrorHandler extends Error {
-
   code;
 
   data;
@@ -16,12 +15,14 @@ class Apiv1ErrorHandler extends Error {
     this.message = message;
     this.code = code;
     this.data = data;
-
   }
-
 }
 
-export async function apiRequest<T>(method: string, path: string, params: unknown): Promise<T> {
+export async function apiRequest<T>(
+  method: string,
+  path: string,
+  params: unknown,
+): Promise<T> {
   const res = await axios[method](urljoin(apiv1Root, path), params);
 
   if (res.data.ok) {
@@ -30,25 +31,41 @@ export async function apiRequest<T>(method: string, path: string, params: unknow
 
   // Return error code if code is exist
   if (res.data.code != null) {
-    const error = new Apiv1ErrorHandler(res.data.error, res.data.code, res.data.data);
+    const error = new Apiv1ErrorHandler(
+      res.data.error,
+      res.data.code,
+      res.data.data,
+    );
     throw error;
   }
 
   throw new Error(res.data.error);
 }
 
-export async function apiGet<T>(path: string, params: unknown = {}): Promise<T> {
+export async function apiGet<T>(
+  path: string,
+  params: unknown = {},
+): Promise<T> {
   return apiRequest<T>('get', path, { params });
 }
 
-export async function apiPost<T>(path: string, params: unknown = {}): Promise<T> {
+export async function apiPost<T>(
+  path: string,
+  params: unknown = {},
+): Promise<T> {
   return apiRequest<T>('post', path, params);
 }
 
-export async function apiPostForm<T>(path: string, formData: FormData): Promise<T> {
+export async function apiPostForm<T>(
+  path: string,
+  formData: FormData,
+): Promise<T> {
   return apiRequest<T>('postForm', path, formData);
 }
 
-export async function apiDelete<T>(path: string, params: unknown = {}): Promise<T> {
+export async function apiDelete<T>(
+  path: string,
+  params: unknown = {},
+): Promise<T> {
   return apiRequest<T>('delete', path, { data: params });
 }

+ 29 - 10
apps/app/src/client/util/apiv3-client.ts

@@ -10,12 +10,13 @@ const apiv3Root = '/_api/v3';
 
 const logger = loggerFactory('growi:apiv3');
 
-
 const apiv3ErrorHandler = (_err: any): any[] => {
   // extract api errors from general 400 err
   const err = axios.isAxiosError(_err) ? _err.response?.data.errors : _err;
   const errs = toArrayIfNot(err);
-  const errorInfo = axios.isAxiosError(_err) ? _err.response?.data.info : undefined;
+  const errorInfo = axios.isAxiosError(_err)
+    ? _err.response?.data.info
+    : undefined;
 
   for (const err of errs) {
     logger.error(err.message);
@@ -28,33 +29,51 @@ const apiv3ErrorHandler = (_err: any): any[] => {
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export async function apiv3Request<T = any>(method: string, path: string, params: unknown): Promise<AxiosResponse<T>> {
+export async function apiv3Request<T = any>(
+  method: string,
+  path: string,
+  params: unknown,
+): Promise<AxiosResponse<T>> {
   try {
     const res = await axios[method](urljoin(apiv3Root, path), params);
     return res;
-  }
-  catch (err) {
+  } catch (err) {
     const errors = apiv3ErrorHandler(err);
     throw errors;
   }
 }
 
-export async function apiv3Get<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+export async function apiv3Get<T = any>(
+  path: string,
+  params: unknown = {},
+): Promise<AxiosResponse<T>> {
   return apiv3Request('get', path, { params });
 }
 
-export async function apiv3Post<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+export async function apiv3Post<T = any>(
+  path: string,
+  params: unknown = {},
+): Promise<AxiosResponse<T>> {
   return apiv3Request('post', path, params);
 }
 
-export async function apiv3PostForm<T = any>(path: string, formData: FormData): Promise<AxiosResponse<T>> {
+export async function apiv3PostForm<T = any>(
+  path: string,
+  formData: FormData,
+): Promise<AxiosResponse<T>> {
   return apiv3Request('postForm', path, formData);
 }
 
-export async function apiv3Put<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+export async function apiv3Put<T = any>(
+  path: string,
+  params: unknown = {},
+): Promise<AxiosResponse<T>> {
   return apiv3Request('put', path, params);
 }
 
-export async function apiv3Delete<T = any>(path: string, params: unknown = {}): Promise<AxiosResponse<T>> {
+export async function apiv3Delete<T = any>(
+  path: string,
+  params: unknown = {},
+): Promise<AxiosResponse<T>> {
   return apiv3Request('delete', path, { params });
 }

+ 48 - 12
apps/app/src/client/util/bookmark-utils.ts

@@ -1,44 +1,80 @@
 import type { IRevision, Ref } from '@growi/core';
 
-import type { BookmarkFolderItems, BookmarkedPage } from '~/interfaces/bookmark-info';
+import type {
+  BookmarkedPage,
+  BookmarkFolderItems,
+} from '~/interfaces/bookmark-info';
 
 import { apiv3Delete, apiv3Post, apiv3Put } from './apiv3-client';
 
 // Check if bookmark folder item has childFolder or bookmarks
-export const hasChildren = ({ childFolder, bookmarks }: { childFolder?: BookmarkFolderItems[], bookmarks?: BookmarkedPage[] }): boolean => {
-  return !!((childFolder && childFolder.length > 0) || (bookmarks && bookmarks.length > 0));
+export const hasChildren = ({
+  childFolder,
+  bookmarks,
+}: {
+  childFolder?: BookmarkFolderItems[];
+  bookmarks?: BookmarkedPage[];
+}): boolean => {
+  return !!(
+    (childFolder && childFolder.length > 0) ||
+    (bookmarks && bookmarks.length > 0)
+  );
 };
 
 // Add new folder helper
-export const addNewFolder = async(name: string, parent: string | null): Promise<void> => {
+export const addNewFolder = async (
+  name: string,
+  parent: string | null,
+): Promise<void> => {
   await apiv3Post('/bookmark-folder', { name, parent });
 };
 
 // Put bookmark to a folder
-export const addBookmarkToFolder = async(pageId: string, folderId: string | null): Promise<void> => {
-  await apiv3Post('/bookmark-folder/add-bookmark-to-folder', { pageId, folderId });
+export const addBookmarkToFolder = async (
+  pageId: string,
+  folderId: string | null,
+): Promise<void> => {
+  await apiv3Post('/bookmark-folder/add-bookmark-to-folder', {
+    pageId,
+    folderId,
+  });
 };
 
 // Delete bookmark folder
-export const deleteBookmarkFolder = async(bookmarkFolderId: string): Promise<void> => {
+export const deleteBookmarkFolder = async (
+  bookmarkFolderId: string,
+): Promise<void> => {
   await apiv3Delete(`/bookmark-folder/${bookmarkFolderId}`);
 };
 
 // Rename page from bookmark item control
-export const renamePage = async(pageId: string, revisionId: Ref<IRevision> | undefined, newPagePath: string): Promise<void> => {
+export const renamePage = async (
+  pageId: string,
+  revisionId: Ref<IRevision> | undefined,
+  newPagePath: string,
+): Promise<void> => {
   await apiv3Put('/pages/rename', { pageId, revisionId, newPagePath });
 };
 
 // Update bookmark by isBookmarked status
-export const toggleBookmark = async(pageId: string, status: boolean): Promise<void> => {
+export const toggleBookmark = async (
+  pageId: string,
+  status: boolean,
+): Promise<void> => {
   await apiv3Put('/bookmark-folder/update-bookmark', { pageId, status });
 };
 
 // Update Bookmark folder
-export const updateBookmarkFolder = async(
-    bookmarkFolderId: string, name: string, parent: string | null, childFolder: BookmarkFolderItems[],
+export const updateBookmarkFolder = async (
+  bookmarkFolderId: string,
+  name: string,
+  parent: string | null,
+  childFolder: BookmarkFolderItems[],
 ): Promise<void> => {
   await apiv3Put('/bookmark-folder', {
-    bookmarkFolderId, name, parent, childFolder,
+    bookmarkFolderId,
+    name,
+    parent,
+    childFolder,
   });
 };

+ 14 - 7
apps/app/src/client/util/scope-util.test.ts

@@ -1,10 +1,9 @@
 import { SCOPE } from '@growi/core/dist/interfaces';
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
 
-import { parseScopes, getDisabledScopes, extractScopes } from './scope-util';
+import { extractScopes, getDisabledScopes, parseScopes } from './scope-util';
 
 describe('scope-util', () => {
-
   const mockScopes = {
     READ: {
       USER: 'read:user',
@@ -45,8 +44,12 @@ describe('scope-util', () => {
     expect(result.ALL).toBeDefined();
 
     // Check admin settings
-    expect(result.ADMIN['admin:setting']['read:admin:setting']).toBe('read:admin:setting');
-    expect(result.ADMIN['admin:setting']['write:admin:setting']).toBe('write:admin:setting');
+    expect(result.ADMIN['admin:setting']['read:admin:setting']).toBe(
+      'read:admin:setting',
+    );
+    expect(result.ADMIN['admin:setting']['write:admin:setting']).toBe(
+      'write:admin:setting',
+    );
 
     // Check ALL category
     expect(result.ALL['read:all']).toBe('read:all');
@@ -79,8 +82,12 @@ describe('scope-util', () => {
   it('should handle multiple wildcard selections', () => {
     const selectedScopes = [SCOPE.READ.ALL, SCOPE.WRITE.ALL];
     const availableScopes = [
-      SCOPE.READ.FEATURES.PAGE, SCOPE.READ.FEATURES.ATTACHMENT, SCOPE.READ.ALL,
-      SCOPE.WRITE.FEATURES.PAGE, SCOPE.WRITE.FEATURES.ATTACHMENT, SCOPE.WRITE.ALL,
+      SCOPE.READ.FEATURES.PAGE,
+      SCOPE.READ.FEATURES.ATTACHMENT,
+      SCOPE.READ.ALL,
+      SCOPE.WRITE.FEATURES.PAGE,
+      SCOPE.WRITE.FEATURES.ATTACHMENT,
+      SCOPE.WRITE.ALL,
     ];
 
     const result = getDisabledScopes(selectedScopes, availableScopes);

+ 26 - 16
apps/app/src/client/util/scope-util.ts

@@ -1,6 +1,5 @@
 import { ALL_SIGN, type Scope } from '@growi/core/dist/interfaces';
 
-
 // Data structure for the final merged scopes
 interface ScopeMap {
   [key: string]: Scope | ScopeMap;
@@ -9,17 +8,17 @@ interface ScopeMap {
 // Input object with arbitrary action keys (e.g., READ, WRITE)
 type ScopesInput = Record<string, any>;
 
-
 function parseSubScope(
-    parentKey: string,
-    subObjForActions: Record<string, any>,
-    actions: string[],
+  parentKey: string,
+  subObjForActions: Record<string, any>,
+  actions: string[],
 ): ScopeMap {
   const result: ScopeMap = {};
 
   for (const action of actions) {
     if (typeof subObjForActions[action] === 'string') {
-      result[`${action.toLowerCase()}:${parentKey.toLowerCase()}`] = subObjForActions[action];
+      result[`${action.toLowerCase()}:${parentKey.toLowerCase()}`] =
+        subObjForActions[action];
       subObjForActions[action] = undefined;
     }
   }
@@ -28,7 +27,9 @@ function parseSubScope(
   for (const action of actions) {
     const obj = subObjForActions[action];
     if (obj && typeof obj === 'object') {
-      Object.keys(obj).forEach(k => childKeys.add(k));
+      Object.keys(obj).forEach((k) => {
+        childKeys.add(k);
+      });
     }
   }
 
@@ -37,7 +38,8 @@ function parseSubScope(
       for (const action of actions) {
         const val = subObjForActions[action]?.[ck];
         if (typeof val === 'string') {
-          result[`${action.toLowerCase()}:${parentKey.toLowerCase()}:all`] = val as Scope;
+          result[`${action.toLowerCase()}:${parentKey.toLowerCase()}:all`] =
+            val as Scope;
         }
       }
       continue;
@@ -55,13 +57,21 @@ function parseSubScope(
   return result;
 }
 
-export function parseScopes({ scopes, isAdmin = false }: { scopes: ScopesInput ; isAdmin?: boolean }): ScopeMap {
+export function parseScopes({
+  scopes,
+  isAdmin = false,
+}: {
+  scopes: ScopesInput;
+  isAdmin?: boolean;
+}): ScopeMap {
   const actions = Object.keys(scopes);
   const topKeys = new Set<string>();
 
   // Collect all top-level keys (e.g., ALL, ADMIN, USER) across all actions
   for (const action of actions) {
-    Object.keys(scopes[action] || {}).forEach(k => topKeys.add(k));
+    Object.keys(scopes[action] || {}).forEach((k) => {
+      topKeys.add(k);
+    });
   }
 
   const result: ScopeMap = {};
@@ -81,8 +91,7 @@ export function parseScopes({ scopes, isAdmin = false }: { scopes: ScopesInput ;
         }
       }
       result.ALL = allObj;
-    }
-    else {
+    } else {
       const subObjForActions: Record<string, any> = {};
       for (const action of actions) {
         subObjForActions[action] = scopes[action]?.[key];
@@ -97,10 +106,12 @@ export function parseScopes({ scopes, isAdmin = false }: { scopes: ScopesInput ;
 /**
  * Determines which scopes should be disabled based on wildcard selections
  */
-export function getDisabledScopes(selectedScopes: Scope[], availableScopes: string[]): Set<Scope> {
+export function getDisabledScopes(
+  selectedScopes: Scope[],
+  availableScopes: string[],
+): Set<Scope> {
   const disabledSet = new Set<Scope>();
 
-
   // If no selected scopes, return empty set
   if (!selectedScopes || selectedScopes.length === 0) {
     return disabledSet;
@@ -133,8 +144,7 @@ export function extractScopes(obj: Record<string, any>): string[] {
   Object.values(obj).forEach((value) => {
     if (typeof value === 'string') {
       result.push(value);
-    }
-    else if (typeof value === 'object' && !Array.isArray(value)) {
+    } else if (typeof value === 'object' && !Array.isArray(value)) {
       result = result.concat(extractScopes(value));
     }
   });

+ 10 - 9
apps/app/src/client/util/t-with-opt.ts

@@ -1,15 +1,16 @@
 import { useCallback } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
-export const useTWithOpt = (): (key: string, opt?: any) => string => {
-
+export const useTWithOpt = (): ((key: string, opt?: any) => string) => {
   const { t } = useTranslation();
 
-  return useCallback((key, opt) => {
-    if (typeof opt === 'object') {
-      return t(key, opt).toString();
-    }
-    return t(key);
-  }, [t]);
+  return useCallback(
+    (key, opt) => {
+      if (typeof opt === 'object') {
+        return t(key, opt).toString();
+      }
+      return t(key);
+    },
+    [t],
+  );
 };

+ 13 - 5
apps/app/src/client/util/toastr.ts

@@ -3,12 +3,14 @@ import { toast } from 'react-toastify';
 
 import { toArrayIfNot } from '~/utils/array-utils';
 
-
 export const toastErrorOption: ToastOptions = {
   autoClose: false,
   closeButton: true,
 };
-export const toastError = (err: string | Error | Error[], option: ToastOptions = toastErrorOption): void => {
+export const toastError = (
+  err: string | Error | Error[],
+  option: ToastOptions = toastErrorOption,
+): void => {
   const errs = toArrayIfNot(err);
 
   if (errs.length === 0) {
@@ -16,7 +18,7 @@ export const toastError = (err: string | Error | Error[], option: ToastOptions =
   }
 
   for (const err of errs) {
-    const message = (typeof err === 'string') ? err : err.message;
+    const message = typeof err === 'string' ? err : err.message;
     toast.error(message, option);
   }
 };
@@ -25,7 +27,10 @@ export const toastSuccessOption: ToastOptions = {
   autoClose: 2000,
   closeButton: true,
 };
-export const toastSuccess = (content: ToastContent, option: ToastOptions = toastSuccessOption): void => {
+export const toastSuccess = (
+  content: ToastContent,
+  option: ToastOptions = toastSuccessOption,
+): void => {
   toast.success(content, option);
 };
 
@@ -33,6 +38,9 @@ export const toastWarningOption: ToastOptions = {
   autoClose: 5000,
   closeButton: true,
 };
-export const toastWarning = (content: ToastContent, option: ToastOptions = toastWarningOption): void => {
+export const toastWarning = (
+  content: ToastContent,
+  option: ToastOptions = toastWarningOption,
+): void => {
   toast.warning(content, option);
 };

+ 36 - 30
apps/app/src/client/util/use-input-validator.ts

@@ -1,5 +1,4 @@
 import { useCallback } from 'react';
-
 import { useTranslation } from 'next-i18next';
 
 export const AlertType = {
@@ -7,7 +6,7 @@ export const AlertType = {
   ERROR: 'Error',
 } as const;
 
-export type AlertType = typeof AlertType[keyof typeof AlertType];
+export type AlertType = (typeof AlertType)[keyof typeof AlertType];
 
 export const ValidationTarget = {
   FOLDER: 'folder_name',
@@ -15,42 +14,49 @@ export const ValidationTarget = {
   DEFAULT: 'field',
 };
 
-export type ValidationTarget = typeof ValidationTarget[keyof typeof ValidationTarget];
+export type ValidationTarget =
+  (typeof ValidationTarget)[keyof typeof ValidationTarget];
 
 export type AlertInfo = {
-  type?: AlertType
-  message?: string,
-  target?: string
-}
-
+  type?: AlertType;
+  message?: string;
+  target?: string;
+};
 
 export type InputValidationResult = {
-  type: AlertType
-  typeLabel: string,
-  message: string,
-  target: string
-}
+  type: AlertType;
+  typeLabel: string;
+  message: string;
+  target: string;
+};
 
-export type InputValidator = (input?: string, alertType?: AlertType) => InputValidationResult | void;
+export type InputValidator = (
+  input?: string,
+  alertType?: AlertType,
+) => InputValidationResult | void;
 
-export const useInputValidator = (validationTarget: ValidationTarget = ValidationTarget.DEFAULT): InputValidator => {
+export const useInputValidator = (
+  validationTarget: ValidationTarget = ValidationTarget.DEFAULT,
+): InputValidator => {
   const { t } = useTranslation();
 
-  const inputValidator: InputValidator = useCallback((input?, alertType = AlertType.WARNING) => {
-    if ((input ?? '').trim() === '') {
-      return {
-        target: validationTarget,
-        type: alertType,
-        typeLabel: t(alertType),
-        message: t(
-          'input_validation.message.field_required',
-          { target: t(`input_validation.target.${validationTarget}`) },
-        ),
-      };
-    }
-
-    return;
-  }, [t, validationTarget]);
+  const inputValidator: InputValidator = useCallback(
+    (input?, alertType = AlertType.WARNING) => {
+      if ((input ?? '').trim() === '') {
+        return {
+          target: validationTarget,
+          type: alertType,
+          typeLabel: t(alertType),
+          message: t('input_validation.message.field_required', {
+            target: t(`input_validation.target.${validationTarget}`),
+          }),
+        };
+      }
+
+      return;
+    },
+    [t, validationTarget],
+  );
 
   return inputValidator;
 };

+ 11 - 15
apps/app/src/pages/[[...path]]/page-data-props.ts

@@ -15,7 +15,7 @@ import assert from 'assert';
 import type { HydratedDocument, model } from 'mongoose';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
-import type { PageModel } from '~/server/models/page';
+import type { PageDocument, PageModel } from '~/server/models/page';
 import type {
   IPageRedirect,
   PageRedirectModel,
@@ -193,11 +193,6 @@ export async function getPageDataForInitial(
       };
     }
 
-    // Add user to seen users
-    if (user != null) {
-      await page.seen(user);
-    }
-
     // Handle existing page with valid meta that is not IPageNotFoundInfo
     page.initLatestRevisionField(revisionId);
     const ssrMaxRevisionBodyLength = configManager.getConfig(
@@ -250,15 +245,13 @@ export async function getPageDataForInitial(
 // Page data retrieval for same-route navigation
 export async function getPageDataForSameRoute(
   context: GetServerSidePropsContext,
-): Promise<
-  GetServerSidePropsResult<
-    Pick<CommonEachProps, 'currentPathname'> &
-      Pick<
-        EachProps,
-        'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'
-      >
-  >
-> {
+): Promise<{
+  props: Pick<CommonEachProps, 'currentPathname'> &
+    Pick<EachProps, 'currentPathname' | 'isIdenticalPathPage' | 'redirectFrom'>;
+  internalProps?: {
+    pageId?: string;
+  };
+}> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { user } = req;
 
@@ -298,5 +291,8 @@ export async function getPageDataForSameRoute(
       isIdenticalPathPage: false,
       redirectFrom,
     },
+    internalProps: {
+      pageId: basicPageInfo?._id?.toString(),
+    },
   };
 }

+ 36 - 2
apps/app/src/pages/[[...path]]/server-side-props.ts

@@ -1,5 +1,7 @@
 import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
 
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+
 import { getServerSideBasicLayoutProps } from '../basic-layout-page';
 import {
   getServerSideCommonInitialProps,
@@ -26,6 +28,30 @@ const nextjsRoutingProps = {
   },
 };
 
+/**
+ * Emit page seen event
+ * @param context - Next.js server-side context
+ * @param pageId - Page ID to mark as seen
+ */
+function emitPageSeenEvent(
+  context: GetServerSidePropsContext,
+  pageId?: string,
+): void {
+  if (pageId == null) {
+    return;
+  }
+
+  const req = context.req as CrowiRequest;
+  const { user, crowi } = req;
+
+  if (user == null) {
+    return;
+  }
+
+  const pageEvent = crowi.event('page');
+  pageEvent.emit('seen', pageId, user);
+}
+
 export async function getServerSidePropsForInitial(
   context: GetServerSidePropsContext,
 ): Promise<GetServerSidePropsResult<Stage2InitialProps>> {
@@ -75,6 +101,9 @@ export async function getServerSidePropsForInitial(
     throw new Error('Invalid merged props structure');
   }
 
+  // Add user to seen users
+  emitPageSeenEvent(context, mergedProps.pageWithMeta?.data?._id);
+
   // -- TODO: persist activity
   // await addActivity(context, getActivityAction(mergedProps));
   return mergedResult;
@@ -85,16 +114,21 @@ export async function getServerSidePropsForSameRoute(
 ): Promise<GetServerSidePropsResult<Stage2EachProps>> {
   // -- TODO: :https://redmine.weseek.co.jp/issues/174725
   // Remove getServerSideI18nProps from getServerSidePropsForSameRoute for performance improvement
-  const [i18nPropsResult, pageDataResult] = await Promise.all([
+  const [i18nPropsResult, pageDataForSameRouteResult] = await Promise.all([
     getServerSideI18nProps(context, ['translation']),
     getPageDataForSameRoute(context),
   ]);
 
+  const { props: pageDataProps, internalProps } = pageDataForSameRouteResult;
+
+  // Add user to seen users
+  emitPageSeenEvent(context, internalProps?.pageId);
+
   // -- TODO: persist activity
   // const mergedProps = await mergedResult.props;
   // await addActivity(context, getActivityAction(mergedProps));
   const mergedResult = mergeGetServerSidePropsResults(
-    pageDataResult,
+    { props: pageDataProps },
     i18nPropsResult,
   );
 

+ 30 - 0
apps/app/src/server/service/page/index.ts

@@ -230,6 +230,36 @@ class PageService implements IPageService {
     // createMany
     this.pageEvent.on('createMany', this.pageEvent.onCreateMany);
     this.pageEvent.on('addSeenUsers', this.pageEvent.onAddSeenUsers);
+
+    // seen - mark page as seen by user
+    this.pageEvent.on(
+      'seen',
+      async (pageId: string, user: IUserHasId): Promise<void> => {
+        if (pageId == null || user == null) {
+          logger.warn('onSeen: pageId or user is null');
+          return;
+        }
+
+        try {
+          const Page = mongoose.model<
+            HydratedDocument<PageDocument>,
+            PageModel
+          >('Page');
+
+          const page = await Page.findById(pageId);
+
+          if (page == null) {
+            logger.warn('onSeen: page not found', { pageId });
+            return;
+          }
+
+          await page.seen(user);
+          logger.debug('onSeen: successfully marked page as seen', { pageId });
+        } catch (err) {
+          logger.error('onSeen: failed to mark page as seen', err);
+        }
+      },
+    );
   }
 
   getEventEmitter(): EventEmitter {

+ 11 - 11
apps/app/src/states/page/hooks.ts

@@ -121,24 +121,24 @@ export const useIsEditable = () => {
   const isGuestUser = useIsGuestUser();
   const isReadOnlyUser = useIsReadOnlyUser();
   const isNotCreatable = useIsNotCreatable();
-
-  const getCombinedConditions = useAtomCallback(
-    useCallback((get) => {
-      const isForbidden = get(isForbiddenAtom);
-      const isIdenticalPath = get(isIdenticalPathAtom);
-
-      return !isForbidden && !isIdenticalPath;
-    }, []),
-  );
+  const isForbidden = useAtomValue(isForbiddenAtom);
+  const isIdenticalPath = useAtomValue(isIdenticalPathAtom);
 
   return useMemo(() => {
     return (
       !isGuestUser &&
       !isReadOnlyUser &&
       !isNotCreatable &&
-      getCombinedConditions()
+      !isForbidden &&
+      !isIdenticalPath
     );
-  }, [getCombinedConditions, isGuestUser, isReadOnlyUser, isNotCreatable]);
+  }, [
+    isGuestUser,
+    isReadOnlyUser,
+    isNotCreatable,
+    isForbidden,
+    isIdenticalPath,
+  ]);
 };
 
 /**

+ 0 - 2
apps/app/src/states/page/use-fetch-current-page.ts

@@ -241,8 +241,6 @@ export const useFetchCurrentPage = (): {
           const { data } = await apiv3Get<FetchedPageResult>('/page', params);
           const { page: newData, meta } = data;
 
-          console.log('Fetched page data:', { newData, meta });
-
           set(currentPageDataAtom, newData ?? undefined);
           set(currentPageEntityIdAtom, newData?._id);
           set(

+ 1 - 6
apps/app/src/states/ui/page-abilities.ts

@@ -110,12 +110,7 @@ export const useIsAbleToChangeEditorMode = (): boolean => {
   const isEditable = useIsEditable();
   const isSharedUser = useIsSharedUser();
 
-  const includesUndefined = [isEditable, isSharedUser].some(
-    (v) => v === undefined,
-  );
-  if (includesUndefined) return false;
-
-  return !!isEditable && !isSharedUser;
+  return isEditable && !isSharedUser;
 };
 
 /**

+ 16 - 23
apps/app/src/states/ui/sidebar/hydrate.ts

@@ -20,29 +20,22 @@ export const useHydrateSidebarAtoms = (
   sidebarConfig?: ISidebarConfig,
   userUISettings?: IUserUISettings,
 ): void => {
-  useHydrateAtoms(
-    sidebarConfig == null || userUISettings == null
-      ? []
-      : [
-          // Use user preference from DB if available, otherwise use system default
-          [
-            preferCollapsedModeAtom,
-            userUISettings?.preferCollapsedModeByUser ??
-              sidebarConfig?.isSidebarCollapsedMode ??
-              false,
-          ],
+  useHydrateAtoms([
+    // Use user preference from DB if available, otherwise use system default
+    [
+      preferCollapsedModeAtom,
+      userUISettings?.preferCollapsedModeByUser ??
+        sidebarConfig?.isSidebarCollapsedMode ??
+        false,
+    ],
 
-          // Sidebar contents type (with default fallback)
-          [
-            currentSidebarContentsAtom,
-            userUISettings?.currentSidebarContents ?? SidebarContentsType.TREE,
-          ],
+    // Sidebar contents type (with default fallback)
+    [
+      currentSidebarContentsAtom,
+      userUISettings?.currentSidebarContents ?? SidebarContentsType.TREE,
+    ],
 
-          // Product navigation width (with default fallback)
-          [
-            currentProductNavWidthAtom,
-            userUISettings?.currentProductNavWidth ?? 320,
-          ],
-        ],
-  );
+    // Product navigation width (with default fallback)
+    [currentProductNavWidthAtom, userUISettings?.currentProductNavWidth ?? 320],
+  ]);
 };

+ 1 - 3
biome.json

@@ -28,9 +28,7 @@
       "!apps/slackbot-proxy/src/public/bootstrap",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs",
-      "!apps/app/src/client/components",
-      "!apps/app/src/client/services",
-      "!apps/app/src/client/util"
+      "!apps/app/src/client/components"
     ]
   },
   "formatter": {