浏览代码

[ui-core] feat: add transformKey to useSaveData hook (#4230)

Ram Prasad Agarwal 3 月之前
父节点
当前提交
3b2068bf52
共有 19 个文件被更改,包括 99 次插入40 次删除
  1. 2 2
      desktop/core/src/desktop/js/apps/admin/Overview/Analytics.tsx
  2. 3 3
      desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.test.tsx
  3. 1 1
      desktop/core/src/desktop/js/apps/newimporter/ImporterPage.test.tsx
  4. 1 1
      desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/LocalFileUploadOption.test.tsx
  5. 1 1
      desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/LocalFileUploadOption.tsx
  6. 1 1
      desktop/core/src/desktop/js/apps/newimporter/types.ts
  7. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.tsx
  8. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.tsx
  9. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx
  10. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/RenameModal/RenameModal.tsx
  11. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ReplicationModal/ReplicationModal.tsx
  12. 1 3
      desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.tsx
  13. 1 1
      desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx
  14. 7 7
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.test.tsx
  15. 1 1
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.ts
  16. 4 4
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useRegularUpload.test.tsx
  17. 1 1
      desktop/core/src/desktop/js/utils/hooks/useFileUpload/useRegularUpload.ts
  18. 49 2
      desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.test.tsx
  19. 21 7
      desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts

+ 2 - 2
desktop/core/src/desktop/js/apps/admin/Overview/Analytics.tsx

@@ -44,11 +44,11 @@ const Analytics = (): JSX.Element => {
     save: updateAnalyticsPreference,
     loading: updatingAnalyticsPreference,
     error: updateAnalyticsPreferenceError
-  } = useSaveData<{ collect_usage: boolean }>(USAGE_ANALYTICS_API_URL, {
+  } = useSaveData<UsageAnalyticsResponse>(USAGE_ANALYTICS_API_URL, {
     method: HttpMethod.PUT,
     onSuccess: response => {
       reloadData();
-      const successMessage = response.collect_usage
+      const successMessage = response.collectUsage
         ? t('Analytics have been activated.')
         : t('Analytics have been deactivated.');
       huePubSub.publish<HueAlert>(GLOBAL_INFO_TOPIC, { message: successMessage });

+ 3 - 3
desktop/core/src/desktop/js/apps/admin/Overview/OverviewTab.test.tsx

@@ -125,7 +125,7 @@ describe('OverviewTab', () => {
       silenceErrors: true,
       ignoreSuccessErrors: true
     };
-    const expectedPostOptions = {
+    const expectedOptions = {
       silenceErrors: true,
       ignoreSuccessErrors: true,
       qsEncodeData: false
@@ -213,7 +213,7 @@ describe('OverviewTab', () => {
           'put',
           USAGE_ANALYTICS_API_URL,
           { collect_usage: true },
-          expectedPostOptions
+          expectedOptions
         );
       });
     });
@@ -229,7 +229,7 @@ describe('OverviewTab', () => {
           'put',
           USAGE_ANALYTICS_API_URL,
           { collect_usage: false },
-          expectedPostOptions
+          expectedOptions
         );
       });
     });

+ 1 - 1
desktop/core/src/desktop/js/apps/newimporter/ImporterPage.test.tsx

@@ -107,7 +107,7 @@ describe('ImporterPage', () => {
       const saveCall = mockSave.mock.calls[0];
       if (saveCall && saveCall[1] && saveCall[1].onSuccess) {
         saveCall[1].onSuccess({
-          file_path: '/tmp/test.csv'
+          filePath: '/tmp/test.csv'
         });
       }
     });

+ 1 - 1
desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/LocalFileUploadOption.test.tsx

@@ -109,7 +109,7 @@ describe('LocalFileUploadOption', () => {
   it('should upload file successfully and sets metadata', () => {
     mockSave.mockImplementation((_data, { onSuccess }) => {
       onSuccess({
-        file_path: '/tmp/data.csv',
+        filePath: '/tmp/data.csv',
         fileName: 'data.csv',
         source: ImporterFileSource.LOCAL
       });

+ 1 - 1
desktop/core/src/desktop/js/apps/newimporter/ImporterSourceSelector/LocalFileUploadOption.tsx

@@ -62,7 +62,7 @@ const LocalFileUploadOption = ({
       upload(payload, {
         onSuccess: data => {
           setFileMetaData({
-            path: data.file_path,
+            path: data.filePath,
             fileName: file.name,
             source: ImporterFileSource.LOCAL
           });

+ 1 - 1
desktop/core/src/desktop/js/apps/newimporter/types.ts

@@ -41,7 +41,7 @@ export enum StoreLocation {
 }
 
 export interface LocalFileUploadResponse {
-  file_path: string;
+  filePath: string;
 }
 
 export interface FileFormatResponse {

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/FileChooserModal/FileChooserModal.tsx

@@ -201,7 +201,7 @@ const FileChooserModal = ({
     loading: createFolderLoading,
     error: createFolderError
   } = useSaveData(CREATE_DIRECTORY_API_URL, {
-    postOptions: { qsEncodeData: true } // TODO: Remove once API supports RAW JSON payload
+    options: { qsEncodeData: true } // TODO: Remove once API supports RAW JSON payload
   });
 
   const handleCreate = () => {

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/CreateAndUpload/CreateAndUploadAction.tsx

@@ -71,7 +71,7 @@ const CreateAndUploadAction = ({
   };
 
   const { save, loading, error } = useSaveData(undefined, {
-    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
+    options: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     onSuccess: onApiSuccess,
     onError: onActionError
   });

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ExtractionModal/ExtractionModal.tsx

@@ -43,7 +43,7 @@ const ExtractionModal = ({
 
   const { save, loading, error } = useSaveData(EXTRACT_API_URL, {
     // TODO: Remove qsEncodeData once API supports RAW JSON payload
-    postOptions: { qsEncodeData: true },
+    options: { qsEncodeData: true },
     skip: !file,
     onSuccess,
     onError

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/RenameModal/RenameModal.tsx

@@ -39,7 +39,7 @@ const RenameModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save, loading, error } = useSaveData(RENAME_API_URL, {
-    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
+    options: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     skip: !file.path,
     onSuccess,
     onError

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/FileAndFolder/ReplicationModal/ReplicationModal.tsx

@@ -39,7 +39,7 @@ const ReplicationModal = ({
   const { t } = i18nReact.useTranslation();
 
   const { save, loading, error } = useSaveData(SET_REPLICATION_API_URL, {
-    postOptions: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
+    options: { qsEncodeData: true }, // TODO: Remove once API supports RAW JSON payload
     skip: !file.path,
     onSuccess,
     onError

+ 1 - 3
desktop/core/src/desktop/js/apps/storageBrowser/StorageDirectoryPage/StorageDirectoryActions/Trash/TrashActions.tsx

@@ -49,9 +49,7 @@ const TrashActions = ({
   const [selectedAction, setSelectedAction] = useState<Actions | undefined>();
 
   const { save, loading } = useSaveData(undefined, {
-    onSuccess: () => {
-      setIsModalOpen(false);
-    },
+    onSuccess: () => setIsModalOpen(false),
     onError: onActionError
   });
 

+ 1 - 1
desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.tsx

@@ -54,7 +54,7 @@ const StorageFilePage = ({ fileStats, onReload }: StorageFilePageProps): JSX.Ele
   const pageOffset = (pageNumber - 1) * pageSize;
 
   const { loading: isSaving, save } = useSaveData(SAVE_FILE_API_URL, {
-    postOptions: { qsEncodeData: true } // TODO: Remove once API supports RAW JSON payload
+    options: { qsEncodeData: true } // TODO: Remove once API supports RAW JSON payload
   });
 
   const { data, loading, error } = useLoadData<FilePreview>(FILE_PREVIEW_API_URL, {

+ 7 - 7
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.test.tsx

@@ -305,10 +305,10 @@ describe('useChunkUpload', () => {
 
   it('should handle successful chunk upload and progress', async () => {
     mockSave
-      .mockImplementationOnce((_, { onSuccess, postOptions }) => {
-        if (postOptions?.onUploadProgress) {
-          postOptions.onUploadProgress({ loaded: 50, total: 100 });
-          postOptions.onUploadProgress({ loaded: 100, total: 100 });
+      .mockImplementationOnce((_, { onSuccess, options }) => {
+        if (options?.onUploadProgress) {
+          options.onUploadProgress({ loaded: 50, total: 100 });
+          options.onUploadProgress({ loaded: 100, total: 100 });
         }
         onSuccess();
       })
@@ -558,9 +558,9 @@ describe('useChunkUpload', () => {
   it('should handle progress updates correctly', async () => {
     const mockProgressEvent = { loaded: 75, total: 100 };
 
-    mockSave.mockImplementationOnce((_, { onSuccess, postOptions }) => {
-      if (postOptions?.onUploadProgress) {
-        postOptions.onUploadProgress(mockProgressEvent);
+    mockSave.mockImplementationOnce((_, { onSuccess, options }) => {
+      if (options?.onUploadProgress) {
+        options.onUploadProgress(mockProgressEvent);
       }
       onSuccess();
     });

+ 1 - 1
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useChunkUpload.ts

@@ -136,7 +136,7 @@ const useChunkUpload = ({
       url,
       onSuccess: onChunkUploadSuccess(chunk),
       onError: error => updateFileVariables(chunk.uuid, { status: FileStatus.Failed, error }),
-      postOptions: { onUploadProgress: onUploadProgress(chunk) }
+      options: { onUploadProgress: onUploadProgress(chunk) }
     });
   };
 

+ 4 - 4
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useRegularUpload.test.tsx

@@ -116,14 +116,14 @@ describe('useRegularUpload', () => {
 
     // Params: destination_path and overwrite=false
     expect(optionsArg).toBeDefined();
-    expect(optionsArg.postOptions).toBeDefined();
-    expect(optionsArg.postOptions.params).toEqual({
+    expect(optionsArg.options).toBeDefined();
+    expect(optionsArg.options.params).toEqual({
       destination_path: '/uploads/',
       overwrite: 'false'
     });
     expect(typeof optionsArg.onSuccess).toBe('function');
     expect(typeof optionsArg.onError).toBe('function');
-    expect(typeof optionsArg.postOptions.onUploadProgress).toBe('function');
+    expect(typeof optionsArg.options.onUploadProgress).toBe('function');
 
     expect(mockUpdateFileVariables).toHaveBeenCalledWith(mockFile.uuid, {
       status: FileStatus.Uploading
@@ -153,7 +153,7 @@ describe('useRegularUpload', () => {
     expect(formDataArg.has('overwrite')).toBe(false);
 
     // Params: destination_path and overwrite=true
-    expect(optionsArg.postOptions.params).toEqual({
+    expect(optionsArg.options.params).toEqual({
       destination_path: '/uploads/',
       overwrite: 'true'
     });

+ 1 - 1
desktop/core/src/desktop/js/utils/hooks/useFileUpload/useRegularUpload.ts

@@ -53,7 +53,7 @@ const useRegularUpload = ({
       onError: error => {
         updateFileVariables(item.uuid, { status: FileStatus.Failed, error });
       },
-      postOptions: {
+      options: {
         params: {
           destination_path: item.filePath,
           overwrite: item.overwrite ? 'true' : 'false'

+ 49 - 2
desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.test.tsx

@@ -305,12 +305,12 @@ describe('useSaveData', () => {
     });
   });
 
-  it('should prioritize qsEncodeData from saveOptions.postOptions', async () => {
+  it('should prioritize qsEncodeData from saveOptions.options', async () => {
     const payload = new FormData();
 
     const { result } = renderHook(() =>
       useSaveData(mockUrl, {
-        postOptions: { qsEncodeData: true }
+        options: { qsEncodeData: true }
       })
     );
 
@@ -414,4 +414,51 @@ describe('useSaveData', () => {
       expect(result.current.loading).toBe(false);
     });
   });
+
+  it('should transform response keys to camelCase by default', async () => {
+    const mockRawData = { product_id: 1, product_name: 'Hue' };
+    const mockCamelCaseData = { productId: 1, productName: 'Hue' };
+    mockSendApiRequest.mockResolvedValue(mockRawData);
+
+    const { result } = renderHook(() => useSaveData(mockUrl));
+
+    act(() => {
+      result.current.save(mockBody);
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mockCamelCaseData);
+    });
+  });
+
+  it('should transform response keys to camelCase when transformKeys is "camelCase"', async () => {
+    const mockRawData = { product_id: 1, product_name: 'Hue' };
+    const mockCamelCaseData = { productId: 1, productName: 'Hue' };
+    mockSendApiRequest.mockResolvedValue(mockRawData);
+
+    const { result } = renderHook(() => useSaveData(mockUrl, { transformKeys: 'camelCase' }));
+
+    act(() => {
+      result.current.save(mockBody);
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mockCamelCaseData);
+    });
+  });
+
+  it('should not transform response keys when transformKeys is "none"', async () => {
+    const mockRawData = { product_id: 1, product_name: 'Hue' };
+    mockSendApiRequest.mockResolvedValue(mockRawData);
+
+    const { result } = renderHook(() => useSaveData(mockUrl, { transformKeys: 'none' }));
+
+    act(() => {
+      result.current.save(mockBody);
+    });
+
+    await waitFor(() => {
+      expect(result.current.data).toEqual(mockRawData);
+    });
+  });
 });

+ 21 - 7
desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.ts

@@ -18,13 +18,15 @@ import { useCallback, useEffect, useState } from 'react';
 import { AxiosError } from 'axios';
 import { ApiFetchOptions, HttpMethod, sendApiRequest } from '../../../api/utils';
 import { isJSON } from '../../jsonUtils';
+import { convertKeysToCamelCase } from '../../../utils/string/changeCasing';
 
 interface saveOptions<T, E> {
   url?: string;
   method?: HttpMethod;
   onSuccess?: (data: T) => void;
   onError?: (error: AxiosError) => void;
-  postOptions?: ApiFetchOptions<T, E>;
+  options?: ApiFetchOptions<T, E>;
+  transformKeys?: 'camelCase' | 'none';
 }
 
 export interface Options<T, E> extends saveOptions<T, E> {
@@ -52,6 +54,18 @@ const useSaveData = <T, U = unknown, E = string>(
     ignoreSuccessErrors: true
   };
 
+  const transformResponse = (data: T): T => {
+    if (options?.transformKeys === 'none') {
+      return data;
+    }
+
+    if (data && (options?.transformKeys === undefined || options?.transformKeys === 'camelCase')) {
+      return convertKeysToCamelCase<T>(data);
+    }
+
+    return data;
+  };
+
   const saveData = useCallback(
     async (body: U, saveOptions?: saveOptions<T, E>) => {
       // Avoid sending data if the skip option is true
@@ -66,21 +80,21 @@ const useSaveData = <T, U = unknown, E = string>(
       const requestOptions = {
         ...requestOptionsDefault,
         qsEncodeData: body instanceof FormData || isJSON(body) ? false : true,
-        ...localOptions?.postOptions,
-        ...saveOptions?.postOptions
+        ...localOptions?.options,
+        ...saveOptions?.options
       };
 
       const method = saveOptions?.method ?? localOptions?.method ?? HttpMethod.POST;
 
       try {
         const response = await sendApiRequest(method, apiUrl, body, requestOptions);
-
-        setData(response);
+        const transformedResponse = transformResponse(response);
+        setData(transformedResponse);
         if (saveOptions?.onSuccess) {
-          saveOptions.onSuccess(response);
+          saveOptions.onSuccess(transformedResponse);
         }
         if (localOptions?.onSuccess) {
-          localOptions.onSuccess(response);
+          localOptions.onSuccess(transformedResponse);
         }
       } catch (error) {
         setError(error as E);