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<String>,
              "name": "testField",
              "objectMetadataId": Any<String>,
            },
            "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.
<img width="700" height="327" alt="image"
src="https://github.com/user-attachments/assets/0c33d13c-c66c-4749-af36-b253abd3449b"
/>

### Related entity error
Still while creating an object
<img width="700" height="327" alt="image"
src="https://github.com/user-attachments/assets/52607788-c4e9-470c-ac8c-23437345ee5c"
/>

### Translated
<img width="700" height="327" alt="image"
src="https://github.com/user-attachments/assets/a7198c20-ae82-47a6-910c-761de9594672"
/>


## 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
This commit is contained in:
Paul Rastoin
2025-10-28 10:50:41 +01:00
committed by GitHub
parent 08e1fc8bca
commit 503a5029da
56 changed files with 2073 additions and 1136 deletions
@@ -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<AllMetadataName, string>;
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,
};
};
@@ -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<string, unknown>,
): extensions is MetadataValidationErrorResponse =>
extensions.code === 'METADATA_VALIDATION_FAILED';
const isMetadataInternalError = (
extensions: Record<string, unknown>,
): 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 };
};
@@ -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 } });
});
});
});
@@ -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 }) => (
<RecoilRoot>
<MockedProvider mocks={mocks} addTypename={false}>
{children}
</MockedProvider>
</RecoilRoot>
);
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 } });
});
});
});
@@ -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,
},
});
});
});
@@ -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 = <T>(
value: MetadataRequestResult<T>,
) => asserts value is SuccessfulMetadataRequestResult<T>;
export const jestExpectSuccessfulMetadataRequestResult: AssertIsSuccessfulMetadataRequestResult =
(value) => {
expect(value.status).toBe('successful');
};
@@ -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<ReturnType<typeof createOneFieldMetadataItemMutation>>
>
> => {
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 {
@@ -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<ReturnType<typeof createOneObjectMetadataItemMutation>>
>
> => {
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 {
@@ -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<ReturnType<typeof deleteOneFieldMetadataItemMutation>>
>
> => {
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 {
@@ -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<ReturnType<typeof deleteOneObjectMetadataItemMutation>>
>
> => {
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 {
@@ -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<ReturnType<typeof updateOneFieldMetadataItemMutation>>
>
> => {
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 {
@@ -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<ReturnType<typeof updateOneObjectMetadataItemMutation>>
>
> => {
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 {
@@ -0,0 +1 @@
export type FailedMetadataRequestResult = { status: 'failed'; error: unknown };
@@ -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<T> =
| FailedMetadataRequestResult
| SuccessfulMetadataRequestResult<T>;
@@ -0,0 +1,4 @@
export type SuccessfulMetadataRequestResult<T> = {
status: 'successful';
response: T;
};
@@ -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(
@@ -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,
@@ -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,
@@ -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: {
@@ -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
<FormProvider {...formConfig}>
@@ -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 (
@@ -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,
});
}
}
};
@@ -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 };
};
@@ -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 };
};
@@ -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<ReturnType<typeof createServerlessFunctionMutation>>
>
> => {
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<ReturnType<typeof updateServerlessFunctionMutation>>
>
> => {
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<ReturnType<typeof deleteServerlessFunctionMutation>>
>
> => {
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,
};
};
@@ -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 };
};
@@ -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'
@@ -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<Awaited<ReturnType<typeof createCoreViewMutation>>>
> => {
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<Awaited<ReturnType<typeof updateCoreViewMutation>>>
> => {
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<Awaited<ReturnType<typeof deleteCoreViewMutation>>>
> => {
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,
};
};
@@ -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<ReturnType<typeof createCoreViewFieldMutation>>[]
>
> => {
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<ReturnType<typeof updateCoreViewFieldMutation>>[]
>
> => {
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<ReturnType<typeof deleteCoreViewFieldMutation>>[]
>
> => {
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<ReturnType<typeof destroyCoreViewFieldMutation>>[]
>
> => {
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,
};
};
@@ -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,
};
};
@@ -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<ReturnType<typeof createCoreViewFilterMutation>>[]
>
> => {
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<ReturnType<typeof updateCoreViewFilterMutation>>[]
>
> => {
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<ReturnType<typeof deleteCoreViewFilterMutation>>[]
>
> => {
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<ReturnType<typeof destroyCoreViewFilterMutation>>[]
>
> => {
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,
};
};
@@ -39,7 +39,7 @@ export const usePersistViewFilterGroupRecords = () => {
[apolloClient],
);
const createCoreViewFilterGroupRecords = useCallback(
const createViewFilterGroups = useCallback(
async (
viewFilterGroupsToCreate: ViewFilterGroup[],
view: Pick<GraphQLView, 'id'>,
@@ -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,
};
};
@@ -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,
};
};
@@ -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<ReturnType<typeof createCoreViewGroupMutation>>[]
>
> => {
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<ReturnType<typeof updateCoreViewGroupMutation>>[]
>
> => {
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<ReturnType<typeof deleteCoreViewGroupMutation>>[]
>
> => {
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<ReturnType<typeof destroyCoreViewGroupMutation>>[]
>
> => {
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,
};
};
@@ -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,
};
};
@@ -16,7 +16,7 @@ export const usePersistViewSortRecords = () => {
const { triggerViewSortOptimisticEffect } =
useTriggerViewSortOptimisticEffect();
const createCoreViewSortRecords = useCallback(
const createViewSorts = useCallback(
(
viewSortsToCreate: CoreViewSortEssential[],
view: Pick<GraphQLView, 'id'>,
@@ -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<CoreViewSortEssential, 'id' | 'viewId'>[]) => {
if (!viewSortsToDelete.length) return;
return Promise.all(
@@ -100,8 +100,8 @@ export const usePersistViewSortRecords = () => {
);
return {
createViewSortRecords: createCoreViewSortRecords,
updateViewSortRecords: updateCoreViewSortRecords,
deleteViewSortRecords: deleteCoreViewSortRecords,
createViewSorts,
updateViewSorts,
deleteViewSorts,
};
};
@@ -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<CreateCoreViewFieldMutationVariables>(
({ __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,
],
);
@@ -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 };
};
@@ -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],
@@ -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,
],
);
@@ -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,
],
);
@@ -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,
],
);
@@ -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,
],
);
@@ -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,
],
);
@@ -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<GraphQLView>) => {
if (!isDefined(view.id)) {
return;
}
await updateOneCoreView({
variables: {
id: view.id,
input: convertUpdateViewInputToCore(view),
},
});
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
},
[
objectMetadataItem.id,
refreshCoreViewsByObjectMetadataId,
updateOneCoreView,
],
);
return {
updateView,
};
};
@@ -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({
@@ -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 } });
}
}),
);
@@ -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,
@@ -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);
},
@@ -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);
@@ -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 (
@@ -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 (
@@ -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};
@@ -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<ReturnType<typeof createMetadataField>>,
) => {
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 (
@@ -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);
+1
View File
@@ -10,6 +10,7 @@
},
"include": [
"**/__mocks__/**/*",
"**/__tests__/**/*",
"jest.config.mjs",
"setupTests.ts",
"src/**/*.d.ts",