From 503a5029daee8c3c429c1c3000158994893d050f Mon Sep 17 00:00:00 2001 From: Paul Rastoin <45004772+prastoin@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:50:41 +0100 Subject: [PATCH] Refactor `twenty-front` metadata api services for v2 (#15360) # Introduction Please first review this PR initial base https://github.com/twentyhq/twenty/pull/15358 In a nutshell refactored the frontend fetchers to display v2 errors format smoothly Please note that the v2 now finished the whole validation and does fail fast anymore ( summary is hardcoded for the moment ) ```json [ { "extensions": { "code": "BAD_USER_INPUT", "errors": { "cronTrigger": [], "databaseEventTrigger": [], "fieldMetadata": [ { "errors": [ { "code": "INVALID_FIELD_INPUT", "message": "Default value should be as quoted string", "value": "", }, { "code": "INVALID_FIELD_INPUT", "message": "Default value "" must be one of the option values", "value": "", }, ], "flatEntityMinimalInformation": { "id": Any, "name": "testField", "objectMetadataId": Any, }, "status": "fail", "type": "create_field", }, ], "index": [], "objectMetadata": [], "routeTrigger": [], "serverlessFunction": [], "view": [], "viewField": [], "viewFilter": [], "viewGroup": [], }, "message": "Validation failed for 0 object(s) and 0 field(s)", "summary": { "invalidCronTrigger": 0, "invalidDatabaseEventTrigger": 0, "invalidFieldMetadata": 0, "invalidIndex": 0, "invalidObjectMetadata": 0, "invalidRouteTrigger": 0, "invalidServerlessFunction": 0, "invalidView": 0, "invalidViewField": 0, "invalidViewFilter": 0, "invalidViewGroup": 0, "totalErrors": 0, }, "userFriendlyMessage": "Validation failed for 0 object(s) and 0 field(s)", }, "message": "Multiple validation errors occurred while creating fields", "name": "GraphQLError", }, ] ``` ## What's done - `usePersistView` tool ( CRUD ) - renamed `usePersistViewX` tools accordingly ( no more records or core ) - Now catching a lot of before unhandled exceptions - refactored each services to handle their own exception handlers and return either the response or the error within a discriminated union record ## Result ### Primary entity error When performing an metadata operation on a given metadata, if validation errors occurs we will display each of them in a toast Here while creating an object metadata. image ### Related entity error Still while creating an object image ### Translated image ## Conclusion This PR is an extract of https://github.com/twentyhq/twenty/pull/15331 close https://github.com/twentyhq/core-team-issues/issues/1776 ## Notes - Not refactor around triggers services as they're not consumed directly by any frontend services --- .../hooks/useMetadataErrorHandler.ts | 111 ++++++++ .../utils/classify-metadata-error.util.ts | 82 ++++++ .../useCreateOneObjectMetadataItem.test.tsx | 9 +- .../useDeleteOneObjectMetadataItem.test.tsx | 18 +- .../__tests__/useFieldMetadataItem.test.tsx | 44 ++- ...est-expect-metadata-request-status.util.ts | 13 + .../hooks/useCreateOneFieldMetadataItem.ts | 66 +++-- .../hooks/useCreateOneObjectMetadataItem.ts | 71 +++-- .../hooks/useDeleteOneFieldMetadataItem.ts | 68 +++-- .../hooks/useDeleteOneObjectMetadataItem.ts | 65 +++-- .../hooks/useUpdateOneFieldMetadataItem.ts | 63 ++-- .../hooks/useUpdateOneObjectMetadataItem.ts | 63 ++-- .../types/FailedMetadataRequestResult.type.ts | 1 + .../types/MetadataRequestResult.type.ts | 6 + .../SuccessfulMetadataRequestResult.type.ts | 4 + .../useSetViewTypeFromLayoutOptionsMenu.ts | 8 +- .../hooks/useHandleRecordGroupField.ts | 17 +- .../hooks/useViewFieldAggregateOperation.tsx | 6 +- .../SettingsObjectFieldItemTableRow.tsx | 9 +- ...SettingsUpdateDataModelObjectAboutForm.tsx | 84 ++---- .../components/tabs/ObjectSettings.tsx | 10 +- ...SettingsDataModelObjectIdentifiersForm.tsx | 34 +-- .../hooks/useCreateOneServerlessFunction.ts | 35 --- .../hooks/useDeleteOneServerlessFunction.ts | 36 --- .../hooks/usePersistServerlessFunction.ts | 183 ++++++++++++ .../hooks/useUpdateOneServerlessFunction.ts | 37 --- .../__tests__/useUpdateViewAggregate.test.tsx | 15 +- .../views/hooks/internal/usePersistView.ts | 129 +++++++++ .../hooks/internal/usePersistViewField.ts | 268 ++++++++++++++++++ .../internal/usePersistViewFieldRecords.ts | 129 --------- .../hooks/internal/usePersistViewFilter.ts | 267 +++++++++++++++++ ...ecords.ts => usePersistViewFilterGroup.ts} | 12 +- .../internal/usePersistViewFilterRecords.ts | 128 --------- .../hooks/internal/usePersistViewGroup.ts | 267 +++++++++++++++++ .../internal/usePersistViewGroupRecords.ts | 128 --------- ...ewSortRecords.ts => usePersistViewSort.ts} | 12 +- .../hooks/useCreateViewFromCurrentView.ts | 132 +++++---- .../src/modules/views/hooks/useDeleteView.ts | 39 --- .../hooks/useSaveAnyFieldFilterToView.ts | 14 +- .../views/hooks/useSaveCurrentViewFields.ts | 13 +- .../views/hooks/useSaveCurrentViewGroups.ts | 23 +- ...aveRecordFilterGroupsToViewFilterGroups.ts | 23 +- .../useSaveRecordFiltersToViewFilters.ts | 32 ++- .../hooks/useSaveRecordSortsToViewSorts.ts | 21 +- .../src/modules/views/hooks/useUpdateView.ts | 42 --- .../views/hooks/useUpdateViewAggregate.ts | 15 +- .../components/ViewPickerListContent.tsx | 6 +- .../hooks/useDeleteViewFromCurrentState.ts | 6 +- .../hooks/useUpdateViewFromCurrentState.ts | 10 +- .../WorkflowEditActionServerlessFunction.tsx | 18 +- .../settings/data-model/SettingsNewObject.tsx | 26 +- .../data-model/SettingsObjectFieldEdit.tsx | 101 ++++--- .../data-model/SettingsObjectTable.tsx | 38 +-- .../SettingsObjectNewFieldConfigure.tsx | 123 ++++---- .../SettingsServerlessFunctionDetail.tsx | 28 +- packages/twenty-front/tsconfig.spec.json | 1 + 56 files changed, 2073 insertions(+), 1136 deletions(-) create mode 100644 packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts create mode 100644 packages/twenty-front/src/modules/metadata-error-handler/utils/classify-metadata-error.util.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/types/FailedMetadataRequestResult.type.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/types/MetadataRequestResult.type.ts create mode 100644 packages/twenty-front/src/modules/object-metadata/types/SuccessfulMetadataRequestResult.type.ts delete mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts delete mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts create mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts delete mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts delete mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts rename packages/twenty-front/src/modules/views/hooks/internal/{usePersistViewFilterGroupRecords.ts => usePersistViewFilterGroup.ts} (91%) delete mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts create mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts delete mode 100644 packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts rename packages/twenty-front/src/modules/views/hooks/internal/{usePersistViewSortRecords.ts => usePersistViewSort.ts} (91%) delete mode 100644 packages/twenty-front/src/modules/views/hooks/useDeleteView.ts delete mode 100644 packages/twenty-front/src/modules/views/hooks/useUpdateView.ts diff --git a/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts new file mode 100644 index 0000000000..4ed468173a --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-error-handler/hooks/useMetadataErrorHandler.ts @@ -0,0 +1,111 @@ +import { type ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { useCallback } from 'react'; + +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { + type AllMetadataName, + WorkspaceMigrationV2ExceptionCode, +} from 'twenty-shared/metadata'; +import { classifyMetadataError } from '../utils/classify-metadata-error.util'; + +export const useMetadataErrorHandler = () => { + const { enqueueErrorSnackBar } = useSnackBar(); + const TRANSLATED_METADATA_NAME = { + objectMetadata: t`object`, + fieldMetadata: t`field`, + view: t`view`, + viewField: t`view field`, + viewGroup: t`view group`, + viewFilter: t`view filter`, + index: t`index`, + serverlessFunction: t`serverless function`, + cronTrigger: t`cron trigger`, + databaseEventTrigger: t`database trigger`, + routeTrigger: t`route trigger`, + } as const satisfies Record; + + const handleMetadataError = useCallback( + ( + error: ApolloError, + options: { + primaryMetadataName: AllMetadataName; + }, + ) => { + const classification = classifyMetadataError({ + error, + primaryMetadataName: options.primaryMetadataName, + }); + + const translatedMetadataName = + TRANSLATED_METADATA_NAME[options.primaryMetadataName]; + + switch (classification.type) { + case 'v1': + enqueueErrorSnackBar({ apolloError: classification.error }); + break; + + case 'v2-validation': { + const { + extensions, + primaryMetadataName, + relatedFailingMetadataNames, + } = classification; + + const targetErrors = extensions.errors[primaryMetadataName] || []; + if (targetErrors.length > 0) { + targetErrors.forEach((entityError) => { + entityError.errors.forEach((validationError) => + enqueueErrorSnackBar({ + message: + validationError.userFriendlyMessage ?? + validationError.message, + }), + ); + }); + } + + if ( + targetErrors.length === 0 && + relatedFailingMetadataNames.length > 0 + ) { + const relatedEntityNames = relatedFailingMetadataNames + .map((metadataName) => TRANSLATED_METADATA_NAME[metadataName]) + .join(', '); + + enqueueErrorSnackBar({ + message: t`Failed to create ${translatedMetadataName}. Related ${relatedEntityNames} validation failed. Please check your configuration and try again.`, + }); + } + + if ( + targetErrors.length === 0 && + relatedFailingMetadataNames.length === 0 + ) { + enqueueErrorSnackBar({ + message: t`Failed to create ${translatedMetadataName}. Please try again.`, + }); + } + break; + } + + case 'v2-internal': { + const { code } = classification; + const errorMessage = + code === + WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR + ? t`An internal error occurred while validating your changes. Please contact support.` + : t`An internal error occurred while applying your changes. Please contact support and try again later.`; + + enqueueErrorSnackBar({ message: errorMessage }); + break; + } + } + }, + [enqueueErrorSnackBar, TRANSLATED_METADATA_NAME], + ); + + return { + handleMetadataError, + }; +}; diff --git a/packages/twenty-front/src/modules/metadata-error-handler/utils/classify-metadata-error.util.ts b/packages/twenty-front/src/modules/metadata-error-handler/utils/classify-metadata-error.util.ts new file mode 100644 index 0000000000..503328cef5 --- /dev/null +++ b/packages/twenty-front/src/modules/metadata-error-handler/utils/classify-metadata-error.util.ts @@ -0,0 +1,82 @@ +import { type ApolloError } from '@apollo/client'; +import { + type AllMetadataName, + type MetadataValidationErrorResponse, + WorkspaceMigrationV2ExceptionCode, +} from 'twenty-shared/metadata'; +import { isDefined } from 'twenty-shared/utils'; + +export type MetadataErrorClassification = + | { type: 'v1'; error: ApolloError } + | { + type: 'v2-validation'; + extensions: MetadataValidationErrorResponse; + primaryMetadataName: AllMetadataName; + relatedFailingMetadataNames: AllMetadataName[]; + } + | { + type: 'v2-internal'; + code: WorkspaceMigrationV2ExceptionCode; + message: string; + }; + +const isMetadataValidationError = ( + extensions: Record, +): extensions is MetadataValidationErrorResponse => + extensions.code === 'METADATA_VALIDATION_FAILED'; + +const isMetadataInternalError = ( + extensions: Record, +): boolean => { + return ( + isDefined(extensions) && + isDefined(extensions.subCode) && + (extensions.subCode === + WorkspaceMigrationV2ExceptionCode.BUILDER_INTERNAL_SERVER_ERROR || + extensions.subCode === + WorkspaceMigrationV2ExceptionCode.RUNNER_INTERNAL_SERVER_ERROR) + ); +}; + +type ClassifyMetadataErrorArgs = { + error: ApolloError; + primaryMetadataName: AllMetadataName; +}; +export const classifyMetadataError = ({ + error, + primaryMetadataName, +}: ClassifyMetadataErrorArgs): MetadataErrorClassification => { + const extensions = error.graphQLErrors?.[0]?.extensions; + + if (!isDefined(extensions)) { + return { type: 'v1', error }; + } + + if (isMetadataValidationError(extensions)) { + const failingMetadataNames = Object.keys(extensions.errors) as [ + keyof MetadataValidationErrorResponse['errors'], + ]; + const relatedFailingMetadataNames = failingMetadataNames.filter( + (metadataName) => + extensions.errors[metadataName].length > 0 && + metadataName !== primaryMetadataName, + ); + + return { + type: 'v2-validation', + extensions, + primaryMetadataName, + relatedFailingMetadataNames, + }; + } + + if (isMetadataInternalError(extensions)) { + return { + type: 'v2-internal', + code: extensions.subCode as WorkspaceMigrationV2ExceptionCode, + message: (extensions.userFriendlyMessage as string) || error.message, + }; + } + + return { type: 'v1', error }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx index 539b92ca95..0aa4845014 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx @@ -10,12 +10,13 @@ import { variables, } from '../__mocks__/useCreateOneObjectMetadataItem'; +import { jestExpectSuccessfulMetadataRequestResult } from '@/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util'; +import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; +import { mockedUserData } from '~/testing/mock-data/users'; import { query as findManyObjectMetadataItemsQuery, responseData as findManyObjectMetadataItemsResponseData, } from '../__mocks__/useFindManyObjectMetadataItems'; -import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; -import { mockedUserData } from '~/testing/mock-data/users'; const mocks = [ { @@ -90,8 +91,8 @@ describe('useCreateOneObjectMetadataItem', () => { namePlural: 'viewFilters', nameSingular: 'viewFilter', }); - - expect(res.data).toEqual({ createOneObject: responseData }); + jestExpectSuccessfulMetadataRequestResult(res); + expect(res.response).toEqual({ data: { createOneObject: responseData } }); }); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx index 5665d95c22..c9b7146bc5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useDeleteOneObjectMetadataItem.test.tsx @@ -1,7 +1,4 @@ -import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; -import { type ReactNode } from 'react'; -import { RecoilRoot } from 'recoil'; import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; @@ -11,8 +8,10 @@ import { variables, } from '../__mocks__/useDeleteOneObjectMetadataItem'; +import { jestExpectSuccessfulMetadataRequestResult } from '@/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { FIND_ALL_CORE_VIEWS } from '@/views/graphql/queries/findAllCoreViews'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { mockedUserData } from '~/testing/mock-data/users'; import { mockedCoreViewsData } from '~/testing/mock-data/views'; import { @@ -65,13 +64,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - {children} - - -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useDeleteOneObjectMetadataItem', () => { it('should work as expected', async () => { @@ -83,7 +78,8 @@ describe('useDeleteOneObjectMetadataItem', () => { const res = await result.current.deleteOneObjectMetadataItem('idToDelete'); - expect(res.data).toEqual({ deleteOneObject: responseData }); + jestExpectSuccessfulMetadataRequestResult(res); + expect(res.response).toEqual({ data: { deleteOneObject: responseData } }); }); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx index 30dd1b57c8..c8830f0cd5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx @@ -15,6 +15,7 @@ import { variables, } from '../__mocks__/useFieldMetadataItem'; +import { jestExpectSuccessfulMetadataRequestResult } from '@/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { mockedUserData } from '~/testing/mock-data/users'; import { @@ -25,8 +26,11 @@ import { jest.mock('@/object-metadata/hooks/useUpdateOneFieldMetadataItem', () => ({ useUpdateOneFieldMetadataItem: () => ({ updateOneFieldMetadataItem: jest.fn().mockResolvedValue({ - data: { - updateOneField: responseData.default, + status: 'successful', + response: { + data: { + updateOneField: responseData.default, + }, }, }), }), @@ -140,13 +144,17 @@ describe('useFieldMetadataItem', () => { }); await act(async () => { - const res = await result.current.activateMetadataField( + const response = await result.current.activateMetadataField( fieldMetadataItem.id, objectMetadataId, ); - expect(res.data).toEqual({ - updateOneField: responseData.default, + jestExpectSuccessfulMetadataRequestResult(response); + + expect(response.response).toEqual({ + data: { + updateOneField: responseData.default, + }, }); }); }); @@ -164,9 +172,12 @@ describe('useFieldMetadataItem', () => { name: 'fieldName', isLabelSyncedWithName: true, }); + jestExpectSuccessfulMetadataRequestResult(res); - expect(res.data).toEqual({ - createOneField: responseData.createMetadataField, + expect(res.response).toEqual({ + data: { + createOneField: responseData.createMetadataField, + }, }); }); }); @@ -177,12 +188,13 @@ describe('useFieldMetadataItem', () => { }); await act(async () => { - const res = await result.current.deactivateMetadataField( + const response = await result.current.deactivateMetadataField( fieldMetadataItem.id, objectMetadataId, ); - expect(res.data).toEqual({ + jestExpectSuccessfulMetadataRequestResult(response); + expect(response.response.data).toEqual({ updateOneField: responseData.default, }); }); @@ -198,9 +210,12 @@ describe('useFieldMetadataItem', () => { idToDelete: fieldMetadataItem.id, objectMetadataId, }); + jestExpectSuccessfulMetadataRequestResult(res); - expect(res.data).toEqual({ - deleteOneField: responseData.default, + expect(res.response).toEqual({ + data: { + deleteOneField: responseData.default, + }, }); }); }); @@ -215,9 +230,12 @@ describe('useFieldMetadataItem', () => { idToDelete: fieldRelationMetadataItem.id, objectMetadataId, }); + jestExpectSuccessfulMetadataRequestResult(res); - expect(res.data).toEqual({ - deleteOneField: responseData.fieldRelation, + expect(res.response).toEqual({ + data: { + deleteOneField: responseData.fieldRelation, + }, }); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util.ts new file mode 100644 index 0000000000..d13e42207c --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/utils/jest-expect-metadata-request-status.util.ts @@ -0,0 +1,13 @@ +import { expect } from '@jest/globals'; + +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { type SuccessfulMetadataRequestResult } from '@/object-metadata/types/SuccessfulMetadataRequestResult.type'; + +type AssertIsSuccessfulMetadataRequestResult = ( + value: MetadataRequestResult, +) => asserts value is SuccessfulMetadataRequestResult; + +export const jestExpectSuccessfulMetadataRequestResult: AssertIsSuccessfulMetadataRequestResult = + (value) => { + expect(value.status).toBe('successful'); + }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts index e4c232b532..118ee2e1d3 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts @@ -1,41 +1,67 @@ -import { useMutation } from '@apollo/client'; - import { type CreateFieldInput, - type CreateOneFieldMetadataItemMutation, - type CreateOneFieldMetadataItemMutationVariables, + useCreateOneFieldMetadataItemMutation, } from '~/generated-metadata/graphql'; -import { CREATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; - +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; export const useCreateOneFieldMetadataItem = () => { const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); - const [mutate] = useMutation< - CreateOneFieldMetadataItemMutation, - CreateOneFieldMetadataItemMutationVariables - >(CREATE_ONE_FIELD_METADATA_ITEM); + const [createOneFieldMetadataItemMutation] = + useCreateOneFieldMetadataItemMutation(); const { refreshCoreViewsByObjectMetadataId } = useRefreshCoreViewsByObjectMetadataId(); - const createOneFieldMetadataItem = async (input: CreateFieldInput) => { - const result = await mutate({ - variables: { - input: { - field: input, + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + + const createOneFieldMetadataItem = async ( + input: CreateFieldInput, + ): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const response = await createOneFieldMetadataItemMutation({ + variables: { + input: { + field: input, + }, }, - }, - }); + }); - await refreshObjectMetadataItems(); + await refreshObjectMetadataItems(); - await refreshCoreViewsByObjectMetadataId(input.objectMetadataId); - return result; + await refreshCoreViewsByObjectMetadataId(input.objectMetadataId); + + return { + status: 'successful', + response, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'fieldMetadata', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } }; return { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index 884b7cd884..7293ab40e1 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -1,15 +1,15 @@ -import { useMutation } from '@apollo/client'; - import { type CreateObjectInput, - type CreateOneObjectMetadataItemMutation, - type CreateOneObjectMetadataItemMutationVariables, + useCreateOneObjectMetadataItemMutation, } from '~/generated-metadata/graphql'; -import { CREATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; - +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; export const useCreateOneObjectMetadataItem = () => { @@ -18,27 +18,52 @@ export const useCreateOneObjectMetadataItem = () => { const { refreshCoreViewsByObjectMetadataId } = useRefreshCoreViewsByObjectMetadataId(); - const [mutate] = useMutation< - CreateOneObjectMetadataItemMutation, - CreateOneObjectMetadataItemMutationVariables - >(CREATE_ONE_OBJECT_METADATA_ITEM); + const [createOneObjectMetadataItemMutation] = + useCreateOneObjectMetadataItemMutation(); - const createOneObjectMetadataItem = async (input: CreateObjectInput) => { - const createdObjectMetadata = await mutate({ - variables: { - input: { object: input }, - }, - }); + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); - await refreshObjectMetadataItems(); + const createOneObjectMetadataItem = async ( + input: CreateObjectInput, + ): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const createdObjectMetadata = await createOneObjectMetadataItemMutation({ + variables: { + input: { object: input }, + }, + }); - if (isDefined(createdObjectMetadata.data?.createOneObject?.id)) { - await refreshCoreViewsByObjectMetadataId( - createdObjectMetadata.data.createOneObject.id, - ); + await refreshObjectMetadataItems(); + + if (isDefined(createdObjectMetadata.data?.createOneObject?.id)) { + await refreshCoreViewsByObjectMetadataId( + createdObjectMetadata.data.createOneObject.id, + ); + } + + return { + status: 'successful', + response: createdObjectMetadata, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'objectMetadata', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; } - - return createdObjectMetadata; }; return { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts index 9e6eeb8628..64dcf21582 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts @@ -1,35 +1,35 @@ -import { useMutation } from '@apollo/client'; - -import { - type DeleteOneFieldMetadataItemMutation, - type DeleteOneFieldMetadataItemMutationVariables, -} from '~/generated-metadata/graphql'; +import { useDeleteOneFieldMetadataItemMutation } from '~/generated-metadata/graphql'; +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; import { useRecoilState } from 'recoil'; -import { DELETE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; export const useDeleteOneFieldMetadataItem = () => { - const [mutate] = useMutation< - DeleteOneFieldMetadataItemMutation, - DeleteOneFieldMetadataItemMutationVariables - >(DELETE_ONE_FIELD_METADATA_ITEM); + const [deleteOneFieldMetadataItemMutation] = + useDeleteOneFieldMetadataItemMutation(); const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); const { refreshCoreViewsByObjectMetadataId } = useRefreshCoreViewsByObjectMetadataId(); + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + const [ recordIndexKanbanAggregateOperation, setRecordIndexKanbanAggregateOperation, ] = useRecoilState(recordIndexKanbanAggregateOperationState); const resetRecordIndexKanbanAggregateOperation = async ( - idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'], + idToDelete: string, ) => { if (recordIndexKanbanAggregateOperation?.fieldMetadataId === idToDelete) { setRecordIndexKanbanAggregateOperation({ @@ -43,21 +43,43 @@ export const useDeleteOneFieldMetadataItem = () => { idToDelete, objectMetadataId, }: { - idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete']; + idToDelete: string; objectMetadataId: string; - }) => { - const result = await mutate({ - variables: { - idToDelete, - }, - }); + }): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const response = await deleteOneFieldMetadataItemMutation({ + variables: { + idToDelete, + }, + }); - await resetRecordIndexKanbanAggregateOperation(idToDelete); + await resetRecordIndexKanbanAggregateOperation(idToDelete); - await refreshObjectMetadataItems(); - await refreshCoreViewsByObjectMetadataId(objectMetadataId); + await refreshObjectMetadataItems(); + await refreshCoreViewsByObjectMetadataId(objectMetadataId); - return result; + return { + status: 'successful', + response, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'fieldMetadata', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } }; return { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts index 770b71fdef..217ec66636 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts @@ -1,39 +1,60 @@ -import { useMutation } from '@apollo/client'; - -import { - type DeleteOneObjectMetadataItemMutation, - type DeleteOneObjectMetadataItemMutationVariables, -} from '~/generated-metadata/graphql'; - -import { DELETE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; +import { useDeleteOneObjectMetadataItemMutation } from '~/generated-metadata/graphql'; +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; export const useDeleteOneObjectMetadataItem = () => { - const [mutate] = useMutation< - DeleteOneObjectMetadataItemMutation, - DeleteOneObjectMetadataItemMutationVariables - >(DELETE_ONE_OBJECT_METADATA_ITEM); + const [deleteOneObjectMetadataItemMutation] = + useDeleteOneObjectMetadataItemMutation(); const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); const { refreshAllCoreViews } = useRefreshAllCoreViews(); + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + const deleteOneObjectMetadataItem = async ( - idToDelete: DeleteOneObjectMetadataItemMutationVariables['idToDelete'], - ) => { - const result = await mutate({ - variables: { - idToDelete, - }, - }); + idToDelete: string, + ): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const response = await deleteOneObjectMetadataItemMutation({ + variables: { + idToDelete, + }, + }); - await refreshObjectMetadataItems(); - await refreshAllCoreViews(); + await refreshObjectMetadataItems(); + await refreshAllCoreViews(); - return result; + return { + status: 'successful', + response, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'objectMetadata', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } }; return { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts index e904c04942..f6996ad36e 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts @@ -1,14 +1,15 @@ -import { useMutation } from '@apollo/client'; - import { - type UpdateOneFieldMetadataItemMutation, type UpdateOneFieldMetadataItemMutationVariables, + useUpdateOneFieldMetadataItemMutation, } from '~/generated-metadata/graphql'; -import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; - +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; export const useUpdateOneFieldMetadataItem = () => { const { refreshObjectMetadataItems } = @@ -17,10 +18,12 @@ export const useUpdateOneFieldMetadataItem = () => { const { refreshCoreViewsByObjectMetadataId } = useRefreshCoreViewsByObjectMetadataId(); - const [mutate] = useMutation< - UpdateOneFieldMetadataItemMutation, - UpdateOneFieldMetadataItemMutationVariables - >(UPDATE_ONE_FIELD_METADATA_ITEM); + const [updateOneFieldMetadataItemMutation] = + useUpdateOneFieldMetadataItemMutation(); + + const { handleMetadataError } = useMetadataErrorHandler(); + + const { enqueueErrorSnackBar } = useSnackBar(); const updateOneFieldMetadataItem = async ({ objectMetadataId, @@ -40,18 +43,40 @@ export const useUpdateOneFieldMetadataItem = () => { | 'options' | 'isLabelSyncedWithName' >; - }) => { - const result = await mutate({ - variables: { - idToUpdate: fieldMetadataIdToUpdate, - updatePayload: updatePayload, - }, - }); + }): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const response = await updateOneFieldMetadataItemMutation({ + variables: { + idToUpdate: fieldMetadataIdToUpdate, + updatePayload: updatePayload, + }, + }); - await refreshObjectMetadataItems(); - await refreshCoreViewsByObjectMetadataId(objectMetadataId); + await refreshObjectMetadataItems(); + await refreshCoreViewsByObjectMetadataId(objectMetadataId); - return result; + return { + status: 'successful', + response, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'fieldMetadata', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } }; return { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts index 6a0247e0dd..ef4681a1a2 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneObjectMetadataItem.ts @@ -1,22 +1,20 @@ -import { useMutation } from '@apollo/client'; - import { + useUpdateOneObjectMetadataItemMutation, type UpdateOneObjectInput, - type UpdateOneObjectMetadataItemMutation, - type UpdateOneObjectMetadataItemMutationVariables, } from '~/generated-metadata/graphql'; -import { UPDATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; - +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; // TODO: Slice the Apollo store synchronously in the update function instead of subscribing, so we can use update after read in the same function call export const useUpdateOneObjectMetadataItem = () => { - const [mutate, { loading }] = useMutation< - UpdateOneObjectMetadataItemMutation, - UpdateOneObjectMetadataItemMutationVariables - >(UPDATE_ONE_OBJECT_METADATA_ITEM); + const [updateOneObjectMetadataItemMutation, { loading }] = + useUpdateOneObjectMetadataItemMutation(); const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); @@ -24,24 +22,49 @@ export const useUpdateOneObjectMetadataItem = () => { const { refreshCoreViewsByObjectMetadataId } = useRefreshCoreViewsByObjectMetadataId(); + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + const updateOneObjectMetadataItem = async ({ idToUpdate, updatePayload, }: { idToUpdate: UpdateOneObjectInput['id']; updatePayload: UpdateOneObjectInput['update']; - }) => { - const result = await mutate({ - variables: { - idToUpdate, - updatePayload, - }, - }); + }): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const response = await updateOneObjectMetadataItemMutation({ + variables: { + idToUpdate, + updatePayload, + }, + }); - await refreshObjectMetadataItems(); - await refreshCoreViewsByObjectMetadataId(idToUpdate); + await refreshObjectMetadataItems(); + await refreshCoreViewsByObjectMetadataId(idToUpdate); - return result; + return { + status: 'successful', + response, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'objectMetadata', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } }; return { diff --git a/packages/twenty-front/src/modules/object-metadata/types/FailedMetadataRequestResult.type.ts b/packages/twenty-front/src/modules/object-metadata/types/FailedMetadataRequestResult.type.ts new file mode 100644 index 0000000000..89cc68bd6b --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/types/FailedMetadataRequestResult.type.ts @@ -0,0 +1 @@ +export type FailedMetadataRequestResult = { status: 'failed'; error: unknown }; diff --git a/packages/twenty-front/src/modules/object-metadata/types/MetadataRequestResult.type.ts b/packages/twenty-front/src/modules/object-metadata/types/MetadataRequestResult.type.ts new file mode 100644 index 0000000000..09314bf8a8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/types/MetadataRequestResult.type.ts @@ -0,0 +1,6 @@ +import { type FailedMetadataRequestResult } from '@/object-metadata/types/FailedMetadataRequestResult.type'; +import { type SuccessfulMetadataRequestResult } from '@/object-metadata/types/SuccessfulMetadataRequestResult.type'; + +export type MetadataRequestResult = + | FailedMetadataRequestResult + | SuccessfulMetadataRequestResult; diff --git a/packages/twenty-front/src/modules/object-metadata/types/SuccessfulMetadataRequestResult.type.ts b/packages/twenty-front/src/modules/object-metadata/types/SuccessfulMetadataRequestResult.type.ts new file mode 100644 index 0000000000..9fcce55d01 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/types/SuccessfulMetadataRequestResult.type.ts @@ -0,0 +1,4 @@ +export type SuccessfulMetadataRequestResult = { + status: 'successful'; + response: T; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts index fed132c140..6431ca878c 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts @@ -4,7 +4,7 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; -import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView'; import { coreViewsState } from '@/views/states/coreViewState'; import { type GraphQLView } from '@/views/types/GraphQLView'; @@ -28,7 +28,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { const { loadRecordIndexStates } = useLoadRecordIndexStates(); - const { createViewGroupRecords } = usePersistViewGroupRecords(); + const { createViewGroups } = usePersistViewGroupRecords(); const { availableFieldsForCalendar } = useGetAvailableFieldsForCalendar(); @@ -58,7 +58,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { fieldMetadataId: randomFieldForKanban, } satisfies ViewGroup); - await createViewGroupRecords( + await createViewGroups( viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ input: { ...viewGroup, @@ -69,7 +69,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => { return viewGroupsToCreate; }, - [objectMetadataItem, createViewGroupRecords], + [objectMetadataItem, createViewGroups], ); const setAndPersistViewType = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts index c0de8dd1f3..4f59f67fc9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts @@ -3,7 +3,7 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; import { type ViewGroup } from '@/views/types/ViewGroup'; import { useRecoilCallback } from 'recoil'; @@ -11,8 +11,7 @@ import { v4 } from 'uuid'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useHandleRecordGroupField = () => { - const { createViewGroupRecords, deleteViewGroupRecords } = - usePersistViewGroupRecords(); + const { createViewGroups, deleteViewGroups } = usePersistViewGroupRecords(); const currentViewIdCallbackState = useRecoilComponentCallbackState( contextStoreCurrentViewIdComponentState, @@ -106,7 +105,7 @@ export const useHandleRecordGroupField = () => { ); if (viewGroupsToCreate.length > 0) { - await createViewGroupRecords( + await createViewGroups( viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ input: { ...viewGroup, @@ -117,7 +116,7 @@ export const useHandleRecordGroupField = () => { } if (viewGroupsToDelete.length > 0) { - await deleteViewGroupRecords( + await deleteViewGroups( viewGroupsToDelete.map((group) => ({ input: { id: group.id, @@ -131,8 +130,8 @@ export const useHandleRecordGroupField = () => { currentViewIdCallbackState, getViewFromPrefetchState, setRecordGroupsFromViewGroups, - createViewGroupRecords, - deleteViewGroupRecords, + createViewGroups, + deleteViewGroups, ], ); @@ -157,7 +156,7 @@ export const useHandleRecordGroupField = () => { return; } - await deleteViewGroupRecords( + await deleteViewGroups( view.viewGroups.map((group) => ({ input: { id: group.id, @@ -168,7 +167,7 @@ export const useHandleRecordGroupField = () => { setRecordGroupsFromViewGroups(view.id, [], objectMetadataItem); }, [ - deleteViewGroupRecords, + deleteViewGroups, currentViewIdCallbackState, getViewFromPrefetchState, setRecordGroupsFromViewGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx index 97b848a6b4..66bb50a250 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useViewFieldAggregateOperation.tsx @@ -3,7 +3,7 @@ import { RecordTableColumnAggregateFooterDropdownContext } from '@/object-record import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState'; import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation'; -import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords'; +import { usePersistViewField } from '@/views/hooks/internal/usePersistViewField'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; import { useContext } from 'react'; @@ -24,14 +24,14 @@ export const useViewFieldAggregateOperation = () => { useRefreshCoreViewsByObjectMetadataId(); const { objectMetadataItem } = useRecordIndexContextOrThrow(); - const { updateViewFieldRecords } = usePersistViewFieldRecords(); + const { updateViewFields } = usePersistViewField(); const updateViewFieldAggregateOperation = async ( aggregateOperation: ExtendedAggregateOperations | null, ) => { if (!currentViewField) { throw new Error('ViewField not found'); } - await updateViewFieldRecords([ + await updateViewFields([ { input: { id: currentViewField.id, diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index a5c971bcc7..1e91a9fc8b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -4,6 +4,7 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMe import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; +import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown'; import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown'; import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState'; @@ -28,7 +29,6 @@ import { RelationType } from '~/generated-metadata/graphql'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem'; -import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { RELATION_TYPES } from '../../constants/RelationTypes'; import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; @@ -127,11 +127,15 @@ export const SettingsObjectFieldItemTableRow = ({ return; } - await deactivateMetadataField( + const deactivationResult = await deactivateMetadataField( activeFieldMetadatItem.id, objectMetadataItem.id, ); + if (deactivationResult.status === 'failed') { + return; + } + // TODO: Add optimistic rendering for core views const deletedViewIds: string[] = []; @@ -159,6 +163,7 @@ export const SettingsObjectFieldItemTableRow = ({ if (readonly) { return; } + updateOneObjectMetadataItem({ idToUpdate: objectMetadataItem.id, updatePayload: { diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx index 7cf8e4262e..3db53d917a 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm.tsx @@ -1,20 +1,17 @@ import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { SettingsDataModelObjectAboutForm } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm'; import { type SettingsDataModelObjectAboutFormValues, settingsDataModelObjectAboutFormSchema, } from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { FormProvider, useForm } from 'react-hook-form'; import { useSetRecoilState } from 'recoil'; import { SettingsPath } from 'twenty-shared/types'; -import { ZodError } from 'zod'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { updatedObjectNamePluralState } from '~/pages/settings/data-model/states/updatedObjectNamePluralState'; -import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; type SettingsUpdateDataModelObjectAboutFormProps = { objectMetadataItem: ObjectMetadataItem; @@ -25,7 +22,6 @@ export const SettingsUpdateDataModelObjectAboutForm = ({ }: SettingsUpdateDataModelObjectAboutFormProps) => { const readonly = isObjectMetadataReadOnly({ objectMetadataItem }); const navigate = useNavigateSettings(); - const { enqueueErrorSnackBar } = useSnackBar(); const setUpdatedObjectNamePlural = useSetRecoilState( updatedObjectNamePluralState, ); @@ -67,40 +63,41 @@ export const SettingsUpdateDataModelObjectAboutForm = ({ const objectNamePluralForRedirection = formValues.namePlural ?? objectMetadataItem.namePlural; - try { - setUpdatedObjectNamePlural(objectNamePluralForRedirection); - - const updatedObject = await updateObjectMetadata(formValues); - - if (formValues.isLabelSyncedWithName !== isLabelSyncedWithName) { - formConfig.reset({ - description, - icon: icon ?? undefined, - isLabelSyncedWithName: formValues.isLabelSyncedWithName, - labelPlural: updatedObject?.data?.updateOneObject.labelPlural, - labelSingular: updatedObject?.data?.updateOneObject.labelSingular, - namePlural: updatedObject?.data?.updateOneObject.namePlural, - nameSingular: updatedObject?.data?.updateOneObject.nameSingular, - }); - } else { - formConfig.reset(undefined, { keepValues: true }); - } - - navigate(SettingsPath.ObjectDetail, { - objectNamePlural: objectNamePluralForRedirection, - }); - } catch (error) { - handleError(error); + if (readonly) { + return; } + + setUpdatedObjectNamePlural(objectNamePluralForRedirection); + const updateResult = await updateObjectMetadata(formValues); + + if (updateResult.status === 'failed') { + return; + } + + const updatedObject = updateResult.response; + + if (formValues.isLabelSyncedWithName !== isLabelSyncedWithName) { + formConfig.reset({ + description, + icon: icon ?? undefined, + isLabelSyncedWithName: formValues.isLabelSyncedWithName, + labelPlural: updatedObject?.data?.updateOneObject.labelPlural, + labelSingular: updatedObject?.data?.updateOneObject.labelSingular, + namePlural: updatedObject?.data?.updateOneObject.namePlural, + nameSingular: updatedObject?.data?.updateOneObject.nameSingular, + }); + } else { + formConfig.reset(undefined, { keepValues: true }); + } + + navigate(SettingsPath.ObjectDetail, { + objectNamePlural: objectNamePluralForRedirection, + }); }; const updateObjectMetadata = async ( formValues: SettingsDataModelObjectAboutFormValues, ) => { - if (readonly) { - return; - } - const updatePayload = { ...formValues }; if (!objectMetadataItem.isCustom) { @@ -123,27 +120,6 @@ export const SettingsUpdateDataModelObjectAboutForm = ({ }); }; - const handleError = (error: unknown) => { - // eslint-disable-next-line no-console - console.error(error); - - if (error instanceof ZodError) { - enqueueErrorSnackBar({ - message: error.issues[0].message, - }); - return; - } - - if (error instanceof ApolloError) { - enqueueErrorSnackBar({ - apolloError: error, - }); - return; - } - - enqueueErrorSnackBar({}); - }; - return ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx index 8ba65ce016..9454fa0d2b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/tabs/ObjectSettings.tsx @@ -1,6 +1,7 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; +import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { SettingsUpdateDataModelObjectAboutForm } from '@/settings/data-model/object-details/components/SettingsUpdateDataModelObjectAboutForm'; import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard'; import styled from '@emotion/styled'; @@ -10,7 +11,6 @@ import { H2Title, IconArchive } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; import { Section } from 'twenty-ui/layout'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; -import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; type ObjectSettingsProps = { objectMetadataItem: ObjectMetadataItem; @@ -31,12 +31,16 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => { const readonly = isObjectMetadataReadOnly({ objectMetadataItem }); const navigate = useNavigateSettings(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); + const handleDisable = async () => { - await updateOneObjectMetadataItem({ + const result = await updateOneObjectMetadataItem({ idToUpdate: objectMetadataItem.id, updatePayload: { isActive: false }, }); - navigate(SettingsPath.Objects); + + if (result.status === 'successful') { + navigate(SettingsPath.Objects); + } }; return ( diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx index 70e28fea6f..b9545a7ec7 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm.tsx @@ -1,22 +1,19 @@ -import styled from '@emotion/styled'; -import { useMemo } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { ZodError, type z } from 'zod'; - import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; import { Select } from '@/ui/input/components/Select'; -import { ApolloError } from '@apollo/client'; +import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; import { t } from '@lingui/core/macro'; +import { useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { isLabelIdentifierFieldMetadataTypes } from 'twenty-shared/utils'; import { IconCircleOff, IconPlus, useIcons } from 'twenty-ui/display'; import { type SelectOption } from 'twenty-ui/input'; -import { isObjectMetadataReadOnly } from '@/object-record/read-only/utils/isObjectMetadataReadOnly'; +import { type z } from 'zod'; export const settingsDataModelObjectIdentifiersFormSchema = objectMetadataItemSchema.pick({ @@ -50,7 +47,6 @@ export const SettingsDataModelObjectIdentifiersForm = ({ mode: 'onTouched', resolver: zodResolver(settingsDataModelObjectIdentifiersFormSchema), }); - const { enqueueErrorSnackBar } = useSnackBar(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); const handleSave = async ( @@ -60,23 +56,13 @@ export const SettingsDataModelObjectIdentifiersForm = ({ return; } - try { - await updateOneObjectMetadataItem({ - idToUpdate: objectMetadataItem.id, - updatePayload: formValues, - }); + const result = await updateOneObjectMetadataItem({ + idToUpdate: objectMetadataItem.id, + updatePayload: formValues, + }); + if (result.status === 'successful') { formConfig.reset(undefined, { keepValues: true }); - } catch (error) { - if (error instanceof ZodError) { - enqueueErrorSnackBar({ - message: error.issues[0].message, - }); - } else { - enqueueErrorSnackBar({ - apolloError: error instanceof ApolloError ? error : undefined, - }); - } } }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts deleted file mode 100644 index b4ba2180fb..0000000000 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useCreateOneServerlessFunction.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useMutation } from '@apollo/client'; - -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction'; -import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; -import { getOperationName } from '@apollo/client/utilities'; -import { - type CreateOneServerlessFunctionItemMutation, - type CreateOneServerlessFunctionItemMutationVariables, - type CreateServerlessFunctionInput, -} from '~/generated-metadata/graphql'; - -export const useCreateOneServerlessFunction = () => { - const apolloMetadataClient = useApolloCoreClient(); - const [mutate] = useMutation< - CreateOneServerlessFunctionItemMutation, - CreateOneServerlessFunctionItemMutationVariables - >(CREATE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient, - }); - - const createOneServerlessFunction = async ( - input: CreateServerlessFunctionInput, - ) => { - return await mutate({ - variables: { - input, - }, - awaitRefetchQueries: true, - refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''], - }); - }; - - return { createOneServerlessFunction }; -}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts deleted file mode 100644 index a177edfed3..0000000000 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction'; -import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; -import { useMutation } from '@apollo/client'; -import { getOperationName } from '@apollo/client/utilities'; -import { - type DeleteOneServerlessFunctionMutation, - type DeleteOneServerlessFunctionMutationVariables, - type ServerlessFunctionIdInput, -} from '~/generated-metadata/graphql'; - -export const useDeleteOneServerlessFunction = () => { - const apolloMetadataClient = useApolloCoreClient(); - const [mutate] = useMutation< - DeleteOneServerlessFunctionMutation, - DeleteOneServerlessFunctionMutationVariables - >(DELETE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient, - }); - - const deleteOneServerlessFunction = async ( - input: ServerlessFunctionIdInput, - ) => { - return await mutate({ - variables: { - input, - }, - awaitRefetchQueries: true, - refetchQueries: [ - getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '', - ], - }); - }; - - return { deleteOneServerlessFunction }; -}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts new file mode 100644 index 0000000000..370b4a76a7 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/usePersistServerlessFunction.ts @@ -0,0 +1,183 @@ +import { useCallback } from 'react'; + +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction'; +import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction'; +import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction'; +import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions'; +import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ApolloError, useMutation } from '@apollo/client'; +import { getOperationName } from '@apollo/client/utilities'; +import { t } from '@lingui/core/macro'; +import { + type CreateOneServerlessFunctionItemMutation, + type CreateOneServerlessFunctionItemMutationVariables, + type DeleteOneServerlessFunctionMutation, + type DeleteOneServerlessFunctionMutationVariables, + type UpdateOneServerlessFunctionMutation, + type UpdateOneServerlessFunctionMutationVariables, +} from '~/generated-metadata/graphql'; + +export const usePersistServerlessFunction = () => { + const apolloMetadataClient = useApolloCoreClient(); + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + + const [createServerlessFunctionMutation] = useMutation< + CreateOneServerlessFunctionItemMutation, + CreateOneServerlessFunctionItemMutationVariables + >(CREATE_ONE_SERVERLESS_FUNCTION, { + client: apolloMetadataClient, + }); + + const [updateServerlessFunctionMutation] = useMutation< + UpdateOneServerlessFunctionMutation, + UpdateOneServerlessFunctionMutationVariables + >(UPDATE_ONE_SERVERLESS_FUNCTION, { + client: apolloMetadataClient, + }); + + const [deleteServerlessFunctionMutation] = useMutation< + DeleteOneServerlessFunctionMutation, + DeleteOneServerlessFunctionMutationVariables + >(DELETE_ONE_SERVERLESS_FUNCTION, { + client: apolloMetadataClient, + }); + + const createServerlessFunction = useCallback( + async ( + variables: CreateOneServerlessFunctionItemMutationVariables, + ): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const result = await createServerlessFunctionMutation({ + variables, + awaitRefetchQueries: true, + refetchQueries: [ + getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? '', + ], + }); + + return { + status: 'successful', + response: result, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'serverlessFunction', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + createServerlessFunctionMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const updateServerlessFunction = useCallback( + async ( + variables: UpdateOneServerlessFunctionMutationVariables, + ): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const result = await updateServerlessFunctionMutation({ + variables, + refetchQueries: [ + getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '', + ], + }); + + return { + status: 'successful', + response: result, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'serverlessFunction', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + updateServerlessFunctionMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const deleteServerlessFunction = useCallback( + async ( + variables: DeleteOneServerlessFunctionMutationVariables, + ): Promise< + MetadataRequestResult< + Awaited> + > + > => { + try { + const result = await deleteServerlessFunctionMutation({ + variables, + awaitRefetchQueries: true, + refetchQueries: [ + getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '', + ], + }); + + return { + status: 'successful', + response: result, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'serverlessFunction', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + deleteServerlessFunctionMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + return { + createServerlessFunction, + updateServerlessFunction, + deleteServerlessFunction, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts deleted file mode 100644 index be7fab2224..0000000000 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useUpdateOneServerlessFunction.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; -import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction'; -import { FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunctionSourceCode'; -import { useMutation } from '@apollo/client'; -import { getOperationName } from '@apollo/client/utilities'; -import { - type UpdateOneServerlessFunctionMutation, - type UpdateOneServerlessFunctionMutationVariables, - type UpdateServerlessFunctionInputUpdates, -} from '~/generated-metadata/graphql'; - -export const useUpdateOneServerlessFunction = ( - serverlessFunctionId: string, -) => { - const apolloMetadataClient = useApolloCoreClient(); - const [mutate] = useMutation< - UpdateOneServerlessFunctionMutation, - UpdateOneServerlessFunctionMutationVariables - >(UPDATE_ONE_SERVERLESS_FUNCTION, { - client: apolloMetadataClient, - }); - - const updateOneServerlessFunction = async ( - update: UpdateServerlessFunctionInputUpdates, - ) => { - return await mutate({ - variables: { - input: { update, id: serverlessFunctionId }, - }, - refetchQueries: [ - getOperationName(FIND_ONE_SERVERLESS_FUNCTION_SOURCE_CODE) ?? '', - ], - }); - }; - - return { updateOneServerlessFunction }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/__tests__/useUpdateViewAggregate.test.tsx b/packages/twenty-front/src/modules/views/hooks/__tests__/useUpdateViewAggregate.test.tsx index 9ba8796c04..fce7b202f6 100644 --- a/packages/twenty-front/src/modules/views/hooks/__tests__/useUpdateViewAggregate.test.tsx +++ b/packages/twenty-front/src/modules/views/hooks/__tests__/useUpdateViewAggregate.test.tsx @@ -1,17 +1,16 @@ import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useUpdateView } from '@/views/hooks/useUpdateView'; +import { usePersistView } from '@/views/hooks/internal/usePersistView'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; import { renderHook } from '@testing-library/react'; import { useSetRecoilState } from 'recoil'; import { useUpdateViewAggregate } from '../useUpdateViewAggregate'; -import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; jest.mock('@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'); -jest.mock('@/views/hooks/useUpdateView'); +jest.mock('@/views/hooks/internal/usePersistView'); jest.mock('@/views/hooks/useRefreshCoreViewsByObjectMetadataId'); jest.mock('recoil'); - describe('useUpdateViewAggregate', () => { const mockCurrentViewId = 'test-view-id'; const mockUpdateView = jest.fn(); @@ -20,7 +19,7 @@ describe('useUpdateViewAggregate', () => { beforeEach(() => { jest.clearAllMocks(); (useRecoilComponentValue as jest.Mock).mockReturnValue(mockCurrentViewId); - (useUpdateView as jest.Mock).mockReturnValue({ + (usePersistView as jest.Mock).mockReturnValue({ updateView: mockUpdateView, }); (useSetRecoilState as jest.Mock).mockReturnValue( @@ -44,8 +43,10 @@ describe('useUpdateViewAggregate', () => { // updateView is called with 'EARLIEST' converted to 'MIN' expect(mockUpdateView).toHaveBeenCalledWith({ id: mockCurrentViewId, - kanbanAggregateOperationFieldMetadataId: 'test-field-id', - kanbanAggregateOperation: AggregateOperations.MIN, + input: { + kanbanAggregateOperationFieldMetadataId: 'test-field-id', + kanbanAggregateOperation: AggregateOperations.MIN, + }, }); // setAggregateOperation is called with 'EARLIEST' diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts new file mode 100644 index 0000000000..4ab299bf4f --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistView.ts @@ -0,0 +1,129 @@ +import { useCallback } from 'react'; + +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { + type CreateCoreViewMutationVariables, + type DeleteCoreViewMutationVariables, + type UpdateCoreViewMutationVariables, + useCreateCoreViewMutation, + useDeleteCoreViewMutation, + useUpdateCoreViewMutation, +} from '~/generated/graphql'; + +export const usePersistView = () => { + const [createCoreViewMutation] = useCreateCoreViewMutation(); + const [updateCoreViewMutation] = useUpdateCoreViewMutation(); + const [deleteCoreViewMutation] = useDeleteCoreViewMutation(); + + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + + const createView = useCallback( + async ( + variables: CreateCoreViewMutationVariables, + ): Promise< + MetadataRequestResult>> + > => { + try { + const result = await createCoreViewMutation({ + variables, + }); + + return { + status: 'successful', + response: result, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'view', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [createCoreViewMutation, handleMetadataError, enqueueErrorSnackBar], + ); + + const updateView = useCallback( + async ( + variables: UpdateCoreViewMutationVariables, + ): Promise< + MetadataRequestResult>> + > => { + try { + const result = await updateCoreViewMutation({ + variables, + }); + + return { + status: 'successful', + response: result, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'view', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [updateCoreViewMutation, handleMetadataError, enqueueErrorSnackBar], + ); + + const deleteView = useCallback( + async ( + variables: DeleteCoreViewMutationVariables, + ): Promise< + MetadataRequestResult>> + > => { + try { + const result = await deleteCoreViewMutation({ + variables, + }); + + return { + status: 'successful', + response: result, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'view', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [deleteCoreViewMutation, handleMetadataError, enqueueErrorSnackBar], + ); + + return { + createView, + updateView, + deleteView, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts new file mode 100644 index 0000000000..2ccd7b3eac --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewField.ts @@ -0,0 +1,268 @@ +import { useCallback } from 'react'; + +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useTriggerViewFieldOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { + type CreateCoreViewFieldMutationVariables, + type DeleteCoreViewFieldMutationVariables, + type DestroyCoreViewFieldMutationVariables, + type UpdateCoreViewFieldMutationVariables, + useCreateCoreViewFieldMutation, + useDeleteCoreViewFieldMutation, + useDestroyCoreViewFieldMutation, + useUpdateCoreViewFieldMutation, +} from '~/generated/graphql'; + +export const usePersistViewField = () => { + const { triggerViewFieldOptimisticEffect } = + useTriggerViewFieldOptimisticEffect(); + + const [createCoreViewFieldMutation] = useCreateCoreViewFieldMutation(); + const [updateCoreViewFieldMutation] = useUpdateCoreViewFieldMutation(); + const [deleteCoreViewFieldMutation] = useDeleteCoreViewFieldMutation(); + const [destroyCoreViewFieldMutation] = useDestroyCoreViewFieldMutation(); + + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + + const createViewFields = useCallback( + async ( + createCoreViewFieldInputs: CreateCoreViewFieldMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (createCoreViewFieldInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + createCoreViewFieldInputs.map(async (variables) => + createCoreViewFieldMutation({ + variables, + update: (_cache, { data }) => { + const createdViewField = data?.createCoreViewField; + if (!isDefined(createdViewField)) { + return; + } + + triggerViewFieldOptimisticEffect({ + createdViewFields: [createdViewField], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewField', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFieldOptimisticEffect, + createCoreViewFieldMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const updateViewFields = useCallback( + async ( + createCoreViewFieldInputs: UpdateCoreViewFieldMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (createCoreViewFieldInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + createCoreViewFieldInputs.map((variables) => + updateCoreViewFieldMutation({ + variables, + update: (_cache, { data }) => { + const updatedViewField = data?.updateCoreViewField; + if (!isDefined(updatedViewField)) { + return; + } + + triggerViewFieldOptimisticEffect({ + updatedViewFields: [updatedViewField], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewField', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFieldOptimisticEffect, + updateCoreViewFieldMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const deleteViewFields = useCallback( + async ( + deleteCoreViewFieldInputs: DeleteCoreViewFieldMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (deleteCoreViewFieldInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + deleteCoreViewFieldInputs.map((variables) => + deleteCoreViewFieldMutation({ + variables, + update: (_cache, { data }) => { + const deletedViewField = data?.deleteCoreViewField; + if (!isDefined(deletedViewField)) { + return; + } + + triggerViewFieldOptimisticEffect({ + deletedViewFields: [deletedViewField], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewField', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFieldOptimisticEffect, + deleteCoreViewFieldMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const destroyViewFields = useCallback( + async ( + destroyCoreViewFieldInputs: DestroyCoreViewFieldMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (destroyCoreViewFieldInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + destroyCoreViewFieldInputs.map((variables) => + destroyCoreViewFieldMutation({ + variables, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewField', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [destroyCoreViewFieldMutation, handleMetadataError, enqueueErrorSnackBar], + ); + + return { + createViewFields, + updateViewFields, + deleteViewFields, + destroyViewFields, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts deleted file mode 100644 index 2e2aa46c1e..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { useCallback } from 'react'; - -import { useTriggerViewFieldOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect'; -import { isDefined } from 'twenty-shared/utils'; -import { - type CreateCoreViewFieldMutationVariables, - type DeleteCoreViewFieldMutationVariables, - type DestroyCoreViewFieldMutationVariables, - type UpdateCoreViewFieldMutationVariables, - useCreateCoreViewFieldMutation, - useDeleteCoreViewFieldMutation, - useDestroyCoreViewFieldMutation, - useUpdateCoreViewFieldMutation, -} from '~/generated/graphql'; - -export const usePersistViewFieldRecords = () => { - const { triggerViewFieldOptimisticEffect } = - useTriggerViewFieldOptimisticEffect(); - - const [createCoreViewFieldMutation] = useCreateCoreViewFieldMutation(); - const [updateCoreViewFieldMutation] = useUpdateCoreViewFieldMutation(); - const [deleteCoreViewFieldMutation] = useDeleteCoreViewFieldMutation(); - const [destroyCoreViewFieldMutation] = useDestroyCoreViewFieldMutation(); - - const createCoreViewFieldRecords = useCallback( - (createCoreViewFieldInputs: CreateCoreViewFieldMutationVariables[]) => { - if (createCoreViewFieldInputs.length === 0) { - return; - } - - return Promise.all( - createCoreViewFieldInputs.map(async (variables) => - createCoreViewFieldMutation({ - variables, - update: (_cache, { data }) => { - const createdViewField = data?.createCoreViewField; - if (!isDefined(createdViewField)) { - return; - } - - triggerViewFieldOptimisticEffect({ - createdViewFields: [createdViewField], - }); - }, - }), - ), - ); - }, - [triggerViewFieldOptimisticEffect, createCoreViewFieldMutation], - ); - - const updateCoreViewFieldRecords = useCallback( - (createCoreViewFieldInputs: UpdateCoreViewFieldMutationVariables[]) => { - if (createCoreViewFieldInputs.length === 0) { - return; - } - - return Promise.all( - createCoreViewFieldInputs.map((variables) => - updateCoreViewFieldMutation({ - variables, - update: (_cache, { data }) => { - const updatedViewField = data?.updateCoreViewField; - if (!isDefined(updatedViewField)) { - return; - } - - triggerViewFieldOptimisticEffect({ - updatedViewFields: [updatedViewField], - }); - }, - }), - ), - ); - }, - [triggerViewFieldOptimisticEffect, updateCoreViewFieldMutation], - ); - - const deleteCoreViewFieldRecords = useCallback( - (deleteCoreViewFieldInputs: DeleteCoreViewFieldMutationVariables[]) => { - if (deleteCoreViewFieldInputs.length === 0) { - return; - } - - return Promise.all( - deleteCoreViewFieldInputs.map((variables) => - deleteCoreViewFieldMutation({ - variables, - update: (_cache, { data }) => { - const deletedViewField = data?.deleteCoreViewField; - if (!isDefined(deletedViewField)) { - return; - } - - triggerViewFieldOptimisticEffect({ - deletedViewFields: [deletedViewField], - }); - }, - }), - ), - ); - }, - [triggerViewFieldOptimisticEffect, deleteCoreViewFieldMutation], - ); - - const destroyCoreViewFieldRecords = useCallback( - (destroyCoreViewFieldInputs: DestroyCoreViewFieldMutationVariables[]) => { - if (destroyCoreViewFieldInputs.length === 0) { - return; - } - - return Promise.all( - destroyCoreViewFieldInputs.map((variables) => - destroyCoreViewFieldMutation({ - variables, - }), - ), - ); - }, - [destroyCoreViewFieldMutation], - ); - - return { - createViewFieldRecords: createCoreViewFieldRecords, - updateViewFieldRecords: updateCoreViewFieldRecords, - deleteViewFieldRecords: deleteCoreViewFieldRecords, - destroyViewFieldRecords: destroyCoreViewFieldRecords, - }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts new file mode 100644 index 0000000000..f236dab241 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilter.ts @@ -0,0 +1,267 @@ +import { useCallback } from 'react'; + +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useTriggerViewFilterOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFilterOptimisticEffect'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { + type CreateCoreViewFilterMutationVariables, + type DeleteCoreViewFilterMutationVariables, + type DestroyCoreViewFilterMutationVariables, + type UpdateCoreViewFilterMutationVariables, + useCreateCoreViewFilterMutation, + useDeleteCoreViewFilterMutation, + useDestroyCoreViewFilterMutation, + useUpdateCoreViewFilterMutation, +} from '~/generated/graphql'; + +export const usePersistViewFilterRecords = () => { + const { triggerViewFilterOptimisticEffect } = + useTriggerViewFilterOptimisticEffect(); + const [createCoreViewFilterMutation] = useCreateCoreViewFilterMutation(); + const [updateCoreViewFilterMutation] = useUpdateCoreViewFilterMutation(); + const [deleteCoreViewFilterMutation] = useDeleteCoreViewFilterMutation(); + const [destroyCoreViewFilterMutation] = useDestroyCoreViewFilterMutation(); + + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + + const createViewFilters = useCallback( + async ( + createCoreViewFilterInputs: CreateCoreViewFilterMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (createCoreViewFilterInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + createCoreViewFilterInputs.map((variables) => + createCoreViewFilterMutation({ + variables, + update: (_cache, { data }) => { + const createdViewFilter = data?.createCoreViewFilter; + if (!isDefined(createdViewFilter)) { + return; + } + + triggerViewFilterOptimisticEffect({ + createdViewFilters: [createdViewFilter], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewFilter', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFilterOptimisticEffect, + createCoreViewFilterMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const updateViewFilters = useCallback( + async ( + updateCoreViewFilterInputs: UpdateCoreViewFilterMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (updateCoreViewFilterInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + updateCoreViewFilterInputs.map((variables) => + updateCoreViewFilterMutation({ + variables, + update: (_cache, { data }) => { + const updatedViewFilter = data?.updateCoreViewFilter; + if (!isDefined(updatedViewFilter)) { + return; + } + + triggerViewFilterOptimisticEffect({ + updatedViewFilters: [updatedViewFilter], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewFilter', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFilterOptimisticEffect, + updateCoreViewFilterMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const deleteViewFilters = useCallback( + async ( + deleteCoreViewFilterInputs: DeleteCoreViewFilterMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (deleteCoreViewFilterInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + deleteCoreViewFilterInputs.map((variables) => + deleteCoreViewFilterMutation({ + variables, + update: (_cache, { data }) => { + const deletedViewFilter = data?.deleteCoreViewFilter; + if (!isDefined(deletedViewFilter)) { + return; + } + + triggerViewFilterOptimisticEffect({ + deletedViewFilters: [deletedViewFilter], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewFilter', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewFilterOptimisticEffect, + deleteCoreViewFilterMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const destroyViewFilters = useCallback( + async ( + destroyCoreViewFilterInputs: DestroyCoreViewFilterMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (destroyCoreViewFilterInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + destroyCoreViewFilterInputs.map((variables) => + destroyCoreViewFilterMutation({ + variables, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewFilter', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [destroyCoreViewFilterMutation, handleMetadataError, enqueueErrorSnackBar], + ); + + return { + createViewFilters, + updateViewFilters, + deleteViewFilters, + destroyViewFilters, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroupRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroup.ts similarity index 91% rename from packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroupRecords.ts rename to packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroup.ts index c6e3bad900..b4e2282d9b 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroupRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterGroup.ts @@ -39,7 +39,7 @@ export const usePersistViewFilterGroupRecords = () => { [apolloClient], ); - const createCoreViewFilterGroupRecords = useCallback( + const createViewFilterGroups = useCallback( async ( viewFilterGroupsToCreate: ViewFilterGroup[], view: Pick, @@ -80,7 +80,7 @@ export const usePersistViewFilterGroupRecords = () => { [createCoreViewFilterGroupRecord], ); - const updateCoreViewFilterGroupRecords = useCallback( + const updateViewFilterGroups = useCallback( (viewFilterGroupsToUpdate: ViewFilterGroup[]) => { if (!viewFilterGroupsToUpdate.length) return; return Promise.all( @@ -104,7 +104,7 @@ export const usePersistViewFilterGroupRecords = () => { [apolloClient], ); - const deleteCoreViewFilterGroupRecords = useCallback( + const deleteViewFilterGroups = useCallback( (viewFilterGroupIdsToDelete: string[]) => { if (!viewFilterGroupIdsToDelete.length) return; return Promise.all( @@ -122,8 +122,8 @@ export const usePersistViewFilterGroupRecords = () => { ); return { - createViewFilterGroupRecords: createCoreViewFilterGroupRecords, - updateViewFilterGroupRecords: updateCoreViewFilterGroupRecords, - deleteViewFilterGroupRecords: deleteCoreViewFilterGroupRecords, + createViewFilterGroups, + updateViewFilterGroups, + deleteViewFilterGroups, }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts deleted file mode 100644 index 5e412dbe8d..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFilterRecords.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { useCallback } from 'react'; - -import { useTriggerViewFilterOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFilterOptimisticEffect'; -import { isDefined } from 'twenty-shared/utils'; -import { - type CreateCoreViewFilterMutationVariables, - type DeleteCoreViewFilterMutationVariables, - type DestroyCoreViewFilterMutationVariables, - type UpdateCoreViewFilterMutationVariables, - useCreateCoreViewFilterMutation, - useDeleteCoreViewFilterMutation, - useDestroyCoreViewFilterMutation, - useUpdateCoreViewFilterMutation, -} from '~/generated/graphql'; - -export const usePersistViewFilterRecords = () => { - const { triggerViewFilterOptimisticEffect } = - useTriggerViewFilterOptimisticEffect(); - const [createCoreViewFilterMutation] = useCreateCoreViewFilterMutation(); - const [updateCoreViewFilterMutation] = useUpdateCoreViewFilterMutation(); - const [deleteCoreViewFilterMutation] = useDeleteCoreViewFilterMutation(); - const [destroyCoreViewFilterMutation] = useDestroyCoreViewFilterMutation(); - - const createCoreViewFilterRecords = useCallback( - (createCoreViewFilterInputs: CreateCoreViewFilterMutationVariables[]) => { - if (createCoreViewFilterInputs.length === 0) { - return; - } - - return Promise.all( - createCoreViewFilterInputs.map((variables) => - createCoreViewFilterMutation({ - variables, - update: (_cache, { data }) => { - const createdViewFilter = data?.createCoreViewFilter; - if (!isDefined(createdViewFilter)) { - return; - } - - triggerViewFilterOptimisticEffect({ - createdViewFilters: [createdViewFilter], - }); - }, - }), - ), - ); - }, - [triggerViewFilterOptimisticEffect, createCoreViewFilterMutation], - ); - - const updateCoreViewFilterRecords = useCallback( - (updateCoreViewFilterInputs: UpdateCoreViewFilterMutationVariables[]) => { - if (updateCoreViewFilterInputs.length === 0) { - return; - } - - return Promise.all( - updateCoreViewFilterInputs.map((variables) => - updateCoreViewFilterMutation({ - variables, - update: (_cache, { data }) => { - const updatedViewFilter = data?.updateCoreViewFilter; - if (!isDefined(updatedViewFilter)) { - return; - } - - triggerViewFilterOptimisticEffect({ - updatedViewFilters: [updatedViewFilter], - }); - }, - }), - ), - ); - }, - [triggerViewFilterOptimisticEffect, updateCoreViewFilterMutation], - ); - - const deleteCoreViewFilterRecords = useCallback( - (deleteCoreViewFilterInputs: DeleteCoreViewFilterMutationVariables[]) => { - if (deleteCoreViewFilterInputs.length === 0) { - return; - } - - return Promise.all( - deleteCoreViewFilterInputs.map((variables) => - deleteCoreViewFilterMutation({ - variables, - update: (_cache, { data }) => { - const deletedViewFilter = data?.deleteCoreViewFilter; - if (!isDefined(deletedViewFilter)) { - return; - } - - triggerViewFilterOptimisticEffect({ - deletedViewFilters: [deletedViewFilter], - }); - }, - }), - ), - ); - }, - [triggerViewFilterOptimisticEffect, deleteCoreViewFilterMutation], - ); - - const destroyCoreViewFilterRecords = useCallback( - (destroyCoreViewFilterInputs: DestroyCoreViewFilterMutationVariables[]) => { - if (destroyCoreViewFilterInputs.length === 0) { - return; - } - - return Promise.all( - destroyCoreViewFilterInputs.map((variables) => - destroyCoreViewFilterMutation({ - variables, - }), - ), - ); - }, - [destroyCoreViewFilterMutation], - ); - - return { - createViewFilterRecords: createCoreViewFilterRecords, - updateViewFilterRecords: updateCoreViewFilterRecords, - deleteViewFilterRecords: deleteCoreViewFilterRecords, - destroyViewFilterRecords: destroyCoreViewFilterRecords, - }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts new file mode 100644 index 0000000000..f2528af025 --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroup.ts @@ -0,0 +1,267 @@ +import { useCallback } from 'react'; + +import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler'; +import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect'; +import { ApolloError } from '@apollo/client'; +import { t } from '@lingui/core/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { + type CreateCoreViewGroupMutationVariables, + type DeleteCoreViewGroupMutationVariables, + type DestroyCoreViewGroupMutationVariables, + type UpdateCoreViewGroupMutationVariables, + useCreateCoreViewGroupMutation, + useDeleteCoreViewGroupMutation, + useDestroyCoreViewGroupMutation, + useUpdateCoreViewGroupMutation, +} from '~/generated/graphql'; + +export const usePersistViewGroupRecords = () => { + const { triggerViewGroupOptimisticEffect } = + useTriggerViewGroupOptimisticEffect(); + const [createCoreViewGroupMutation] = useCreateCoreViewGroupMutation(); + const [updateCoreViewGroupMutation] = useUpdateCoreViewGroupMutation(); + const [deleteCoreViewGroupMutation] = useDeleteCoreViewGroupMutation(); + const [destroyCoreViewGroupMutation] = useDestroyCoreViewGroupMutation(); + + const { handleMetadataError } = useMetadataErrorHandler(); + const { enqueueErrorSnackBar } = useSnackBar(); + + const createViewGroups = useCallback( + async ( + createCoreViewGroupInputs: CreateCoreViewGroupMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (createCoreViewGroupInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + createCoreViewGroupInputs.map((variables) => + createCoreViewGroupMutation({ + variables, + update: (_cache, { data }) => { + const createdViewGroup = data?.createCoreViewGroup; + if (!isDefined(createdViewGroup)) { + return; + } + + triggerViewGroupOptimisticEffect({ + createdViewGroups: [createdViewGroup], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewGroup', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewGroupOptimisticEffect, + createCoreViewGroupMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const updateViewGroups = useCallback( + async ( + updateCoreViewGroupInputs: UpdateCoreViewGroupMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (updateCoreViewGroupInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + updateCoreViewGroupInputs.map((variables) => + updateCoreViewGroupMutation({ + variables, + update: (_cache, { data }) => { + const updatedViewGroup = data?.updateCoreViewGroup; + if (!isDefined(updatedViewGroup)) { + return; + } + + triggerViewGroupOptimisticEffect({ + updatedViewGroups: [updatedViewGroup], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewGroup', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewGroupOptimisticEffect, + updateCoreViewGroupMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const deleteViewGroups = useCallback( + async ( + deleteCoreViewGroupInputs: DeleteCoreViewGroupMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (deleteCoreViewGroupInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + deleteCoreViewGroupInputs.map((variables) => + deleteCoreViewGroupMutation({ + variables, + update: (_cache, { data }) => { + const deletedViewGroup = data?.deleteCoreViewGroup; + if (!isDefined(deletedViewGroup)) { + return; + } + + triggerViewGroupOptimisticEffect({ + deletedViewGroups: [deletedViewGroup], + }); + }, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewGroup', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [ + triggerViewGroupOptimisticEffect, + deleteCoreViewGroupMutation, + handleMetadataError, + enqueueErrorSnackBar, + ], + ); + + const destroyViewGroups = useCallback( + async ( + destroyCoreViewGroupInputs: DestroyCoreViewGroupMutationVariables[], + ): Promise< + MetadataRequestResult< + Awaited>[] + > + > => { + if (destroyCoreViewGroupInputs.length === 0) { + return { + status: 'successful', + response: [], + }; + } + + try { + const results = await Promise.all( + destroyCoreViewGroupInputs.map((variables) => + destroyCoreViewGroupMutation({ + variables, + }), + ), + ); + + return { + status: 'successful', + response: results, + }; + } catch (error) { + if (error instanceof ApolloError) { + handleMetadataError(error, { + primaryMetadataName: 'viewGroup', + }); + } else { + enqueueErrorSnackBar({ message: t`An error occurred.` }); + } + + return { + status: 'failed', + error, + }; + } + }, + [destroyCoreViewGroupMutation, handleMetadataError, enqueueErrorSnackBar], + ); + + return { + createViewGroups, + updateViewGroups, + deleteViewGroups, + destroyViewGroups, + }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts deleted file mode 100644 index dbff7ac0aa..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewGroupRecords.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { useCallback } from 'react'; - -import { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect'; -import { isDefined } from 'twenty-shared/utils'; -import { - type CreateCoreViewGroupMutationVariables, - type DeleteCoreViewGroupMutationVariables, - type DestroyCoreViewGroupMutationVariables, - type UpdateCoreViewGroupMutationVariables, - useCreateCoreViewGroupMutation, - useDeleteCoreViewGroupMutation, - useDestroyCoreViewGroupMutation, - useUpdateCoreViewGroupMutation, -} from '~/generated/graphql'; - -export const usePersistViewGroupRecords = () => { - const { triggerViewGroupOptimisticEffect } = - useTriggerViewGroupOptimisticEffect(); - const [createCoreViewGroupMutation] = useCreateCoreViewGroupMutation(); - const [updateCoreViewGroupMutation] = useUpdateCoreViewGroupMutation(); - const [deleteCoreViewGroupMutation] = useDeleteCoreViewGroupMutation(); - const [destroyCoreViewGroupMutation] = useDestroyCoreViewGroupMutation(); - - const createCoreViewGroupRecords = useCallback( - (createCoreViewGroupInputs: CreateCoreViewGroupMutationVariables[]) => { - if (createCoreViewGroupInputs.length === 0) { - return; - } - - return Promise.all( - createCoreViewGroupInputs.map((variables) => - createCoreViewGroupMutation({ - variables, - update: (_cache, { data }) => { - const createdViewGroup = data?.createCoreViewGroup; - if (!isDefined(createdViewGroup)) { - return; - } - - triggerViewGroupOptimisticEffect({ - createdViewGroups: [createdViewGroup], - }); - }, - }), - ), - ); - }, - [triggerViewGroupOptimisticEffect, createCoreViewGroupMutation], - ); - - const updateCoreViewGroupRecords = useCallback( - (updateCoreViewGroupInputs: UpdateCoreViewGroupMutationVariables[]) => { - if (updateCoreViewGroupInputs.length === 0) { - return; - } - - return Promise.all( - updateCoreViewGroupInputs.map((variables) => - updateCoreViewGroupMutation({ - variables, - update: (_cache, { data }) => { - const updatedViewGroup = data?.updateCoreViewGroup; - if (!isDefined(updatedViewGroup)) { - return; - } - - triggerViewGroupOptimisticEffect({ - updatedViewGroups: [updatedViewGroup], - }); - }, - }), - ), - ); - }, - [triggerViewGroupOptimisticEffect, updateCoreViewGroupMutation], - ); - - const deleteCoreViewGroupRecords = useCallback( - (deleteCoreViewGroupInputs: DeleteCoreViewGroupMutationVariables[]) => { - if (deleteCoreViewGroupInputs.length === 0) { - return; - } - - return Promise.all( - deleteCoreViewGroupInputs.map((variables) => - deleteCoreViewGroupMutation({ - variables, - update: (_cache, { data }) => { - const deletedViewGroup = data?.deleteCoreViewGroup; - if (!isDefined(deletedViewGroup)) { - return; - } - - triggerViewGroupOptimisticEffect({ - deletedViewGroups: [deletedViewGroup], - }); - }, - }), - ), - ); - }, - [triggerViewGroupOptimisticEffect, deleteCoreViewGroupMutation], - ); - - const destroyCoreViewGroupRecords = useCallback( - (destroyCoreViewGroupInputs: DestroyCoreViewGroupMutationVariables[]) => { - if (destroyCoreViewGroupInputs.length === 0) { - return; - } - - return Promise.all( - destroyCoreViewGroupInputs.map((variables) => - destroyCoreViewGroupMutation({ - variables, - }), - ), - ); - }, - [destroyCoreViewGroupMutation], - ); - - return { - createViewGroupRecords: createCoreViewGroupRecords, - updateViewGroupRecords: updateCoreViewGroupRecords, - deleteViewGroupRecords: deleteCoreViewGroupRecords, - destroyViewGroupRecords: destroyCoreViewGroupRecords, - }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewSortRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewSort.ts similarity index 91% rename from packages/twenty-front/src/modules/views/hooks/internal/usePersistViewSortRecords.ts rename to packages/twenty-front/src/modules/views/hooks/internal/usePersistViewSort.ts index c915221633..c300ec37cd 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewSortRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewSort.ts @@ -16,7 +16,7 @@ export const usePersistViewSortRecords = () => { const { triggerViewSortOptimisticEffect } = useTriggerViewSortOptimisticEffect(); - const createCoreViewSortRecords = useCallback( + const createViewSorts = useCallback( ( viewSortsToCreate: CoreViewSortEssential[], view: Pick, @@ -49,7 +49,7 @@ export const usePersistViewSortRecords = () => { [apolloClient, triggerViewSortOptimisticEffect], ); - const updateCoreViewSortRecords = useCallback( + const updateViewSorts = useCallback( (viewSortsToUpdate: CoreViewSortEssential[]) => { if (!viewSortsToUpdate.length) return; return Promise.all( @@ -77,7 +77,7 @@ export const usePersistViewSortRecords = () => { [apolloClient, triggerViewSortOptimisticEffect], ); - const deleteCoreViewSortRecords = useCallback( + const deleteViewSorts = useCallback( (viewSortsToDelete: Pick[]) => { if (!viewSortsToDelete.length) return; return Promise.all( @@ -100,8 +100,8 @@ export const usePersistViewSortRecords = () => { ); return { - createViewSortRecords: createCoreViewSortRecords, - updateViewSortRecords: updateCoreViewSortRecords, - deleteViewSortRecords: deleteCoreViewSortRecords, + createViewSorts, + updateViewSorts, + deleteViewSorts, }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts index b84cc8b5cf..e357c0bf99 100644 --- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts @@ -7,11 +7,12 @@ import { currentRecordSortsComponentState } from '@/object-record/record-sort/st import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords'; -import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords'; -import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; -import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; -import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; +import { usePersistView } from '@/views/hooks/internal/usePersistView'; +import { usePersistViewField } from '@/views/hooks/internal/usePersistViewField'; +import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilter'; +import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroup'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; +import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSort'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState'; import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; @@ -28,14 +29,12 @@ import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { ViewCalendarLayout } from '~/generated-metadata/graphql'; -import { - type CreateCoreViewFieldMutationVariables, - useCreateCoreViewMutation, -} from '~/generated/graphql'; +import { type CreateCoreViewFieldMutationVariables } from '~/generated/graphql'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { - const [createCoreViewMutation] = useCreateCoreViewMutation(); + const { createView } = usePersistView(); + const currentViewIdCallbackState = useRecoilComponentCallbackState( contextStoreCurrentViewIdComponentState, viewBarComponentId, @@ -45,15 +44,15 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { anyFieldFilterValueComponentState, ); - const { createViewFieldRecords } = usePersistViewFieldRecords(); + const { createViewFields } = usePersistViewField(); - const { createViewSortRecords } = usePersistViewSortRecords(); + const { createViewSorts } = usePersistViewSortRecords(); - const { createViewGroupRecords } = usePersistViewGroupRecords(); + const { createViewGroups } = usePersistViewGroupRecords(); - const { createViewFilterRecords } = usePersistViewFilterRecords(); + const { createViewFilters } = usePersistViewFilterRecords(); - const { createViewFilterGroupRecords } = usePersistViewFilterGroupRecords(); + const { createViewFilterGroups } = usePersistViewFilterGroupRecords(); const { objectMetadataItem } = useRecordIndexContextOrThrow(); @@ -120,44 +119,49 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { const viewType = type ?? sourceView.type; - const result = await createCoreViewMutation({ - variables: { - input: { - id: id ?? v4(), - name: name ?? sourceView.name, - icon: icon ?? sourceView.icon, - key: null, - kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate - ? sourceView.kanbanAggregateOperation + const result = await createView({ + input: { + id: id ?? v4(), + name: name ?? sourceView.name, + icon: icon ?? sourceView.icon, + key: null, + kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate + ? sourceView.kanbanAggregateOperation + : undefined, + kanbanAggregateOperationFieldMetadataId: + shouldCopyFiltersAndSortsAndAggregate + ? sourceView.kanbanAggregateOperationFieldMetadataId + : undefined, + type: convertViewTypeToCore(viewType), + objectMetadataId: sourceView.objectMetadataId, + openRecordIn: convertViewOpenRecordInToCore( + sourceView.openRecordIn, + ), + anyFieldFilterValue: anyFieldFilterValue, + calendarLayout: + viewType === ViewType.Calendar + ? ViewCalendarLayout.MONTH + : undefined, + calendarFieldMetadataId: + viewType === ViewType.Calendar + ? calendarFieldMetadataId : undefined, - kanbanAggregateOperationFieldMetadataId: - shouldCopyFiltersAndSortsAndAggregate - ? sourceView.kanbanAggregateOperationFieldMetadataId - : undefined, - type: convertViewTypeToCore(viewType), - objectMetadataId: sourceView.objectMetadataId, - openRecordIn: convertViewOpenRecordInToCore( - sourceView.openRecordIn, - ), - anyFieldFilterValue: anyFieldFilterValue, - calendarLayout: - viewType === ViewType.Calendar - ? ViewCalendarLayout.MONTH - : undefined, - calendarFieldMetadataId: - viewType === ViewType.Calendar - ? calendarFieldMetadataId - : undefined, - }, }, }); - const newViewId = result.data?.createCoreView.id; + + if (result.status === 'failed') { + set(isPersistingViewFieldsState, false); + return undefined; + } + + const newViewId = result.response.data?.createCoreView.id; if (isUndefinedOrNull(newViewId)) { + set(isPersistingViewFieldsState, false); throw new Error('Failed to create view'); } - await createViewFieldRecords( + const fieldResult = await createViewFields( sourceView.viewFields.map( ({ __typename, id: _id, ...viewField }) => ({ input: { ...viewField, id: v4(), viewId: newViewId }, @@ -165,6 +169,11 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { ), ); + if (fieldResult.status === 'failed') { + set(isPersistingViewFieldsState, false); + return undefined; + } + if (type === ViewType.Kanban) { if (!isDefined(kanbanFieldMetadataId)) { throw new Error('Kanban view must have a kanban field'); @@ -194,7 +203,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { fieldMetadataId: kanbanFieldMetadataId, } satisfies ViewGroup); - await createViewGroupRecords( + const groupResult = await createViewGroups( viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ input: { ...viewGroup, @@ -202,6 +211,11 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { }, })), ); + + if (groupResult.status === 'failed') { + set(isPersistingViewFieldsState, false); + return undefined; + } } if (shouldCopyFiltersAndSortsAndAggregate === true) { @@ -232,9 +246,10 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { id: v4(), })); - await createViewFilterGroupRecords(viewFilterGroupsToCreate, { + await createViewFilterGroups(viewFilterGroupsToCreate, { id: newViewId, }); + const createViewFilterInputs = viewFiltersToCreate.map( (viewFilter) => ({ input: { @@ -249,8 +264,15 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { }, }), ); - await createViewFilterRecords(createViewFilterInputs); - await createViewSortRecords(viewSortsToCreate, { id: newViewId }); + + const filterResult = await createViewFilters(createViewFilterInputs); + + if (filterResult.status === 'failed') { + set(isPersistingViewFieldsState, false); + return undefined; + } + + await createViewSorts(viewSortsToCreate, { id: newViewId }); } await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id); @@ -260,18 +282,18 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => { }, [ currentViewIdCallbackState, - createViewFieldRecords, - createCoreViewMutation, + createViewFields, + createView, anyFieldFilterValue, objectMetadataItem.fields, objectMetadataItem.id, - createViewGroupRecords, + createViewGroups, currentRecordFilterGroups, currentRecordFilters, currentRecordSorts, - createViewFilterGroupRecords, - createViewFilterRecords, - createViewSortRecords, + createViewFilterGroups, + createViewFilters, + createViewSorts, refreshCoreViewsByObjectMetadataId, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useDeleteView.ts b/packages/twenty-front/src/modules/views/hooks/useDeleteView.ts deleted file mode 100644 index 16cf15841b..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/useDeleteView.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; -import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { useDeleteCoreViewMutation } from '~/generated/graphql'; - -export const useDeleteView = () => { - const [deleteCoreViewMutation] = useDeleteCoreViewMutation(); - const { refreshCoreViewsByObjectMetadataId } = - useRefreshCoreViewsByObjectMetadataId(); - - const deleteView = useRecoilCallback( - ({ snapshot }) => - async (viewId: string) => { - const currentView = snapshot - .getLoadable( - coreViewFromViewIdFamilySelector({ - viewId, - }), - ) - .getValue(); - - if (!isDefined(currentView)) { - return; - } - - await deleteCoreViewMutation({ - variables: { - id: viewId, - }, - }); - - await refreshCoreViewsByObjectMetadataId(currentView.objectMetadataId); - }, - [deleteCoreViewMutation, refreshCoreViewsByObjectMetadataId], - ); - - return { deleteView }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveAnyFieldFilterToView.ts b/packages/twenty-front/src/modules/views/hooks/useSaveAnyFieldFilterToView.ts index 24d0fe3105..8537a552b7 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveAnyFieldFilterToView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveAnyFieldFilterToView.ts @@ -1,12 +1,13 @@ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; +import { usePersistView } from '@/views/hooks/internal/usePersistView'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; -import { useUpdateView } from '@/views/hooks/useUpdateView'; +import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const useSaveAnyFieldFilterToView = () => { - const { updateView } = useUpdateView(); + const { updateView } = usePersistView(); const { currentView } = useGetCurrentViewOnly(); @@ -28,10 +29,17 @@ export const useSaveAnyFieldFilterToView = () => { .getValue(); if (currentAnyFieldFilterValue !== currentViewAnyFieldFilterValue) { - await updateView({ + const formattedCurrentView = convertUpdateViewInputToCore({ ...currentView, anyFieldFilterValue: currentAnyFieldFilterValue, }); + await updateView({ + id: currentView.id, + input: { + ...formattedCurrentView, + anyFieldFilterValue: currentAnyFieldFilterValue, + }, + }); } }, [updateView, anyFieldFilterValueCallbackState, currentView], diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts index af939e6226..7610b6f7e8 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFields.ts @@ -2,7 +2,7 @@ import { useRecoilCallback } from 'recoil'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { usePersistViewFieldRecords } from '@/views/hooks/internal/usePersistViewFieldRecords'; +import { usePersistViewField } from '@/views/hooks/internal/usePersistViewField'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState'; import { type ViewField } from '@/views/types/ViewField'; @@ -15,8 +15,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useSaveCurrentViewFields = () => { - const { createViewFieldRecords, updateViewFieldRecords } = - usePersistViewFieldRecords(); + const { createViewFields, updateViewFields } = usePersistViewField(); const { getViewFromPrefetchState } = useGetViewFromPrefetchState(); @@ -120,17 +119,17 @@ export const useSaveCurrentViewFields = () => { ); await Promise.all([ - createViewFieldRecords(viewFieldsToCreate), - updateViewFieldRecords(viewFieldsToUpdate), + createViewFields(viewFieldsToCreate), + updateViewFields(viewFieldsToUpdate), ]); set(isPersistingViewFieldsState, false); }, [ - createViewFieldRecords, + createViewFields, currentViewIdCallbackState, getViewFromPrefetchState, - updateViewFieldRecords, + updateViewFields, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts index 4f377c330c..07e1eccea9 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts @@ -2,7 +2,7 @@ import { useRecoilCallback } from 'recoil'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; -import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords'; +import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroup'; import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState'; import { type ViewGroup } from '@/views/types/ViewGroup'; import { isDefined } from 'twenty-shared/utils'; @@ -10,8 +10,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const useSaveCurrentViewGroups = () => { - const { createViewGroupRecords, updateViewGroupRecords } = - usePersistViewGroupRecords(); + const { createViewGroups, updateViewGroups } = usePersistViewGroupRecords(); const { getViewFromPrefetchState } = useGetViewFromPrefetchState(); @@ -62,7 +61,7 @@ export const useSaveCurrentViewGroups = () => { return; } - await updateViewGroupRecords([ + await updateViewGroups([ { input: { id: existingField.id, @@ -76,11 +75,7 @@ export const useSaveCurrentViewGroups = () => { }, ]); }, - [ - currentViewIdCallbackState, - getViewFromPrefetchState, - updateViewGroupRecords, - ], + [currentViewIdCallbackState, getViewFromPrefetchState, updateViewGroups], ); const saveViewGroups = useRecoilCallback( @@ -94,7 +89,7 @@ export const useSaveCurrentViewGroups = () => { return; } - const view = await getViewFromPrefetchState(currentViewId); + const view = getViewFromPrefetchState(currentViewId); if (isUndefinedOrNull(view)) { return; @@ -151,7 +146,7 @@ export const useSaveCurrentViewGroups = () => { ); await Promise.all([ - createViewGroupRecords( + createViewGroups( viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({ input: { ...viewGroup, @@ -159,14 +154,14 @@ export const useSaveCurrentViewGroups = () => { }, })), ), - updateViewGroupRecords(viewGroupsToUpdate), + updateViewGroups(viewGroupsToUpdate), ]); }, [ - createViewGroupRecords, + createViewGroups, currentViewIdCallbackState, getViewFromPrefetchState, - updateViewGroupRecords, + updateViewGroups, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts index 85369fa111..1d6600f9a3 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups.ts @@ -1,7 +1,7 @@ import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroupRecords'; +import { usePersistViewFilterGroupRecords } from '@/views/hooks/internal/usePersistViewFilterGroup'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate'; import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete'; @@ -12,9 +12,9 @@ import { isDefined } from 'twenty-shared/utils'; export const useSaveRecordFilterGroupsToViewFilterGroups = () => { const { - createViewFilterGroupRecords, - updateViewFilterGroupRecords, - deleteViewFilterGroupRecords, + createViewFilterGroups, + updateViewFilterGroups, + deleteViewFilterGroups, } = usePersistViewFilterGroupRecords(); const { currentView } = useGetCurrentViewOnly(); @@ -63,19 +63,16 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => { (viewFilterGroup) => viewFilterGroup.id, ); - await createViewFilterGroupRecords( - viewFilterGroupsToCreate, - currentView, - ); - await updateViewFilterGroupRecords(viewFilterGroupsToUpdate); - await deleteViewFilterGroupRecords(viewFilterGroupIdsToDelete); + await createViewFilterGroups(viewFilterGroupsToCreate, currentView); + await updateViewFilterGroups(viewFilterGroupsToUpdate); + await deleteViewFilterGroups(viewFilterGroupIdsToDelete); }, [ currentView, currentRecordFilterGroupsCallbackState, - createViewFilterGroupRecords, - updateViewFilterGroupRecords, - deleteViewFilterGroupRecords, + createViewFilterGroups, + updateViewFilterGroups, + deleteViewFilterGroups, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts index 261a478449..0f3bff7ce7 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordFiltersToViewFilters.ts @@ -1,7 +1,7 @@ import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilterRecords'; +import { usePersistViewFilterRecords } from '@/views/hooks/internal/usePersistViewFilter'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate'; import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete'; @@ -11,11 +11,8 @@ import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const useSaveRecordFiltersToViewFilters = () => { - const { - createViewFilterRecords, - updateViewFilterRecords, - deleteViewFilterRecords, - } = usePersistViewFilterRecords(); + const { createViewFilters, updateViewFilters, deleteViewFilters } = + usePersistViewFilterRecords(); const { currentView } = useGetCurrentViewOnly(); @@ -94,16 +91,27 @@ export const useSaveRecordFiltersToViewFilters = () => { }), ); - await createViewFilterRecords(createViewFilterInputs); - await updateViewFilterRecords(updateViewFilterInputs); - await deleteViewFilterRecords(deleteViewFilterInputs); + const createResult = await createViewFilters(createViewFilterInputs); + if (createResult.status === 'failed') { + return; + } + + const updateResult = await updateViewFilters(updateViewFilterInputs); + if (updateResult.status === 'failed') { + return; + } + + const deleteResult = await deleteViewFilters(deleteViewFilterInputs); + if (deleteResult.status === 'failed') { + return; + } }, [ currentView, currentRecordFiltersCallbackState, - createViewFilterRecords, - updateViewFilterRecords, - deleteViewFilterRecords, + createViewFilters, + updateViewFilters, + deleteViewFilters, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts index eb5a196961..ff6a76d390 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveRecordSortsToViewSorts.ts @@ -1,7 +1,7 @@ import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; -import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords'; +import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSort'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate'; import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete'; @@ -11,11 +11,8 @@ import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; export const useSaveRecordSortsToViewSorts = () => { - const { - createViewSortRecords, - updateViewSortRecords, - deleteViewSortRecords, - } = usePersistViewSortRecords(); + const { createViewSorts, updateViewSorts, deleteViewSorts } = + usePersistViewSortRecords(); const { currentView } = useGetCurrentViewOnly(); @@ -56,16 +53,16 @@ export const useSaveRecordSortsToViewSorts = () => { newViewSorts, ); - await createViewSortRecords(viewSortsToCreate, currentView); - await updateViewSortRecords(viewSortsToUpdate); - await deleteViewSortRecords(viewSortsToDelete); + await createViewSorts(viewSortsToCreate, currentView); + await updateViewSorts(viewSortsToUpdate); + await deleteViewSorts(viewSortsToDelete); }, [ currentView, currentRecordSortsCallbackState, - createViewSortRecords, - updateViewSortRecords, - deleteViewSortRecords, + createViewSorts, + updateViewSorts, + deleteViewSorts, ], ); diff --git a/packages/twenty-front/src/modules/views/hooks/useUpdateView.ts b/packages/twenty-front/src/modules/views/hooks/useUpdateView.ts deleted file mode 100644 index 095e0d0eaf..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/useUpdateView.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; -import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; -import { type GraphQLView } from '@/views/types/GraphQLView'; -import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore'; -import { useRecoilCallback } from 'recoil'; -import { isDefined } from 'twenty-shared/utils'; -import { useUpdateCoreViewMutation } from '~/generated/graphql'; - -export const useUpdateView = () => { - const [updateOneCoreView] = useUpdateCoreViewMutation(); - - const { refreshCoreViewsByObjectMetadataId } = - useRefreshCoreViewsByObjectMetadataId(); - - const { objectMetadataItem } = useRecordIndexContextOrThrow(); - - const updateView = useRecoilCallback( - () => async (view: Partial) => { - if (!isDefined(view.id)) { - return; - } - - await updateOneCoreView({ - variables: { - id: view.id, - input: convertUpdateViewInputToCore(view), - }, - }); - - await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id); - }, - [ - objectMetadataItem.id, - refreshCoreViewsByObjectMetadataId, - updateOneCoreView, - ], - ); - - return { - updateView, - }; -}; diff --git a/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts b/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts index 069ea4558f..0a1102a473 100644 --- a/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts +++ b/packages/twenty-front/src/modules/views/hooks/useUpdateViewAggregate.ts @@ -3,8 +3,8 @@ import { recordIndexKanbanAggregateOperationState } from '@/object-record/record import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations'; import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { usePersistView } from '@/views/hooks/internal/usePersistView'; import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; -import { useUpdateView } from '@/views/hooks/useUpdateView'; import { useCallback } from 'react'; import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -13,7 +13,7 @@ export const useUpdateViewAggregate = () => { const currentViewId = useRecoilComponentValue( contextStoreCurrentViewIdComponentState, ); - const { updateView } = useUpdateView(); + const { updateView } = usePersistView(); const setRecordIndexKanbanAggregateOperationState = useSetRecoilState( recordIndexKanbanAggregateOperationState, @@ -39,10 +39,17 @@ export const useUpdateViewAggregate = () => { kanbanAggregateOperation, ) : null; + + if (!isDefined(currentViewId)) { + return; + } + updateView({ id: currentViewId, - kanbanAggregateOperationFieldMetadataId, - kanbanAggregateOperation: convertedKanbanAggregateOperation, + input: { + kanbanAggregateOperationFieldMetadataId, + kanbanAggregateOperation: convertedKanbanAggregateOperation, + }, }); setRecordIndexKanbanAggregateOperationState({ diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx index 6c0cb11ad2..ba8e841785 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx @@ -10,10 +10,10 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; +import { usePersistView } from '@/views/hooks/internal/usePersistView'; import { useChangeView } from '@/views/hooks/useChangeView'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { useOpenCreateViewDropdown } from '@/views/hooks/useOpenCreateViewDropown'; -import { useUpdateView } from '@/views/hooks/useUpdateView'; import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector'; import { ViewPickerOptionDropdown } from '@/views/view-picker/components/ViewPickerOptionDropdown'; import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId'; @@ -48,7 +48,7 @@ export const ViewPickerListContent = () => { const { setViewPickerMode } = useViewPickerMode(); - const { updateView } = useUpdateView(); + const { updateView } = usePersistView(); const { changeView } = useChangeView(); const { closeDropdown } = useCloseDropdown(); @@ -85,7 +85,7 @@ export const ViewPickerListContent = () => { Promise.all( viewsReordered.map(async (view, index) => { if (view.position !== index) { - await updateView({ id: view.id, position: index }); + await updateView({ id: view.id, input: { position: index } }); } }), ); diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useDeleteViewFromCurrentState.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useDeleteViewFromCurrentState.ts index 325eb561fa..0421818eaa 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useDeleteViewFromCurrentState.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useDeleteViewFromCurrentState.ts @@ -3,8 +3,8 @@ import { useRecoilCallback, useRecoilValue } from 'recoil'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { usePersistView } from '@/views/hooks/internal/usePersistView'; import { useChangeView } from '@/views/hooks/useChangeView'; -import { useDeleteView } from '@/views/hooks/useDeleteView'; import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly'; import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector'; import { useCloseAndResetViewPicker } from '@/views/view-picker/hooks/useCloseAndResetViewPicker'; @@ -43,7 +43,7 @@ export const useDeleteViewFromCurrentState = (viewBarInstanceId?: string) => { const { changeView } = useChangeView(); - const { deleteView } = useDeleteView(); + const { deleteView } = usePersistView(); const deleteViewFromCurrentState = useRecoilCallback( ({ set, snapshot }) => @@ -67,7 +67,7 @@ export const useDeleteViewFromCurrentState = (viewBarInstanceId?: string) => { ); } - await deleteView(viewPickerReferenceViewId); + await deleteView({ id: viewPickerReferenceViewId }); }, [ currentView, diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts index 5371e900d9..a21c17780c 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useUpdateViewFromCurrentState.ts @@ -1,7 +1,7 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { usePersistView } from '@/views/hooks/internal/usePersistView'; import { useChangeView } from '@/views/hooks/useChangeView'; -import { useUpdateView } from '@/views/hooks/useUpdateView'; import { useCloseAndResetViewPicker } from '@/views/view-picker/hooks/useCloseAndResetViewPicker'; import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState'; import { viewPickerIsDirtyComponentState } from '@/views/view-picker/states/viewPickerIsDirtyComponentState'; @@ -32,7 +32,7 @@ export const useUpdateViewFromCurrentState = () => { const viewPickerReferenceViewIdCallbackState = useRecoilComponentCallbackState(viewPickerReferenceViewIdComponentState); - const { updateView } = useUpdateView(); + const { updateView } = usePersistView(); const { changeView } = useChangeView(); const updateViewFromCurrentState = useRecoilCallback( @@ -57,8 +57,10 @@ export const useUpdateViewFromCurrentState = () => { await updateView({ id: viewPickerReferenceViewId, - name: viewPickerInputName, - icon: viewPickerSelectedIcon, + input: { + name: viewPickerInputName, + icon: viewPickerSelectedIcon, + }, }); changeView(viewPickerReferenceViewId); }, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx index a8f7d4b24d..d2dfd7a0a3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx @@ -1,7 +1,6 @@ import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader'; import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; -import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction'; import { useFullScreenModal } from '@/ui/layout/fullscreen/hooks/useFullScreenModal'; import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow'; @@ -39,6 +38,7 @@ import { useLingui } from '@lingui/react/macro'; import { SOURCE_FOLDER_NAME } from '@/serverless-functions/constants/SourceFolderName'; import { computeNewSources } from '@/serverless-functions/utils/computeNewSources'; +import { usePersistServerlessFunction } from '@/settings/serverless-functions/hooks/usePersistServerlessFunction'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; import { CODE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CodeAction'; import { type Monaco } from '@monaco-editor/react'; @@ -105,8 +105,7 @@ export const WorkflowEditActionServerlessFunction = ({ activeTabIdComponentState, WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, ); - const { updateOneServerlessFunction } = - useUpdateOneServerlessFunction(serverlessFunctionId); + const { updateServerlessFunction } = usePersistServerlessFunction(); const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersionOrThrow(); @@ -149,10 +148,15 @@ export const WorkflowEditActionServerlessFunction = ({ }); const handleSave = useDebouncedCallback(async () => { - await updateOneServerlessFunction({ - name: formValues.name, - description: formValues.description, - code: formValues.code, + await updateServerlessFunction({ + input: { + id: serverlessFunctionId, + update: { + name: formValues.name, + description: formValues.description, + code: formValues.code, + }, + }, }); }, 500); diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx index 3909501c39..ab0dd3db9b 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx @@ -1,6 +1,3 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { FormProvider, useForm } from 'react-hook-form'; - import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; @@ -10,11 +7,11 @@ import { type SettingsDataModelObjectAboutFormValues, settingsDataModelObjectAboutFormSchema, } from '@/settings/data-model/validation-schemas/settingsDataModelObjectAboutFormSchema'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; -import { ApolloError } from '@apollo/client'; +import { zodResolver } from '@hookform/resolvers/zod'; import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { H2Title } from 'twenty-ui/display'; @@ -24,7 +21,6 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings'; export const SettingsNewObject = () => { const { t } = useLingui(); const navigate = useNavigateSettings(); - const { enqueueErrorSnackBar } = useSnackBar(); const [isLoading, setIsLoading] = useState(false); const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem(); @@ -43,25 +39,21 @@ export const SettingsNewObject = () => { const handleSave = async ( formValues: SettingsDataModelObjectAboutFormValues, ) => { - try { - setIsLoading(true); - const { data: response } = await createOneObjectMetadataItem(formValues); + setIsLoading(true); + const result = await createOneObjectMetadataItem(formValues); + + if (result.status === 'successful') { + const response = result.response.data; navigate( response ? SettingsPath.ObjectDetail : SettingsPath.Objects, response ? { objectNamePlural: response.createOneObject.namePlural } : undefined, ); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - enqueueErrorSnackBar({ - apolloError: error instanceof ApolloError ? error : undefined, - }); - } finally { - setIsLoading(false); } + + setIsLoading(false); }; return ( diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index 06706ca7be..faa75d399a 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -21,11 +21,9 @@ import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/field import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { shouldNavigateBackToMemorizedUrlOnSaveState } from '@/ui/navigation/states/shouldNavigateBackToMemorizedUrlOnSaveState'; -import { ApolloError } from '@apollo/client'; import { useLingui } from '@lingui/react/macro'; import { useRecoilState } from 'recoil'; import { AppPath, SettingsPath } from 'twenty-shared/types'; @@ -59,8 +57,6 @@ export const SettingsObjectFieldEdit = () => { setShouldNavigateBackToMemorizedUrlOnSave, ] = useRecoilState(shouldNavigateBackToMemorizedUrlOnSaveState); - const { enqueueErrorSnackBar } = useSnackBar(); - const { objectNamePlural = '', fieldName = '' } = useParams(); const { findObjectMetadataItemByNamePlural } = useFilteredObjectMetadataItems(); @@ -127,47 +123,47 @@ export const SettingsObjectFieldEdit = () => { const { dirtyFields } = formConfig.formState; setNewNameDuringSave(formValues.name); - try { - if ( - formValues.type === FieldMetadataType.RELATION && - 'relation' in formValues && - 'relation' in dirtyFields - ) { - const { relationFieldMetadataItem } = - getRelationMetadata({ - fieldMetadataItem: fieldMetadataItem, - }) ?? {}; + if ( + formValues.type === FieldMetadataType.RELATION && + 'relation' in formValues && + 'relation' in dirtyFields + ) { + const { relationFieldMetadataItem } = + getRelationMetadata({ + fieldMetadataItem: fieldMetadataItem, + }) ?? {}; - if (isDefined(relationFieldMetadataItem)) { - await updateOneFieldMetadataItem({ - objectMetadataId: objectMetadataItem.id, - fieldMetadataIdToUpdate: relationFieldMetadataItem.id, - updatePayload: formValues.relation.field, - }); - } - } - - const otherDirtyFields = omit(dirtyFields, 'relation'); - - if (Object.keys(otherDirtyFields).length > 0) { - const formattedInput = Object.fromEntries( - Object.entries(formatFieldMetadataItemInput(formValues)).filter( - ([key]) => Object.keys(otherDirtyFields).includes(key), - ), - ); - - await updateOneFieldMetadataItem({ + if (isDefined(relationFieldMetadataItem)) { + const result = await updateOneFieldMetadataItem({ objectMetadataId: objectMetadataItem.id, - fieldMetadataIdToUpdate: fieldMetadataItem.id, - updatePayload: formattedInput, + fieldMetadataIdToUpdate: relationFieldMetadataItem.id, + updatePayload: formValues.relation.field, }); + if (result.status === 'failed') { + return; + } + } + } + + const otherDirtyFields = omit(dirtyFields, 'relation'); + + if (Object.keys(otherDirtyFields).length > 0) { + const formattedInput = Object.fromEntries( + Object.entries(formatFieldMetadataItemInput(formValues)).filter( + ([key]) => Object.keys(otherDirtyFields).includes(key), + ), + ); + + const updateResult = await updateOneFieldMetadataItem({ + objectMetadataId: objectMetadataItem.id, + fieldMetadataIdToUpdate: fieldMetadataItem.id, + updatePayload: formattedInput, + }); + + if (updateResult.status === 'successful') { navigateBackOrToSettings(); } - } catch (error) { - enqueueErrorSnackBar({ - apolloError: error instanceof ApolloError ? error : undefined, - }); } }; @@ -198,10 +194,15 @@ export const SettingsObjectFieldEdit = () => { return; } - await deactivateMetadataField(fieldMetadataItem.id, objectMetadataItem.id); - navigateSettings(SettingsPath.ObjectDetail, { - objectNamePlural, - }); + const deactivationResult = await deactivateMetadataField( + fieldMetadataItem.id, + objectMetadataItem.id, + ); + if (deactivationResult.status === 'successful') { + navigateSettings(SettingsPath.ObjectDetail, { + objectNamePlural, + }); + } }; const handleActivate = async () => { @@ -209,10 +210,16 @@ export const SettingsObjectFieldEdit = () => { return; } - await activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id); - navigateSettings(SettingsPath.ObjectDetail, { - objectNamePlural, - }); + const activationResult = await activateMetadataField( + fieldMetadataItem.id, + objectMetadataItem.id, + ); + + if (activationResult.status === 'successful') { + navigateSettings(SettingsPath.ObjectDetail, { + objectNamePlural, + }); + } }; return ( diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx index 6f976541b2..d8fc991b6f 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectTable.tsx @@ -1,30 +1,30 @@ +import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; +import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount'; import { SettingsObjectMetadataItemTableRow, StyledObjectTableRow, } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow'; -import { GET_SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata'; -import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; -import { isNonEmptyArray } from '@sniptt/guards'; -import { TableSection } from '@/ui/layout/table/components/TableSection'; -import { getSettingsPath } from 'twenty-shared/utils'; -import { SettingsPath } from 'twenty-shared/types'; import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown'; -import { Table } from '@/ui/layout/table/components/Table'; -import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; -import { useMemo, useState } from 'react'; -import { normalizeSearchText } from '~/utils/normalizeSearchText'; -import { IconChevronRight, IconSearch } from 'twenty-ui/display'; -import styled from '@emotion/styled'; import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo'; -import type { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem'; -import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount'; -import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useLingui } from '@lingui/react/macro'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; -import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; -import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; +import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; +import { Table } from '@/ui/layout/table/components/Table'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { TableSection } from '@/ui/layout/table/components/TableSection'; +import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { isNonEmptyArray } from '@sniptt/guards'; +import { useMemo, useState } from 'react'; +import { SettingsPath } from 'twenty-shared/types'; +import { getSettingsPath } from 'twenty-shared/utils'; +import { IconChevronRight, IconSearch } from 'twenty-ui/display'; +import { GET_SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata'; +import type { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem'; +import { normalizeSearchText } from '~/utils/normalizeSearchText'; const StyledIconChevronRight = styled(IconChevronRight)` color: ${({ theme }) => theme.font.color.tertiary}; diff --git a/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx b/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx index 23cfa59da9..573426cebf 100644 --- a/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/new-field/SettingsObjectNewFieldConfigure.tsx @@ -14,7 +14,6 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { type View } from '@/views/types/View'; import { ViewType } from '@/views/types/ViewType'; -import { ApolloError } from '@apollo/client'; import { zodResolver } from '@hookform/resolvers/zod'; import { useLingui } from '@lingui/react/macro'; import { useEffect, useState } from 'react'; @@ -25,7 +24,7 @@ import { type RelationCreationPayload, SettingsPath, } from 'twenty-shared/types'; -import { CustomError, getSettingsPath } from 'twenty-shared/utils'; +import { getSettingsPath } from 'twenty-shared/utils'; import { H2Title } from 'twenty-ui/display'; import { Section } from 'twenty-ui/layout'; import { type z } from 'zod'; @@ -122,69 +121,79 @@ export const SettingsObjectNewFieldConfigure = () => { const handleSave = async ( formValues: SettingsDataModelNewFieldFormValues, ) => { - try { - setIsSaving(true); - if (formValues.type === FieldMetadataType.MORPH_RELATION) { - const { - morphRelationObjectMetadataIds, - targetFieldLabel, - iconOnDestination, - relationType, - } = formValues; - if (morphRelationObjectMetadataIds.length > 1) { - await createMetadataField({ - ...formValues, - type: FieldMetadataType.MORPH_RELATION, - objectMetadataId: activeObjectMetadataItem.id, - isLabelSyncedWithName: false, - morphRelationsCreationPayload: morphRelationObjectMetadataIds.map( - (morphRelationObjectMetadataId: string) => ({ - type: relationType, - targetObjectMetadataId: morphRelationObjectMetadataId, - targetFieldLabel, - targetFieldIcon: iconOnDestination, - }), - ), - }); - } else if (morphRelationObjectMetadataIds.length === 1) { - const relationCreationPayload = { - type: relationType, - targetObjectMetadataId: morphRelationObjectMetadataIds[0], - targetFieldLabel, - targetFieldIcon: iconOnDestination, - } satisfies RelationCreationPayload; + setIsSaving(true); - await createMetadataField({ - ...formValues, - type: FieldMetadataType.RELATION, - objectMetadataId: activeObjectMetadataItem.id, - relationCreationPayload, - }); - } else { - throw new CustomError( - 'Please select at least one destination object for this relation.', - 'FIELD_METADATA_RELATION_MALFORMED', - ); - } - } else { - await createMetadataField({ - ...formValues, - objectMetadataId: activeObjectMetadataItem.id, + const createCleanUp = ( + creationResult: Awaited>, + ) => { + if (creationResult.status === 'successful') { + navigate(SettingsPath.ObjectDetail, { + objectNamePlural, }); } + setIsSaving(false); + }; - navigate(SettingsPath.ObjectDetail, { - objectNamePlural, + if (formValues.type !== FieldMetadataType.MORPH_RELATION) { + const creationResult = await createMetadataField({ + ...formValues, + objectMetadataId: activeObjectMetadataItem.id, }); - setIsSaving(false); - } catch (error) { - setIsSaving(false); - enqueueErrorSnackBar({ - apolloError: error instanceof ApolloError ? error : undefined, - }); + return createCleanUp(creationResult); + } + + const { + morphRelationObjectMetadataIds, + targetFieldLabel, + iconOnDestination, + relationType, + } = formValues; + + switch (true) { + case morphRelationObjectMetadataIds.length > 1: { + const creationResult = await createMetadataField({ + ...formValues, + type: FieldMetadataType.MORPH_RELATION, + objectMetadataId: activeObjectMetadataItem.id, + isLabelSyncedWithName: false, + morphRelationsCreationPayload: morphRelationObjectMetadataIds.map( + (morphRelationObjectMetadataId: string) => ({ + type: relationType, + targetObjectMetadataId: morphRelationObjectMetadataId, + targetFieldLabel, + targetFieldIcon: iconOnDestination, + }), + ), + }); + return createCleanUp(creationResult); + } + case morphRelationObjectMetadataIds.length === 1: { + const relationCreationPayload = { + type: relationType, + targetObjectMetadataId: morphRelationObjectMetadataIds[0], + targetFieldLabel, + targetFieldIcon: iconOnDestination, + } satisfies RelationCreationPayload; + + const creationResult = await createMetadataField({ + ...formValues, + type: FieldMetadataType.RELATION, + objectMetadataId: activeObjectMetadataItem.id, + relationCreationPayload, + }); + + return createCleanUp(creationResult); + } + default: { + enqueueErrorSnackBar({ + message: t`Please select at least one destination object for this relation.`, + }); + return setIsSaving(false); + } } }; + if (!activeObjectMetadataItem) return null; return ( diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx index dea3cc7e3d..7330c9368d 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -1,29 +1,29 @@ import { useTestServerlessFunction } from '@/serverless-functions/hooks/useTestServerlessFunction'; +import { computeNewSources } from '@/serverless-functions/utils/computeNewSources'; +import { flattenSources } from '@/serverless-functions/utils/flattenSources'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab'; import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab'; import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab'; import { SettingsServerlessFunctionTriggersTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTriggersTab'; +import { usePersistServerlessFunction } from '@/settings/serverless-functions/hooks/usePersistServerlessFunction'; import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; -import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { TabList } from '@/ui/layout/tab-list/components/TabList'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { t } from '@lingui/core/macro'; import { useNavigate, useParams } from 'react-router-dom'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; import { + IconBolt, IconCode, IconSettings, IconTestPipe, - IconBolt, } from 'twenty-ui/display'; import { useDebouncedCallback } from 'use-debounce'; -import { t } from '@lingui/core/macro'; import { useFindOneApplicationQuery } from '~/generated-metadata/graphql'; -import { computeNewSources } from '@/serverless-functions/utils/computeNewSources'; -import { flattenSources } from '@/serverless-functions/utils/flattenSources'; -import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; const SERVERLESS_FUNCTION_DETAIL_ID = 'serverless-function-detail'; @@ -45,8 +45,7 @@ export const SettingsServerlessFunctionDetail = () => { activeTabIdComponentState, instanceId, ); - const { updateOneServerlessFunction } = - useUpdateOneServerlessFunction(serverlessFunctionId); + const { updateServerlessFunction } = usePersistServerlessFunction(); const { formValues, setFormValues, serverlessFunction, loading } = useServerlessFunctionUpdateFormState({ serverlessFunctionId }); @@ -56,10 +55,15 @@ export const SettingsServerlessFunctionDetail = () => { }); const handleSave = useDebouncedCallback(async () => { - await updateOneServerlessFunction({ - name: formValues.name, - description: formValues.description, - code: formValues.code, + await updateServerlessFunction({ + input: { + id: serverlessFunctionId, + update: { + name: formValues.name, + description: formValues.description, + code: formValues.code, + }, + }, }); }, 500); diff --git a/packages/twenty-front/tsconfig.spec.json b/packages/twenty-front/tsconfig.spec.json index 33cf9584c7..fca51306a3 100644 --- a/packages/twenty-front/tsconfig.spec.json +++ b/packages/twenty-front/tsconfig.spec.json @@ -10,6 +10,7 @@ }, "include": [ "**/__mocks__/**/*", + "**/__tests__/**/*", "jest.config.mjs", "setupTests.ts", "src/**/*.d.ts",