Fix view picker small bugs (#16987)

This PR solves small bugs around the view picker.

- Couldn’t obtain optimistic update after a re-order of a view by drag
and drop, we needed to refresh the page
- Picking a new icon wouldn’t trigger optimistic update (same problem)
- Picking a new icon would change the view (difficult to understand
behavior)
- Picking a new icon would trigger left drawer collapse (z-index
problem)

Since core views are not being handled by object metadata items anymore,
and that all view logic is plugged on coreViewsState, this PR
implemented optimistic effect by upserting into this state.

Fixes https://github.com/twentyhq/twenty/issues/15422
Fixes https://github.com/twentyhq/twenty/issues/16986

# Before


https://github.com/user-attachments/assets/64099c21-df9f-4772-ab0d-9ea449aed761

# After


https://github.com/user-attachments/assets/f4e844b3-6530-4178-abdb-b7a10d2327b8

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2026-01-08 12:05:45 +01:00
committed by GitHub
co-authored by Charles Bochet
parent 71fd05315c
commit e9b7ad21d2
28 changed files with 404 additions and 187 deletions
@@ -4,7 +4,7 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { usePersistView } from '@/views/hooks/internal/usePersistView';
import { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { type ViewGroup } from '@/views/types/ViewGroup';
@@ -26,7 +26,7 @@ export const useHandleRecordGroupField = () => {
const { setRecordGroupsFromViewGroups } = useSetRecordGroups();
const { updateView } = usePersistView();
const { performViewAPIUpdate } = usePerformViewAPIUpdate();
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const { refreshCoreViewsByObjectMetadataId } =
useRefreshCoreViewsByObjectMetadataId();
@@ -55,7 +55,7 @@ export const useHandleRecordGroupField = () => {
return;
}
const updatedViewResult = await updateView({
const updatedViewResult = await performViewAPIUpdate({
id: view.id,
input: {
mainGroupByFieldMetadataId: fieldMetadataItem.id,
@@ -133,7 +133,7 @@ export const useHandleRecordGroupField = () => {
[
currentViewIdCallbackState,
getViewFromPrefetchState,
updateView,
performViewAPIUpdate,
setRecordGroupsFromViewGroups,
objectMetadataItem,
refreshCoreViewsByObjectMetadataId,
@@ -162,14 +162,18 @@ export const useHandleRecordGroupField = () => {
return;
}
await updateView({
await performViewAPIUpdate({
id: view.id,
input: {
mainGroupByFieldMetadataId: null,
},
});
},
[currentViewIdCallbackState, getViewFromPrefetchState, updateView],
[
currentViewIdCallbackState,
getViewFromPrefetchState,
performViewAPIUpdate,
],
);
return { handleRecordGroupFieldChange, resetRecordGroupField };
@@ -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 { usePersistViewField } from '@/views/hooks/internal/usePersistViewField';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
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 { updateViewFields } = usePersistViewField();
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
const updateViewFieldAggregateOperation = async (
aggregateOperation: ExtendedAggregateOperations | null,
) => {
if (!currentViewField) {
throw new Error('ViewField not found');
}
await updateViewFields([
await performViewFieldAPIUpdate([
{
input: {
id: currentViewField.id,
@@ -9,6 +9,8 @@ import { SelectableList } from '@/ui/layout/selectable-list/components/Selectabl
import { arrayToChunks } from '~/utils/array/arrayToChunks';
import { ICON_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/ui/input/components/constants/IconPickerDropdownContentWidth';
import { IconPickerScrollEffect } from '@/ui/input/hooks/IconPickerScrollEffect';
import { iconPickerVisibleCountState } from '@/ui/input/states/iconPickerVisibleCountState';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { type DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
@@ -25,8 +27,6 @@ import {
type IconButtonVariant,
LightIconButton,
} from 'twenty-ui/input';
import { IconPickerScrollEffect } from '@/ui/input/hooks/IconPickerScrollEffect';
import { iconPickerVisibleCountState } from '@/ui/input/states/iconPickerVisibleCountState';
export type IconPickerProps = {
disabled?: boolean;
@@ -86,7 +86,7 @@ const convertIconKeyToLabel = (iconKey: string) =>
type IconPickerIconProps = {
iconKey: string;
onClick: () => void;
onSelect: () => void;
selectedIconKey?: string;
Icon: IconComponent;
focusedIconKey?: string;
@@ -94,7 +94,7 @@ type IconPickerIconProps = {
const IconPickerIcon = ({
iconKey,
onClick,
onSelect,
selectedIconKey,
Icon,
focusedIconKey,
@@ -106,7 +106,7 @@ const IconPickerIcon = ({
return (
<StyledMatrixItem>
<SelectableListItem itemId={iconKey} onEnter={onClick}>
<SelectableListItem itemId={iconKey} onEnter={onSelect}>
<StyledLightIconButton
key={iconKey}
aria-label={convertIconKeyToLabel(iconKey)}
@@ -115,7 +115,7 @@ const IconPickerIcon = ({
isSelected={iconKey === selectedIconKey || !!isSelectedItemId}
isFocused={iconKey === focusedIconKey}
Icon={Icon}
onClick={onClick}
onClick={onSelect}
/>
</SelectableListItem>
</StyledMatrixItem>
@@ -301,7 +301,7 @@ export const IconPicker = ({
<IconPickerIcon
key={iconKey}
iconKey={iconKey}
onClick={() => {
onSelect={() => {
onChange({ iconKey, Icon: getIcon(iconKey) });
closeDropdown(dropdownId);
}}
@@ -19,7 +19,7 @@ const StyledEdge = styled.div<StyledEdgeProps>`
side === 'right' ? 'right' : 'left'}: -${RESIZE_EDGE_WIDTH_PX / 2}px;
width: ${RESIZE_EDGE_WIDTH_PX}px;
cursor: col-resize;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
@@ -12,16 +12,13 @@ import { v4 } from 'uuid';
import {
type CreateCoreViewMutationVariables,
type DestroyCoreViewMutationVariables,
type UpdateCoreViewMutationVariables,
useCreateCoreViewMutation,
useDestroyCoreViewMutation,
useUpdateCoreViewMutation,
ViewType,
} from '~/generated/graphql';
export const usePersistView = () => {
export const usePerformViewAPIPersist = () => {
const [createCoreViewMutation] = useCreateCoreViewMutation();
const [updateCoreViewMutation] = useUpdateCoreViewMutation();
const [destroyCoreViewMutation] = useDestroyCoreViewMutation();
const { triggerViewGroupOptimisticEffectAtViewCreation } =
useViewsSideEffectsOnViewGroups();
@@ -29,7 +26,7 @@ export const usePersistView = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const createView = useCallback(
const performViewAPICreate = useCallback(
async (
variables: CreateCoreViewMutationVariables,
objectMetadataItemId: string,
@@ -93,41 +90,7 @@ export const usePersistView = () => {
],
);
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',
operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[updateCoreViewMutation, handleMetadataError, enqueueErrorSnackBar],
);
const destroyView = useCallback(
const performViewAPIDestroy = useCallback(
async (
variables: DestroyCoreViewMutationVariables,
): Promise<
@@ -162,8 +125,7 @@ export const usePersistView = () => {
);
return {
createView,
updateView,
destroyView,
performViewAPICreate,
performViewAPIDestroy,
};
};
@@ -0,0 +1,86 @@
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 { coreViewsState } from '@/views/states/coreViewState';
import { ApolloError } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { useSetRecoilState } from 'recoil';
import { CrudOperationType } from 'twenty-shared/types';
import { upsertPropertiesOfItemIntoArrayOfObjectsComparingId } from 'twenty-shared/utils';
import {
type CoreView,
type UpdateCoreViewMutationVariables,
useUpdateCoreViewMutation,
} from '~/generated/graphql';
export const usePerformViewAPIUpdate = () => {
const [updateCoreViewMutation] = useUpdateCoreViewMutation();
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const setCoreViews = useSetRecoilState(coreViewsState);
const performViewAPIUpdate = useCallback(
async (
variables: UpdateCoreViewMutationVariables,
): Promise<
MetadataRequestResult<Awaited<ReturnType<typeof updateCoreViewMutation>>>
> => {
try {
setCoreViews((currentCoreViews) =>
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
currentCoreViews,
{
...variables.input,
id: variables.id,
} as CoreView,
),
);
const result = await updateCoreViewMutation({
variables,
});
setCoreViews((currentCoreViews) =>
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
currentCoreViews,
{
...result.data?.updateCoreView,
id: variables.id,
} as CoreView,
),
);
return {
status: 'successful',
response: result,
};
} catch (error) {
if (error instanceof ApolloError) {
handleMetadataError(error, {
primaryMetadataName: 'view',
operationType: CrudOperationType.UPDATE,
});
} else {
enqueueErrorSnackBar({ message: t`An error occurred.` });
}
return {
status: 'failed',
error,
};
}
},
[
setCoreViews,
updateCoreViewMutation,
handleMetadataError,
enqueueErrorSnackBar,
],
);
return { performViewAPIUpdate };
};
@@ -18,7 +18,7 @@ import {
useDestroyCoreViewFieldMutation,
useUpdateCoreViewFieldMutation,
} from '~/generated/graphql';
export const usePersistViewField = () => {
export const usePerformViewFieldAPIPersist = () => {
const { triggerViewFieldOptimisticEffect } =
useTriggerViewFieldOptimisticEffect();
@@ -31,7 +31,7 @@ export const usePersistViewField = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const createViewFields = useCallback(
const performViewFieldAPICreate = useCallback(
async (
createCoreViewFieldInputs: CreateManyCoreViewFieldsMutationVariables,
): Promise<
@@ -92,7 +92,7 @@ export const usePersistViewField = () => {
],
);
const updateViewFields = useCallback(
const performViewFieldAPIUpdate = useCallback(
async (
createCoreViewFieldInputs: UpdateCoreViewFieldMutationVariables[],
): Promise<
@@ -154,7 +154,7 @@ export const usePersistViewField = () => {
],
);
const deleteViewFields = useCallback(
const performViewFieldAPIDelete = useCallback(
async (
deleteCoreViewFieldInputs: DeleteCoreViewFieldMutationVariables[],
): Promise<
@@ -216,7 +216,7 @@ export const usePersistViewField = () => {
],
);
const destroyViewFields = useCallback(
const performViewFieldAPIDestroy = useCallback(
async (
destroyCoreViewFieldInputs: DestroyCoreViewFieldMutationVariables[],
): Promise<
@@ -264,9 +264,9 @@ export const usePersistViewField = () => {
);
return {
createViewFields,
updateViewFields,
deleteViewFields,
destroyViewFields,
performViewFieldAPICreate,
performViewFieldAPIUpdate,
performViewFieldAPIDelete,
performViewFieldAPIDestroy,
};
};
@@ -19,7 +19,7 @@ import {
useUpdateCoreViewFilterMutation,
} from '~/generated/graphql';
export const usePersistViewFilterRecords = () => {
export const usePerformViewFilterAPIPersist = () => {
const { triggerViewFilterOptimisticEffect } =
useTriggerViewFilterOptimisticEffect();
const [createCoreViewFilterMutation] = useCreateCoreViewFilterMutation();
@@ -30,7 +30,7 @@ export const usePersistViewFilterRecords = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const createViewFilters = useCallback(
const performViewFilterAPICreate = useCallback(
async (
createCoreViewFilterInputs: CreateCoreViewFilterMutationVariables[],
): Promise<
@@ -92,7 +92,7 @@ export const usePersistViewFilterRecords = () => {
],
);
const updateViewFilters = useCallback(
const performViewFilterAPIUpdate = useCallback(
async (
updateCoreViewFilterInputs: UpdateCoreViewFilterMutationVariables[],
): Promise<
@@ -154,7 +154,7 @@ export const usePersistViewFilterRecords = () => {
],
);
const deleteViewFilters = useCallback(
const performViewFilterAPIDelete = useCallback(
async (
deleteCoreViewFilterInputs: DeleteCoreViewFilterMutationVariables[],
): Promise<
@@ -216,7 +216,7 @@ export const usePersistViewFilterRecords = () => {
],
);
const destroyViewFilters = useCallback(
const performViewFilterAPIDestroy = useCallback(
async (
destroyCoreViewFilterInputs: DestroyCoreViewFilterMutationVariables[],
): Promise<
@@ -264,9 +264,9 @@ export const usePersistViewFilterRecords = () => {
);
return {
createViewFilters,
updateViewFilters,
deleteViewFilters,
destroyViewFilters,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
performViewFilterAPIDestroy,
};
};
@@ -9,7 +9,7 @@ import { useApolloClient } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewFilterGroup } from '~/generated/graphql';
export const usePersistViewFilterGroupRecords = () => {
export const usePerformViewFilterGroupAPIPersist = () => {
const apolloClient = useApolloClient();
const createCoreViewFilterGroupRecord = useCallback(
@@ -39,7 +39,7 @@ export const usePersistViewFilterGroupRecords = () => {
[apolloClient],
);
const createViewFilterGroups = useCallback(
const performViewFilterGroupAPICreate = useCallback(
async (
viewFilterGroupsToCreate: ViewFilterGroup[],
view: Pick<GraphQLView, 'id'>,
@@ -80,7 +80,7 @@ export const usePersistViewFilterGroupRecords = () => {
[createCoreViewFilterGroupRecord],
);
const updateViewFilterGroups = useCallback(
const performViewFilterGroupAPIUpdate = useCallback(
(viewFilterGroupsToUpdate: ViewFilterGroup[]) => {
if (!viewFilterGroupsToUpdate.length) return;
return Promise.all(
@@ -104,7 +104,7 @@ export const usePersistViewFilterGroupRecords = () => {
[apolloClient],
);
const deleteViewFilterGroups = useCallback(
const performViewFilterGroupAPIDelete = useCallback(
(viewFilterGroupIdsToDelete: string[]) => {
if (!viewFilterGroupIdsToDelete.length) return;
return Promise.all(
@@ -122,8 +122,8 @@ export const usePersistViewFilterGroupRecords = () => {
);
return {
createViewFilterGroups,
updateViewFilterGroups,
deleteViewFilterGroups,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
};
};
@@ -13,7 +13,7 @@ import {
useUpdateCoreViewGroupMutation,
} from '~/generated/graphql';
export const usePersistViewGroupRecords = () => {
export const usePerformViewGroupAPIPersist = () => {
const { triggerViewGroupOptimisticEffect } =
useTriggerViewGroupOptimisticEffect();
@@ -22,7 +22,7 @@ export const usePersistViewGroupRecords = () => {
const { handleMetadataError } = useMetadataErrorHandler();
const { enqueueErrorSnackBar } = useSnackBar();
const updateViewGroups = useCallback(
const performViewGroupAPIUpdate = useCallback(
async (
updateCoreViewGroupInputs: UpdateCoreViewGroupMutationVariables[],
): Promise<
@@ -85,6 +85,6 @@ export const usePersistViewGroupRecords = () => {
);
return {
updateViewGroups,
performViewGroupAPIUpdate,
};
};
@@ -10,13 +10,13 @@ import { useApolloClient } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewSort } from '~/generated/graphql';
export const usePersistViewSortRecords = () => {
export const usePerformViewSortAPIPersist = () => {
const apolloClient = useApolloClient();
const { triggerViewSortOptimisticEffect } =
useTriggerViewSortOptimisticEffect();
const createViewSorts = useCallback(
const performViewSortAPICreate = useCallback(
(
viewSortsToCreate: CoreViewSortEssential[],
view: Pick<GraphQLView, 'id'>,
@@ -49,7 +49,7 @@ export const usePersistViewSortRecords = () => {
[apolloClient, triggerViewSortOptimisticEffect],
);
const updateViewSorts = useCallback(
const performViewSortAPIUpdate = useCallback(
(viewSortsToUpdate: CoreViewSortEssential[]) => {
if (!viewSortsToUpdate.length) return;
return Promise.all(
@@ -77,7 +77,7 @@ export const usePersistViewSortRecords = () => {
[apolloClient, triggerViewSortOptimisticEffect],
);
const deleteViewSorts = useCallback(
const performViewSortAPIDelete = useCallback(
(viewSortsToDelete: Pick<CoreViewSortEssential, 'id' | 'viewId'>[]) => {
if (!viewSortsToDelete.length) return;
return Promise.all(
@@ -100,8 +100,8 @@ export const usePersistViewSortRecords = () => {
);
return {
createViewSorts,
updateViewSorts,
deleteViewSorts,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
};
};
@@ -7,11 +7,11 @@ 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 { 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 { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSort';
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { type GraphQLView } from '@/views/types/GraphQLView';
@@ -29,7 +29,7 @@ import { ViewCalendarLayout } from '~/generated-metadata/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const { createView } = usePersistView();
const { performViewAPICreate } = usePerformViewAPIPersist();
const currentViewIdCallbackState = useRecoilComponentCallbackState(
contextStoreCurrentViewIdComponentState,
@@ -40,13 +40,14 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
anyFieldFilterValueComponentState,
);
const { createViewFields } = usePersistViewField();
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
const { createViewSorts } = usePersistViewSortRecords();
const { performViewSortAPICreate } = usePerformViewSortAPIPersist();
const { createViewFilters } = usePersistViewFilterRecords();
const { performViewFilterAPICreate } = usePerformViewFilterAPIPersist();
const { createViewFilterGroups } = usePersistViewFilterGroupRecords();
const { performViewFilterGroupAPICreate } =
usePerformViewFilterGroupAPIPersist();
const { objectMetadataItem } = useRecordIndexContextOrThrow();
@@ -113,7 +114,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const viewType = type ?? sourceView.type;
const result = await createView(
const result = await performViewAPICreate(
{
input: {
id: id ?? v4(),
@@ -160,7 +161,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
throw new Error('Failed to create view');
}
const fieldResult = await createViewFields({
const fieldResult = await performViewFieldAPICreate({
inputs: sourceView.viewFields.map(
({ __typename, id: _id, ...viewField }) => ({
...viewField,
@@ -202,7 +203,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
id: v4(),
}));
await createViewFilterGroups(viewFilterGroupsToCreate, {
await performViewFilterGroupAPICreate(viewFilterGroupsToCreate, {
id: newViewId,
});
@@ -221,13 +222,15 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
}),
);
const filterResult = await createViewFilters(createViewFilterInputs);
const filterResult = await performViewFilterAPICreate(
createViewFilterInputs,
);
if (filterResult.status === 'failed') {
return undefined;
}
await createViewSorts(viewSortsToCreate, { id: newViewId });
await performViewSortAPICreate(viewSortsToCreate, { id: newViewId });
}
await refreshCoreViewsByObjectMetadataId(objectMetadataItem.id);
@@ -236,17 +239,17 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
},
[
currentViewIdCallbackState,
createView,
performViewAPICreate,
anyFieldFilterValue,
objectMetadataItem,
createViewFields,
performViewFieldAPICreate,
refreshCoreViewsByObjectMetadataId,
currentRecordFilterGroups,
currentRecordFilters,
currentRecordSorts,
createViewFilterGroups,
createViewFilters,
createViewSorts,
performViewFilterGroupAPICreate,
performViewFilterAPICreate,
performViewSortAPICreate,
],
);
@@ -1,6 +1,6 @@
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 { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore';
@@ -9,7 +9,7 @@ import { isDefined } from 'twenty-shared/utils';
export const useSaveAnyFieldFilterToView = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const { updateView } = usePersistView();
const { performViewAPIUpdate } = usePerformViewAPIUpdate();
const { currentView } = useGetCurrentViewOnly();
@@ -31,7 +31,7 @@ export const useSaveAnyFieldFilterToView = () => {
.getValue();
if (currentAnyFieldFilterValue !== currentViewAnyFieldFilterValue) {
await updateView({
await performViewAPIUpdate({
id: currentView.id,
input: convertUpdateViewInputToCore({
...currentView,
@@ -42,7 +42,7 @@ export const useSaveAnyFieldFilterToView = () => {
},
[
canPersistChanges,
updateView,
performViewAPIUpdate,
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 { usePersistViewField } from '@/views/hooks/internal/usePersistViewField';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
import { type ViewField } from '@/views/types/ViewField';
@@ -15,7 +15,8 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useSaveCurrentViewFields = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const { createViewFields, updateViewFields } = usePersistViewField();
const { performViewFieldAPICreate, performViewFieldAPIUpdate } =
usePerformViewFieldAPIPersist();
const { getViewFromPrefetchState } = useGetViewFromPrefetchState();
@@ -123,16 +124,16 @@ export const useSaveCurrentViewFields = () => {
);
await Promise.all([
createViewFields({ inputs: viewFieldsToCreate }),
updateViewFields(viewFieldsToUpdate),
performViewFieldAPICreate({ inputs: viewFieldsToCreate }),
performViewFieldAPIUpdate(viewFieldsToUpdate),
]);
},
[
canPersistChanges,
createViewFields,
performViewFieldAPICreate,
currentViewIdCallbackState,
getViewFromPrefetchState,
updateViewFields,
performViewFieldAPIUpdate,
],
);
@@ -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/usePersistViewGroup';
import { usePerformViewGroupAPIPersist } from '@/views/hooks/internal/usePerformViewGroupAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetViewFromPrefetchState } from '@/views/hooks/useGetViewFromPrefetchState';
import { type ViewGroup } from '@/views/types/ViewGroup';
@@ -12,7 +12,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useSaveCurrentViewGroups = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const { updateViewGroups } = usePersistViewGroupRecords();
const { performViewGroupAPIUpdate } = usePerformViewGroupAPIPersist();
const { getViewFromPrefetchState } = useGetViewFromPrefetchState();
@@ -67,7 +67,7 @@ export const useSaveCurrentViewGroups = () => {
return;
}
await updateViewGroups([
await performViewGroupAPIUpdate([
{
input: {
id: existingField.id,
@@ -84,7 +84,7 @@ export const useSaveCurrentViewGroups = () => {
canPersistChanges,
currentViewIdCallbackState,
getViewFromPrefetchState,
updateViewGroups,
performViewGroupAPIUpdate,
],
);
@@ -154,13 +154,13 @@ export const useSaveCurrentViewGroups = () => {
throw new Error('mainGroupByFieldMetadataId is required');
}
await updateViewGroups(viewGroupsToUpdate);
await performViewGroupAPIUpdate(viewGroupsToUpdate);
},
[
canPersistChanges,
currentViewIdCallbackState,
getViewFromPrefetchState,
updateViewGroups,
performViewGroupAPIUpdate,
],
);
@@ -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/usePersistViewFilterGroup';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
@@ -14,10 +14,10 @@ import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const {
createViewFilterGroups,
updateViewFilterGroups,
deleteViewFilterGroups,
} = usePersistViewFilterGroupRecords();
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
} = usePerformViewFilterGroupAPIPersist();
const { currentView } = useGetCurrentViewOnly();
@@ -65,17 +65,20 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
(viewFilterGroup) => viewFilterGroup.id,
);
await createViewFilterGroups(viewFilterGroupsToCreate, currentView);
await updateViewFilterGroups(viewFilterGroupsToUpdate);
await deleteViewFilterGroups(viewFilterGroupIdsToDelete);
await performViewFilterGroupAPICreate(
viewFilterGroupsToCreate,
currentView,
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
await performViewFilterGroupAPIDelete(viewFilterGroupIdsToDelete);
},
[
canPersistChanges,
currentView,
currentRecordFilterGroupsCallbackState,
createViewFilterGroups,
updateViewFilterGroups,
deleteViewFilterGroups,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
],
);
@@ -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/usePersistViewFilter';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
@@ -13,8 +13,11 @@ import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordFiltersToViewFilters = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const { createViewFilters, updateViewFilters, deleteViewFilters } =
usePersistViewFilterRecords();
const {
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
} = usePerformViewFilterAPIPersist();
const { currentView } = useGetCurrentViewOnly();
@@ -93,17 +96,23 @@ export const useSaveRecordFiltersToViewFilters = () => {
}),
);
const createResult = await createViewFilters(createViewFilterInputs);
const createResult = await performViewFilterAPICreate(
createViewFilterInputs,
);
if (createResult.status === 'failed') {
return;
}
const updateResult = await updateViewFilters(updateViewFilterInputs);
const updateResult = await performViewFilterAPIUpdate(
updateViewFilterInputs,
);
if (updateResult.status === 'failed') {
return;
}
const deleteResult = await deleteViewFilters(deleteViewFilterInputs);
const deleteResult = await performViewFilterAPIDelete(
deleteViewFilterInputs,
);
if (deleteResult.status === 'failed') {
return;
}
@@ -112,9 +121,9 @@ export const useSaveRecordFiltersToViewFilters = () => {
canPersistChanges,
currentView,
currentRecordFiltersCallbackState,
createViewFilters,
updateViewFilters,
deleteViewFilters,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
],
);
@@ -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/usePersistViewSort';
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
@@ -13,8 +13,11 @@ import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordSortsToViewSorts = () => {
const { canPersistChanges } = useCanPersistViewChanges();
const { createViewSorts, updateViewSorts, deleteViewSorts } =
usePersistViewSortRecords();
const {
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
} = usePerformViewSortAPIPersist();
const { currentView } = useGetCurrentViewOnly();
@@ -55,17 +58,17 @@ export const useSaveRecordSortsToViewSorts = () => {
newViewSorts,
);
await createViewSorts(viewSortsToCreate, currentView);
await updateViewSorts(viewSortsToUpdate);
await deleteViewSorts(viewSortsToDelete);
await performViewSortAPICreate(viewSortsToCreate, currentView);
await performViewSortAPIUpdate(viewSortsToUpdate);
await performViewSortAPIDelete(viewSortsToDelete);
},
[
canPersistChanges,
currentView,
currentRecordSortsCallbackState,
createViewSorts,
updateViewSorts,
deleteViewSorts,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
],
);
@@ -4,7 +4,7 @@ import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/use
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 { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
@@ -20,7 +20,7 @@ export const useUpdateViewAggregate = () => {
const currentViewId = useRecoilComponentValue(
contextStoreCurrentViewIdComponentState,
);
const { updateView } = usePersistView();
const { performViewAPIUpdate } = usePerformViewAPIUpdate();
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const updateViewAggregate = useRecoilCallback(
@@ -50,7 +50,7 @@ export const useUpdateViewAggregate = () => {
return;
}
const updatedViewResult = await updateView({
const updatedViewResult = await performViewAPIUpdate({
id: currentViewId,
input: {
kanbanAggregateOperationFieldMetadataId,
@@ -78,7 +78,12 @@ export const useUpdateViewAggregate = () => {
loadRecordIndexStates(updatedView, objectMetadataItem);
}
},
[canPersistChanges, currentViewId, updateView, loadRecordIndexStates],
[
canPersistChanges,
currentViewId,
performViewAPIUpdate,
loadRecordIndexStates,
],
);
return {
@@ -25,13 +25,13 @@ export const convertCoreViewToView = (
key: convertedKey,
objectMetadataId: coreView.objectMetadataId,
isCompact: coreView.isCompact,
viewFields: coreView.viewFields.map((viewField) =>
viewFields: coreView.viewFields?.map((viewField) =>
convertCoreViewFieldToViewField(viewField),
),
viewGroups: coreView.viewGroups.map((viewGroup) =>
viewGroups: coreView.viewGroups?.map((viewGroup) =>
convertCoreViewGroupToViewGroup(viewGroup),
),
viewFilters: coreView.viewFilters.map((viewFilter) =>
viewFilters: coreView.viewFilters?.map((viewFilter) =>
convertCoreViewFilterToViewFilter(viewFilter),
),
viewFilterGroups: coreView.viewFilterGroups?.map(
@@ -11,7 +11,7 @@ import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/Dropdo
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 { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
import { useChangeView } from '@/views/hooks/useChangeView';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useOpenCreateViewDropdown } from '@/views/hooks/useOpenCreateViewDropown';
@@ -61,7 +61,7 @@ export const ViewPickerListContent = () => {
const { setViewPickerMode } = useViewPickerMode();
const { updateView } = usePersistView();
const { performViewAPIUpdate } = usePerformViewAPIUpdate();
const { changeView } = useChangeView();
const { closeDropdown } = useCloseDropdown();
@@ -98,12 +98,15 @@ export const ViewPickerListContent = () => {
Promise.all(
viewsReordered.map(async (view, index) => {
if (view.position !== index) {
await updateView({ id: view.id, input: { position: index } });
await performViewAPIUpdate({
id: view.id,
input: { position: index },
});
}
}),
);
},
[updateView, workspaceViews],
[performViewAPIUpdate, workspaceViews],
);
const handleUnlistedDragEnd = useCallback(
@@ -118,12 +121,15 @@ export const ViewPickerListContent = () => {
Promise.all(
viewsReordered.map(async (view, index) => {
if (view.position !== index) {
await updateView({ id: view.id, input: { position: index } });
await performViewAPIUpdate({
id: view.id,
input: { position: index },
});
}
}),
);
},
[updateView, unlistedViews],
[performViewAPIUpdate, unlistedViews],
);
return (
@@ -3,7 +3,7 @@ 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 { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { useChangeView } from '@/views/hooks/useChangeView';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/selectors/coreViewsByObjectMetadataIdFamilySelector';
@@ -44,7 +44,7 @@ export const useDestroyViewFromCurrentState = (viewBarInstanceId?: string) => {
const { changeView } = useChangeView();
const { destroyView } = usePersistView();
const { performViewAPIDestroy } = usePerformViewAPIPersist();
const destroyViewFromCurrentState = useRecoilCallback(
({ set, snapshot }) =>
@@ -74,14 +74,14 @@ export const useDestroyViewFromCurrentState = (viewBarInstanceId?: string) => {
views.filter((view) => view.id !== viewPickerReferenceViewId),
);
await destroyView({ id: viewPickerReferenceViewId });
await performViewAPIDestroy({ id: viewPickerReferenceViewId });
},
[
currentView,
closeAndResetViewPicker,
objectMetadataItem.id,
changeView,
destroyView,
performViewAPIDestroy,
viewPickerIsDirtyCallbackState,
viewPickerIsPersistingCallbackState,
viewPickerReferenceViewIdCallbackState,
@@ -1,8 +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 { usePerformViewAPIUpdate } from '@/views/hooks/internal/usePerformViewAPIUpdate';
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
import { useChangeView } from '@/views/hooks/useChangeView';
import { useCloseAndResetViewPicker } from '@/views/view-picker/hooks/useCloseAndResetViewPicker';
import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState';
import { viewPickerIsDirtyComponentState } from '@/views/view-picker/states/viewPickerIsDirtyComponentState';
@@ -39,8 +38,7 @@ export const useUpdateViewFromCurrentState = () => {
viewPickerVisibilityComponentState,
);
const { updateView } = usePersistView();
const { changeView } = useChangeView();
const { performViewAPIUpdate } = usePerformViewAPIUpdate();
const updateViewFromCurrentState = useRecoilCallback(
({ set, snapshot }) =>
@@ -71,7 +69,7 @@ export const useUpdateViewFromCurrentState = () => {
viewPickerVisibilityCallbackState,
);
await updateView({
await performViewAPIUpdate({
id: viewPickerReferenceViewId,
input: {
name: viewPickerInputName,
@@ -79,7 +77,6 @@ export const useUpdateViewFromCurrentState = () => {
visibility: visibility,
},
});
changeView(viewPickerReferenceViewId);
},
[
canPersistChanges,
@@ -90,8 +87,7 @@ export const useUpdateViewFromCurrentState = () => {
viewPickerInputNameCallbackState,
viewPickerSelectedIconCallbackState,
viewPickerVisibilityCallbackState,
updateView,
changeView,
performViewAPIUpdate,
],
);
@@ -1,4 +1,4 @@
import { upsertIntoArrayOfObjectsComparingId } from '@/utils/array/upsertIntoArrayOfObjectComparingId';
import { upsertIntoArrayOfObjectsComparingId } from '@/utils/array/upsertIntoArrayOfObjectsComparingId';
type TestObject = {
id: string;
@@ -0,0 +1,107 @@
import { upsertPropertiesOfItemIntoArrayOfObjectsComparingId } from '@/utils/array/upsertPropertiesOfItemIntoArrayOfObjectsComparingId';
type TestObject = {
id: string;
name: string;
value: number;
};
const mockTestObjects: TestObject[] = [
{
id: '1',
name: 'Test 1',
value: 100,
},
{
id: '2',
name: 'Test 2',
value: 200,
},
{
id: '3',
name: 'Test 3',
value: 300,
},
];
describe('upsertPropertiesOfItemIntoArrayOfObjectsComparingId', () => {
it('should insert partial properties into empty array', () => {
const partialItem = { id: '1', name: 'New Item' };
expect(
upsertPropertiesOfItemIntoArrayOfObjectsComparingId<TestObject>(
[],
partialItem,
),
).toStrictEqual([partialItem]);
});
it('should append item when id does not exist', () => {
const newItem: TestObject = {
id: '4',
name: 'Test 4',
value: 400,
};
expect(
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
mockTestObjects,
newItem,
),
).toStrictEqual([...mockTestObjects, newItem]);
});
it('should merge partial properties into existing item', () => {
const partialUpdate = { id: '2', name: 'Updated Test 2' };
expect(
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
mockTestObjects,
partialUpdate,
),
).toStrictEqual([
mockTestObjects[0],
{ id: '2', name: 'Updated Test 2', value: 200 },
mockTestObjects[2],
]);
});
it('should replace all properties when full item is provided', () => {
const fullUpdate: TestObject = {
id: '1',
name: 'Replaced Test 1',
value: 999,
};
expect(
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
mockTestObjects,
fullUpdate,
),
).toStrictEqual([fullUpdate, mockTestObjects[1], mockTestObjects[2]]);
});
it('should not mutate original array when updating', () => {
const originalArray = [...mockTestObjects];
const partialUpdate = { id: '2', value: 999 };
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
mockTestObjects,
partialUpdate,
);
expect(mockTestObjects).toStrictEqual(originalArray);
});
it('should not mutate original array when inserting', () => {
const originalArray = [...mockTestObjects];
const newItem: TestObject = { id: '5', name: 'Test 5', value: 500 };
upsertPropertiesOfItemIntoArrayOfObjectsComparingId(
mockTestObjects,
newItem,
);
expect(mockTestObjects).toStrictEqual(originalArray);
});
});
@@ -0,0 +1,31 @@
import { findById } from '@/utils/array/findById';
export const upsertPropertiesOfItemIntoArrayOfObjectsComparingId = <
T extends { id: string },
>(
arrayToUpsertInto: T[],
propertiesToUpsert: Partial<T> & { id: string },
): T[] => {
const alreadyExistingItemIndex = arrayToUpsertInto.findIndex(
findById(propertiesToUpsert.id),
);
const shouldReplaceItem = alreadyExistingItemIndex > -1;
if (shouldReplaceItem) {
const newArray = [...arrayToUpsertInto];
const itemToUpsert = {
...arrayToUpsertInto[alreadyExistingItemIndex],
...propertiesToUpsert,
} as T;
newArray.splice(alreadyExistingItemIndex, 1, itemToUpsert);
return newArray;
} else {
return arrayToUpsertInto.concat({
...propertiesToUpsert,
} as T);
}
};
+2 -1
View File
@@ -17,7 +17,8 @@ export { isNonEmptyArray } from './array/isNonEmptyArray';
export { mapById } from './array/mapById';
export { mapByProperty } from './array/mapByProperty';
export { sumByProperty } from './array/sumByProperty';
export { upsertIntoArrayOfObjectsComparingId } from './array/upsertIntoArrayOfObjectComparingId';
export { upsertIntoArrayOfObjectsComparingId } from './array/upsertIntoArrayOfObjectsComparingId';
export { upsertPropertiesOfItemIntoArrayOfObjectsComparingId } from './array/upsertPropertiesOfItemIntoArrayOfObjectsComparingId';
export { assertUnreachable } from './assertUnreachable';
export { computeDiffBetweenObjects } from './compute-diff-between-objects';
export { isPlainDateAfter } from './date/isPlainDateAfter';