Files
twenty/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts
T
Paul Rastoin 503a5029da 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
2025-10-28 10:50:41 +01:00

173 lines
5.0 KiB
TypeScript

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/usePersistViewGroup';
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useSaveCurrentViewGroups = () => {
const { createViewGroups, updateViewGroups } = usePersistViewGroupRecords();
const { getViewFromPrefetchState } = useGetViewFromPrefetchState();
const currentViewIdCallbackState = useRecoilComponentCallbackState(
contextStoreCurrentViewIdComponentState,
);
const saveViewGroup = useRecoilCallback(
({ snapshot }) =>
async (viewGroupToSave: ViewGroup) => {
const currentViewId = snapshot
.getLoadable(currentViewIdCallbackState)
.getValue();
if (!currentViewId) {
return;
}
const view = getViewFromPrefetchState(currentViewId);
if (isUndefinedOrNull(view)) {
return;
}
const currentViewGroups = view.viewGroups;
const existingField = currentViewGroups.find(
(currentViewGroup) =>
currentViewGroup.fieldValue === viewGroupToSave.fieldValue,
);
if (isUndefinedOrNull(existingField)) {
return;
}
if (
isDeeplyEqual(
{
position: existingField.position,
isVisible: existingField.isVisible,
},
{
position: viewGroupToSave.position,
isVisible: viewGroupToSave.isVisible,
},
)
) {
return;
}
await updateViewGroups([
{
input: {
id: existingField.id,
update: {
isVisible: viewGroupToSave.isVisible,
position: viewGroupToSave.position,
fieldMetadataId: viewGroupToSave.fieldMetadataId,
fieldValue: viewGroupToSave.fieldValue,
},
},
},
]);
},
[currentViewIdCallbackState, getViewFromPrefetchState, updateViewGroups],
);
const saveViewGroups = useRecoilCallback(
({ snapshot }) =>
async (viewGroupsToSave: ViewGroup[]) => {
const currentViewId = snapshot
.getLoadable(currentViewIdCallbackState)
.getValue();
if (!currentViewId) {
return;
}
const view = getViewFromPrefetchState(currentViewId);
if (isUndefinedOrNull(view)) {
return;
}
const currentViewGroups = view.viewGroups;
const viewGroupsToUpdate = viewGroupsToSave
.map((viewGroupToSave) => {
const existingField = currentViewGroups.find(
(currentViewGroup) =>
currentViewGroup.fieldValue === viewGroupToSave.fieldValue,
);
if (isUndefinedOrNull(existingField)) {
return undefined;
}
if (
isDeeplyEqual(
{
position: existingField.position,
isVisible: existingField.isVisible,
},
{
position: viewGroupToSave.position,
isVisible: viewGroupToSave.isVisible,
},
)
) {
return undefined;
}
return {
input: {
id: existingField.id,
update: {
isVisible: viewGroupToSave.isVisible,
position: viewGroupToSave.position,
fieldMetadataId: viewGroupToSave.fieldMetadataId,
fieldValue: viewGroupToSave.fieldValue,
},
},
};
})
.filter(isDefined);
const viewGroupsToCreate = viewGroupsToSave.filter(
(viewFieldToSave) =>
!currentViewGroups.some(
(currentViewGroup) =>
currentViewGroup.fieldValue === viewFieldToSave.fieldValue,
),
);
await Promise.all([
createViewGroups(
viewGroupsToCreate.map(({ __typename, ...viewGroup }) => ({
input: {
...viewGroup,
viewId: view.id,
},
})),
),
updateViewGroups(viewGroupsToUpdate),
]);
},
[
createViewGroups,
currentViewIdCallbackState,
getViewFromPrefetchState,
updateViewGroups,
],
);
return {
saveViewGroup,
saveViewGroups,
};
};