Added SSE effect for view relations objects (#18386)
This PR adds what is necessary for having SSE working for view relations : fields, filters, filter groups and sorts. This should allow to have AI working well while creating views with detailed filtering and sorting. ## Demo https://github.com/user-attachments/assets/026c7fb5-8e1a-4498-b7f4-d16993e5a7c4 ## Fixes Also fixed in this PR while working on the filter area : - Advanced filter does not update - Advanced filter sub field selection is broken (due to Jotai migration) - No view fields when creating a new view - Error on advanced filter deletion (cascade delete wasn't taken into account on the frontend) - Bug advanced filter creation --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
-108
@@ -1,108 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { ViewFilterOperand } from 'twenty-shared/types';
|
||||
import { getFilterTypeFromFieldType, isDefined } from 'twenty-shared/utils';
|
||||
import { getJestMetadataAndApolloMocksAndActionMenuWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndActionMenuWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import { useApplyViewFiltersToCurrentRecordFilters } from '@/views/hooks/useApplyViewFiltersToCurrentRecordFilters';
|
||||
|
||||
const mockObjectMetadataItemNameSingular = 'company';
|
||||
|
||||
describe('useApplyViewFiltersToCurrentRecordFilters', () => {
|
||||
const mockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === mockObjectMetadataItemNameSingular,
|
||||
);
|
||||
|
||||
if (!isDefined(mockObjectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Missing mock object metadata item with name singular ${mockObjectMetadataItemNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const mockFieldMetadataItem = mockObjectMetadataItem.fields[0];
|
||||
|
||||
const mockViewFilter: ViewFilter = {
|
||||
__typename: 'ViewFilter',
|
||||
id: 'filter-1',
|
||||
fieldMetadataId: mockFieldMetadataItem.id,
|
||||
operand: ViewFilterOperand.CONTAINS,
|
||||
value: 'test',
|
||||
displayValue: mockFieldMetadataItem.label,
|
||||
viewFilterGroupId: 'group-1',
|
||||
positionInViewFilterGroup: 0,
|
||||
};
|
||||
|
||||
it('should apply view filters to current record filters', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { applyViewFiltersToCurrentRecordFilters } =
|
||||
useApplyViewFiltersToCurrentRecordFilters();
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
return { applyViewFiltersToCurrentRecordFilters, currentRecordFilters };
|
||||
},
|
||||
{
|
||||
wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: 'instanceId',
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
mockObjectMetadataItemNameSingular,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.applyViewFiltersToCurrentRecordFilters([mockViewFilter]);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toEqual([
|
||||
{
|
||||
id: mockViewFilter.id,
|
||||
fieldMetadataId: mockViewFilter.fieldMetadataId,
|
||||
value: mockViewFilter.value,
|
||||
displayValue: mockViewFilter.displayValue,
|
||||
operand: mockViewFilter.operand,
|
||||
recordFilterGroupId: mockViewFilter.viewFilterGroupId,
|
||||
positionInRecordFilterGroup: mockViewFilter.positionInViewFilterGroup,
|
||||
label: mockFieldMetadataItem.label,
|
||||
type: getFilterTypeFromFieldType(mockFieldMetadataItem.type),
|
||||
} satisfies RecordFilter,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty view filters array', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const { applyViewFiltersToCurrentRecordFilters } =
|
||||
useApplyViewFiltersToCurrentRecordFilters();
|
||||
|
||||
const currentRecordFilters = useAtomComponentStateValue(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
return { applyViewFiltersToCurrentRecordFilters, currentRecordFilters };
|
||||
},
|
||||
{
|
||||
wrapper: getJestMetadataAndApolloMocksAndActionMenuWrapper({
|
||||
apolloMocks: [],
|
||||
componentInstanceId: 'instanceId',
|
||||
contextStoreCurrentObjectMetadataNameSingular:
|
||||
mockObjectMetadataItemNameSingular,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.applyViewFiltersToCurrentRecordFilters([]);
|
||||
});
|
||||
|
||||
expect(result.current.currentRecordFilters).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { recordIndexShouldHideEmptyRecordGroupsComponentState } from '@/object-record/record-index/states/recordIndexShouldHideEmptyRecordGroupsComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
@@ -9,6 +10,7 @@ import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/select
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { getFilterableFields } from '@/views/utils/getFilterableFields';
|
||||
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
|
||||
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined, removePropertiesFromRecord } from 'twenty-shared/utils';
|
||||
@@ -106,6 +108,27 @@ export const useApplyCoreViewsForObjectMetadataId = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
coreView.viewFilterGroups,
|
||||
existingView.viewFilterGroups,
|
||||
)
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
|
||||
jotaiStore.set(
|
||||
currentRecordFilterGroupsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
mapViewFilterGroupsToRecordFilterGroups(
|
||||
view.viewFilterGroups ?? [],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
coreView.viewSorts.map((viewSort) =>
|
||||
@@ -115,6 +138,7 @@ export const useApplyCoreViewsForObjectMetadataId = () => {
|
||||
)
|
||||
) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
|
||||
jotaiStore.set(
|
||||
currentRecordSortsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { useMapViewFiltersToFilters } from './useMapViewFiltersToFilters';
|
||||
|
||||
export const useApplyViewFiltersToCurrentRecordFilters = () => {
|
||||
const setCurrentRecordFilters = useSetAtomComponentState(
|
||||
currentRecordFiltersComponentState,
|
||||
);
|
||||
|
||||
const { mapViewFiltersToRecordFilters } = useMapViewFiltersToFilters();
|
||||
|
||||
const applyViewFiltersToCurrentRecordFilters = (
|
||||
viewFilters: ViewFilter[],
|
||||
) => {
|
||||
const recordFiltersToApply = mapViewFiltersToRecordFilters(viewFilters);
|
||||
|
||||
setCurrentRecordFilters(recordFiltersToApply);
|
||||
};
|
||||
|
||||
return {
|
||||
applyViewFiltersToCurrentRecordFilters,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,24 @@
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useGetViewById = (viewId: string | null) => {
|
||||
const view = useAtomFamilySelectorValue(coreViewFromViewIdFamilySelector, {
|
||||
viewId: viewId ?? '',
|
||||
});
|
||||
export const useGetViewById = () => {
|
||||
const store = useStore();
|
||||
|
||||
const getViewById = useCallback(
|
||||
(viewId: string | null) => {
|
||||
const view = store.get(
|
||||
coreViewFromViewIdFamilySelector.selectorFamily({
|
||||
viewId: viewId ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
return { view };
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return {
|
||||
view: viewId ? view : undefined,
|
||||
getViewById,
|
||||
};
|
||||
};
|
||||
|
||||
+33
-1
@@ -1,8 +1,10 @@
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
|
||||
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
|
||||
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
|
||||
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
|
||||
@@ -26,6 +28,8 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const setCoreViews = useSetAtomState(coreViewsState);
|
||||
|
||||
const saveRecordFilterGroupsToViewFilterGroups = useCallback(async () => {
|
||||
if (!canPersistChanges || !isDefined(currentView)) {
|
||||
return;
|
||||
@@ -55,6 +59,33 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
|
||||
newViewFilterGroups,
|
||||
);
|
||||
|
||||
const viewFiltersToOptimisticallyCascadeDelete =
|
||||
currentView.viewFilters.filter((viewFilter) =>
|
||||
viewFilterGroupsToDelete.some(
|
||||
(viewFilterGroupToDelete) =>
|
||||
viewFilterGroupToDelete.id === viewFilter.viewFilterGroupId,
|
||||
),
|
||||
);
|
||||
|
||||
for (const viewFilterToCascadeDelete of viewFiltersToOptimisticallyCascadeDelete) {
|
||||
setCoreViews((currentCoreViews) => {
|
||||
const updatedCoreViews = currentCoreViews.map((coreView) => {
|
||||
if (coreView.id !== currentView.id) {
|
||||
return coreView;
|
||||
}
|
||||
|
||||
return {
|
||||
...coreView,
|
||||
viewFilters: coreView.viewFilters.filter(
|
||||
(viewFilter) => viewFilter.id !== viewFilterToCascadeDelete.id,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return updatedCoreViews;
|
||||
});
|
||||
}
|
||||
|
||||
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
@@ -71,13 +102,14 @@ export const useSaveRecordFilterGroupsToViewFilterGroups = () => {
|
||||
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
|
||||
await performViewFilterGroupAPIDelete(viewFilterGroupIdsToDelete);
|
||||
}, [
|
||||
store,
|
||||
canPersistChanges,
|
||||
currentView,
|
||||
store,
|
||||
currentRecordFilterGroupsCallbackState,
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDelete,
|
||||
setCoreViews,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
|
||||
import { useCanPersistViewChanges } from '@/views/hooks/useCanPersistViewChanges';
|
||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
|
||||
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
|
||||
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
|
||||
@@ -19,7 +22,9 @@ export const useSaveRecordFiltersToViewFilters = () => {
|
||||
performViewFilterAPIDelete,
|
||||
} = usePerformViewFilterAPIPersist();
|
||||
|
||||
const { currentView } = useGetCurrentViewOnly();
|
||||
const contextStoreCurrentViewId = useAtomComponentStateValue(
|
||||
contextStoreCurrentViewIdComponentState,
|
||||
);
|
||||
|
||||
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
@@ -28,6 +33,18 @@ export const useSaveRecordFiltersToViewFilters = () => {
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordFiltersToViewFilters = useCallback(async () => {
|
||||
const views = store.get(coreViewsState.atom);
|
||||
|
||||
const currentCoreView = views.find(
|
||||
(view) => view.id === contextStoreCurrentViewId,
|
||||
);
|
||||
|
||||
if (!isDefined(currentCoreView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentView = convertCoreViewToView(currentCoreView);
|
||||
|
||||
if (!canPersistChanges || !isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
@@ -110,11 +127,11 @@ export const useSaveRecordFiltersToViewFilters = () => {
|
||||
}, [
|
||||
store,
|
||||
canPersistChanges,
|
||||
currentView,
|
||||
currentRecordFiltersCallbackState,
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
contextStoreCurrentViewId,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
|
||||
|
||||
export const useViewById = (viewId: string | null) => {
|
||||
const view = useAtomFamilySelectorValue(coreViewFromViewIdFamilySelector, {
|
||||
viewId: viewId ?? '',
|
||||
});
|
||||
|
||||
return {
|
||||
view: viewId ? view : undefined,
|
||||
};
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { type ColumnDefinition } from '@/object-record/record-table/types/Column
|
||||
export type ViewField = {
|
||||
__typename: 'ViewField';
|
||||
id: string;
|
||||
viewId?: string;
|
||||
fieldMetadataId: string;
|
||||
position: number;
|
||||
isVisible: boolean;
|
||||
|
||||
Reference in New Issue
Block a user