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

WIP: refactor SWR hooks for PersonalSettings

Yuki Takei 6 месяцев назад
Родитель
Сommit
59e34bb12f

+ 2 - 3
apps/app/src/client/components/Me/ApiTokenSettings.tsx

@@ -6,14 +6,13 @@ import {
   apiv3Put,
 } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { usePersonalSettings, useSWRxPersonalSettings } from '~/stores/personal-settings';
+import { useSWRxPersonalSettings } from '~/stores/personal-settings';
 
 
 export const ApiTokenSettings = React.memo((): JSX.Element => {
 
   const { t } = useTranslation();
-  const { mutate: mutateDatabaseData } = useSWRxPersonalSettings();
-  const { data: personalSettingsData } = usePersonalSettings();
+  const { data: personalSettingsData, mutate: mutateDatabaseData } = useSWRxPersonalSettings();
 
   const submitHandler = useCallback(async() => {
 

+ 2 - 2
apps/app/src/client/components/Me/AssociateModal.tsx

@@ -13,7 +13,7 @@ import {
 } from 'reactstrap';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
+import { useAssociateLdapAccount, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 import { LdapAuthTest } from '../Admin/Security/LdapAuthTest';
 
@@ -25,7 +25,7 @@ type Props = {
 const AssociateModal = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { mutate: mutatePersonalExternalAccounts } = useSWRxPersonalExternalAccounts();
-  const { associateLdapAccount } = usePersonalSettings();
+  const { trigger: associateLdapAccount, isMutating } = useAssociateLdapAccount();
   const [activeTab, setActiveTab] = useState(1);
   const { isOpen, onClose } = props;
 

+ 31 - 18
apps/app/src/client/components/Me/BasicInfoSettings.tsx

@@ -1,5 +1,6 @@
-import React, { type JSX } from 'react';
+import React, { useState, useEffect, type JSX } from 'react';
 
+import type { IUser } from '@growi/core/dist/interfaces';
 import { useAtomValue } from 'jotai';
 import { useTranslation, i18n } from 'next-i18next';
 
@@ -7,22 +8,34 @@ import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { registrationWhitelistAtom } from '~/states/server-configurations';
-import { usePersonalSettings } from '~/stores/personal-settings';
+import { useSWRxPersonalSettings, useUpdateBasicInfo } from '~/stores/personal-settings';
 
 export const BasicInfoSettings = (): JSX.Element => {
   const { t } = useTranslation();
   const registrationWhitelist = useAtomValue(registrationWhitelistAtom);
 
   const {
-    data: personalSettingsInfo, mutate: mutatePersonalSettings, sync, updateBasicInfo, error,
-  } = usePersonalSettings();
+    data: personalSettingsInfo, error,
+  } = useSWRxPersonalSettings();
 
+  // Form state management
+  const [formData, setFormData] = useState<IUser | null>(null);
 
-  const submitHandler = async() => {
+  // Sync form data with server data
+  useEffect(() => {
+    if (personalSettingsInfo != null) {
+      setFormData(personalSettingsInfo);
+    }
+  }, [personalSettingsInfo]);
+
+  const { trigger: updateBasicInfo, isMutating } = useUpdateBasicInfo();
 
+  const submitHandler = async() => {
     try {
-      await updateBasicInfo();
-      sync();
+      if (formData == null) {
+        throw new Error('personalSettingsInfo is not loaded');
+      }
+      await updateBasicInfo(formData);
       toastSuccess(t('toaster.update_successed', { target: t('Basic Info'), ns: 'commons' }));
     }
     catch (errs) {
@@ -39,11 +52,11 @@ export const BasicInfoSettings = (): JSX.Element => {
     }
   };
 
-  const changePersonalSettingsHandler = (updateData) => {
-    if (personalSettingsInfo == null) {
+  const changePersonalSettingsHandler = (updateData: Partial<IUser>) => {
+    if (formData == null) {
       return;
     }
-    mutatePersonalSettings({ ...personalSettingsInfo, ...updateData });
+    setFormData({ ...formData, ...updateData });
   };
 
 
@@ -57,7 +70,7 @@ export const BasicInfoSettings = (): JSX.Element => {
             className="form-control"
             type="text"
             name="userForm[name]"
-            defaultValue={personalSettingsInfo?.name || ''}
+            value={formData?.name || ''}
             onChange={e => changePersonalSettingsHandler({ name: e.target.value })}
           />
         </div>
@@ -70,7 +83,7 @@ export const BasicInfoSettings = (): JSX.Element => {
             className="form-control"
             type="text"
             name="userForm[email]"
-            defaultValue={personalSettingsInfo?.email || ''}
+            value={formData?.email || ''}
             onChange={e => changePersonalSettingsHandler({ email: e.target.value })}
           />
           {registrationWhitelist != null && registrationWhitelist.length !== 0 && (
@@ -93,7 +106,7 @@ export const BasicInfoSettings = (): JSX.Element => {
               id="radioEmailShow"
               className="form-check-input"
               name="userForm[isEmailPublished]"
-              checked={personalSettingsInfo?.isEmailPublished === true}
+              checked={formData?.isEmailPublished === true}
               onChange={() => changePersonalSettingsHandler({ isEmailPublished: true })}
             />
             <label className="form-label form-check-label mb-0" htmlFor="radioEmailShow">{t('Show')}</label>
@@ -104,7 +117,7 @@ export const BasicInfoSettings = (): JSX.Element => {
               id="radioEmailHide"
               className="form-check-input"
               name="userForm[isEmailPublished]"
-              checked={personalSettingsInfo?.isEmailPublished === false}
+              checked={formData?.isEmailPublished === false}
               onChange={() => changePersonalSettingsHandler({ isEmailPublished: false })}
             />
             <label className="form-label form-check-label mb-0" htmlFor="radioEmailHide">{t('Hide')}</label>
@@ -127,8 +140,8 @@ export const BasicInfoSettings = (): JSX.Element => {
                     id={`radioLang${locale}`}
                     className="form-check-input"
                     name="userForm[lang]"
-                    checked={personalSettingsInfo?.lang === locale}
-                    onChange={() => changePersonalSettingsHandler({ lang: locale })}
+                    checked={formData?.lang === locale}
+                    onChange={() => changePersonalSettingsHandler({ lang: locale as IUser['lang'] })}
                   />
                   <label className="form-label form-check-label mb-0" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name') as string}</label>
                 </div>
@@ -145,7 +158,7 @@ export const BasicInfoSettings = (): JSX.Element => {
             type="text"
             key="slackMemberId"
             name="userForm[slackMemberId]"
-            defaultValue={personalSettingsInfo?.slackMemberId || ''}
+            value={formData?.slackMemberId || ''}
             onChange={e => changePersonalSettingsHandler({ slackMemberId: e.target.value })}
           />
         </div>
@@ -158,7 +171,7 @@ export const BasicInfoSettings = (): JSX.Element => {
             type="button"
             className="btn btn-primary"
             onClick={submitHandler}
-            disabled={error != null}
+            disabled={error != null || isMutating || formData == null}
           >
             {t('Update')}
           </button>

+ 2 - 2
apps/app/src/client/components/Me/DisassociateModal.tsx

@@ -11,7 +11,7 @@ import {
 
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
-import { usePersonalSettings, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
+import { useDisassociateLdapAccount, useSWRxPersonalExternalAccounts } from '~/stores/personal-settings';
 
 type Props = {
   isOpen: boolean,
@@ -24,7 +24,7 @@ const DisassociateModal = (props: Props): JSX.Element => {
 
   const { t } = useTranslation();
   const { mutate: mutatePersonalExternalAccounts } = useSWRxPersonalExternalAccounts();
-  const { disassociateLdapAccount } = usePersonalSettings();
+  const { trigger: disassociateLdapAccount } = useDisassociateLdapAccount();
 
   const { providerType, accountId } = props.accountForDisassociate;
 

+ 2 - 2
apps/app/src/client/components/Me/PasswordSettings.jsx

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { usePersonalSettings } from '~/stores/personal-settings';
+import { useSWRxPersonalSettings } from '~/stores/personal-settings';
 
 
 class PasswordSettings extends React.Component {
@@ -160,7 +160,7 @@ PasswordSettings.propTypes = {
 
 const PasswordSettingsWrapperFC = (props) => {
   const { t } = useTranslation();
-  const { mutate: mutatePersonalSettings } = usePersonalSettings();
+  const { mutate: mutatePersonalSettings } = useSWRxPersonalSettings();
 
   const submitHandler = useCallback(() => {
     mutatePersonalSettings();

+ 2 - 2
apps/app/src/client/components/PageEditor/DrawioModal.tsx

@@ -14,7 +14,7 @@ import { replaceFocusedDrawioWithEditor, getMarkdownDrawioMxfile } from '~/clien
 import { useRendererConfig } from '~/states/server-configurations';
 import { useDrawioModalActions, useDrawioModalStatus } from '~/states/ui/modal/drawio';
 import { useDrawioModalForEditorStatus, useDrawioModalForEditorActions } from '~/states/ui/modal/drawio-for-editor';
-import { usePersonalSettings } from '~/stores/personal-settings';
+import { useSWRxPersonalSettings } from '~/stores/personal-settings';
 import loggerFactory from '~/utils/logger';
 
 
@@ -56,7 +56,7 @@ const drawioConfig: DrawioConfig = {
 
 const DrawioModalSubstance = (): JSX.Element => {
   const { drawioUri } = useRendererConfig();
-  const { data: personalSettingsInfo } = usePersonalSettings({
+  const { data: personalSettingsInfo } = useSWRxPersonalSettings({
     // make immutable
     revalidateIfStale: false,
     revalidateOnFocus: false,

+ 2 - 2
apps/app/src/client/components/TemplateModal/TemplateModal.tsx

@@ -23,7 +23,7 @@ import {
 } from 'reactstrap';
 
 import { useSWRxTemplate, useSWRxTemplates } from '~/features/templates/stores';
-import { usePersonalSettings } from '~/stores/personal-settings';
+import { useSWRxPersonalSettings } from '~/stores/personal-settings';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
@@ -120,7 +120,7 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
 
   const { t } = useTranslation(['translation', 'commons']);
 
-  const { data: personalSettingsInfo } = usePersonalSettings();
+  const { data: personalSettingsInfo } = useSWRxPersonalSettings();
   const { data: rendererOptions } = usePreviewOptions();
   const { data: templateSummaries, isLoading } = useSWRxTemplates();
 

+ 58 - 76
apps/app/src/stores/personal-settings.tsx

@@ -4,23 +4,18 @@ import type { HasObjectId, IExternalAccount, IUser } from '@growi/core/dist/inte
 import { useTranslation } from 'next-i18next';
 import type { SWRConfiguration, SWRResponse } from 'swr';
 import useSWR from 'swr';
+import useSWRMutation from 'swr/mutation';
 
 import type {
   IResGenerateAccessToken, IResGetAccessToken, IAccessTokenInfo,
 } from '~/interfaces/access-token';
 import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
 import { useIsGuestUser } from '~/states/context';
-import loggerFactory from '~/utils/logger';
 
 import {
   apiv3Delete, apiv3Get, apiv3Put, apiv3Post,
 } from '../client/util/apiv3-client';
 
-import { useStaticSWR } from './use-static-swr';
-
-
-const logger = loggerFactory('growi:stores:personal-settings');
-
 
 export const useSWRxPersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> => {
   const isGuestUser = useIsGuestUser();
@@ -34,81 +29,68 @@ export const useSWRxPersonalSettings = (config?: SWRConfiguration): SWRResponse<
   );
 };
 
-export type IPersonalSettingsInfoOption = {
-  sync: () => void,
-  updateBasicInfo: () => Promise<void>,
-  associateLdapAccount: (account: { username: string, password: string }) => Promise<void>,
-  disassociateLdapAccount: (account: { providerType: IExternalAuthProviderType, accountId: string }) => Promise<void>,
-}
-
-export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
+/**
+ * Hook for updating basic user information using SWR Mutation
+ * This hook returns a trigger function that updates the user's basic info
+ * and automatically updates the SWR cache after successful mutation.
+ */
+export const useUpdateBasicInfo = () => {
   const { i18n } = useTranslation();
-  const { data: personalSettingsDataFromDB, mutate: revalidate } = useSWRxPersonalSettings(config);
-  const key = personalSettingsDataFromDB != null ? 'personalSettingsInfo' : null;
-
-  const swrResult = useStaticSWR<IUser, Error>(key, undefined, { fallbackData: personalSettingsDataFromDB });
 
-  // Sync with database
-  const sync = async(): Promise<void> => {
-    const { mutate } = swrResult;
-    const result = await revalidate();
-    mutate(result);
-  };
-
-  const updateBasicInfo = async(): Promise<void> => {
-    const { data } = swrResult;
-
-    if (data == null) {
-      return;
-    }
-
-    const updateData = {
-      name: data.name,
-      email: data.email,
-      isEmailPublished: data.isEmailPublished,
-      lang: data.lang,
-      slackMemberId: data.slackMemberId,
-    };
-
-    // invoke API
-    try {
-      await apiv3Put('/personal-setting/', updateData);
+  return useSWRMutation(
+    '/personal-setting',
+    async (_key, { arg }: { arg: IUser }) => {
+      const updateData = {
+        name: arg.name,
+        email: arg.email,
+        isEmailPublished: arg.isEmailPublished,
+        lang: arg.lang,
+        slackMemberId: arg.slackMemberId,
+      };
+
+      const response = await apiv3Put<{ currentUser: IUser }>('/personal-setting/', updateData);
       i18n.changeLanguage(updateData.lang);
-    }
-    catch (errs) {
-      logger.error(errs);
-      throw errs;
-    }
-  };
-
-
-  const associateLdapAccount = async(account): Promise<void> => {
-    try {
-      await apiv3Put('/personal-setting/associate-ldap', account);
-    }
-    catch (err) {
-      logger.error(err);
-      throw new Error('Failed to associate ldap account');
-    }
-  };
+      return response.data.currentUser;
+    },
+    {
+      populateCache: true, // Update SWR cache with the result
+      revalidate: false, // No need to revalidate since we're populating the cache
+    },
+  );
+};
 
-  const disassociateLdapAccount = async(account): Promise<void> => {
-    try {
-      await apiv3Put('/personal-setting/disassociate-ldap', account);
-    }
-    catch (err) {
-      logger.error(err);
-      throw new Error('Failed to disassociate ldap account');
-    }
-  };
+/**
+ * Hook for associating LDAP account using SWR Mutation
+ */
+export const useAssociateLdapAccount = () => {
+  return useSWRMutation(
+    '/personal-setting',
+    async (_key, { arg }: { arg: { username: string, password: string } }) => {
+      const response = await apiv3Put<{ currentUser: IUser }>('/personal-setting/associate-ldap', arg);
+      return response.data.currentUser;
+    },
+    {
+      populateCache: true,
+      revalidate: false,
+    },
+  );
+};
 
-  return {
-    ...swrResult,
-    sync,
-    updateBasicInfo,
-    associateLdapAccount,
-    disassociateLdapAccount,
-  };
+/**
+ * Hook for disassociating LDAP account using SWR Mutation
+ */
+export const useDisassociateLdapAccount = () => {
+  return useSWRMutation(
+    '/personal-setting',
+    async (_key, { arg }: { arg: { providerType: IExternalAuthProviderType, accountId: string } }) => {
+      const response = await apiv3Put<{ currentUser: IUser }>('/personal-setting/disassociate-ldap', arg);
+      return response.data.currentUser;
+    },
+    {
+      populateCache: true,
+      revalidate: false,
+    },
+  );
 };
 
 export const useSWRxPersonalExternalAccounts = (): SWRResponse<(IExternalAccount<IExternalAuthProviderType> & HasObjectId)[], Error> => {

+ 0 - 6
apps/app/src/stores/use-static-swr.ts

@@ -1,6 +0,0 @@
-import { useSWRStatic } from '@growi/core/dist/swr';
-
-/**
- * @deprecated Import { useSWRStatic } from '@growi/core/dist/swr' instead.
- */
-export const useStaticSWR = useSWRStatic;