Add ability to save any field filter to view (#13401)
This PR adds the ability to save an any field filter to a view. It adds a new `anyFieldFilterValue` on both core view and workspace view entities. It also introduces the necessary utils that mimic the logic that manages the save of record filters and record sorts on views.
This commit is contained in:
+1
@@ -25,5 +25,6 @@ export const findAllViewsOperationSignatureFactory: RecordGqlOperationSignatureF
|
|||||||
viewSorts: true,
|
viewSorts: true,
|
||||||
viewFields: true,
|
viewFields: true,
|
||||||
viewGroups: true,
|
viewGroups: true,
|
||||||
|
anyFieldFilterValue: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/h
|
|||||||
import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
|
import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
|
||||||
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
|
||||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||||
|
import { useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } from '@/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter';
|
||||||
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
|
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
|
||||||
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
|
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
|
||||||
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
|
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
|
||||||
@@ -84,10 +85,14 @@ export const UpdateViewButtonGroup = () => {
|
|||||||
const { viewSortsAreDifferentFromRecordSorts } =
|
const { viewSortsAreDifferentFromRecordSorts } =
|
||||||
useAreViewSortsDifferentFromRecordSorts();
|
useAreViewSortsDifferentFromRecordSorts();
|
||||||
|
|
||||||
|
const { viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } =
|
||||||
|
useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter();
|
||||||
|
|
||||||
const canShowButton =
|
const canShowButton =
|
||||||
(viewFiltersAreDifferentFromRecordFilters ||
|
(viewFiltersAreDifferentFromRecordFilters ||
|
||||||
viewSortsAreDifferentFromRecordSorts ||
|
viewSortsAreDifferentFromRecordSorts ||
|
||||||
viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
|
viewFilterGroupsAreDifferentFromRecordFilterGroups ||
|
||||||
|
viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter) &&
|
||||||
!hasFiltersQueryParams;
|
!hasFiltersQueryParams;
|
||||||
|
|
||||||
if (!canShowButton) {
|
if (!canShowButton) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDro
|
|||||||
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
|
||||||
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId';
|
import { VIEW_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ViewSortDropdownId';
|
||||||
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
|
import { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
|
||||||
|
import { ViewBarAnyFieldFilterEffect } from '@/views/components/ViewBarAnyFieldFilterEffect';
|
||||||
import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown';
|
import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown';
|
||||||
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
|
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
|
||||||
import { ViewBarRecordFilterGroupEffect } from '@/views/components/ViewBarRecordFilterGroupEffect';
|
import { ViewBarRecordFilterGroupEffect } from '@/views/components/ViewBarRecordFilterGroupEffect';
|
||||||
@@ -43,6 +44,7 @@ export const ViewBar = ({
|
|||||||
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
|
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
|
||||||
>
|
>
|
||||||
<ViewBarRecordFilterGroupEffect />
|
<ViewBarRecordFilterGroupEffect />
|
||||||
|
<ViewBarAnyFieldFilterEffect />
|
||||||
<ViewBarRecordFilterEffect />
|
<ViewBarRecordFilterEffect />
|
||||||
<ViewBarRecordSortEffect />
|
<ViewBarRecordSortEffect />
|
||||||
<QueryParamsFiltersEffect />
|
<QueryParamsFiltersEffect />
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||||
|
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||||
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||||
|
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
|
||||||
|
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import { hasInitializedAnyFieldFilterComponentFamilyState } from '@/views/states/hasInitializedAnyFieldFilterComponentFamilyState';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const ViewBarAnyFieldFilterEffect = () => {
|
||||||
|
const currentViewId = useRecoilComponentValueV2(
|
||||||
|
contextStoreCurrentViewIdComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
||||||
|
|
||||||
|
const currentView = useRecoilValue(
|
||||||
|
prefetchViewFromViewIdFamilySelector({
|
||||||
|
viewId: currentViewId ?? '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [hasInitializedAnyFieldFilter, setHasInitializedAnyFieldFilter] =
|
||||||
|
useRecoilComponentFamilyStateV2(
|
||||||
|
hasInitializedAnyFieldFilterComponentFamilyState,
|
||||||
|
{
|
||||||
|
viewId: currentViewId ?? undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAnyFieldFilterValue = useSetRecoilComponentStateV2(
|
||||||
|
anyFieldFilterValueComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasInitializedAnyFieldFilter && isDefined(currentView)) {
|
||||||
|
if (currentView.objectMetadataId !== objectMetadataItem.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAnyFieldFilterValue(currentView.anyFieldFilterValue ?? '');
|
||||||
|
|
||||||
|
setHasInitializedAnyFieldFilter(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
setAnyFieldFilterValue,
|
||||||
|
currentViewId,
|
||||||
|
hasInitializedAnyFieldFilter,
|
||||||
|
setHasInitializedAnyFieldFilter,
|
||||||
|
currentView,
|
||||||
|
objectMetadataItem,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -27,8 +27,10 @@ import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDrop
|
|||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
import { AnyFieldSearchDropdownButton } from '@/views/components/AnyFieldSearchDropdownButton';
|
import { AnyFieldSearchDropdownButton } from '@/views/components/AnyFieldSearchDropdownButton';
|
||||||
import { ANY_FIELD_SEARCH_DROPDOWN_ID } from '@/views/constants/AnyFieldSearchDropdownId';
|
import { ANY_FIELD_SEARCH_DROPDOWN_ID } from '@/views/constants/AnyFieldSearchDropdownId';
|
||||||
|
import { useApplyCurrentViewAnyFieldFilterToAnyFieldFilter } from '@/views/hooks/useApplyCurrentViewAnyFieldFilterToAnyFieldFilter';
|
||||||
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
|
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
|
||||||
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
|
||||||
|
import { useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } from '@/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter';
|
||||||
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||||
@@ -144,11 +146,8 @@ export const ViewBarDetails = ({
|
|||||||
const { viewSortsAreDifferentFromRecordSorts } =
|
const { viewSortsAreDifferentFromRecordSorts } =
|
||||||
useAreViewSortsDifferentFromRecordSorts();
|
useAreViewSortsDifferentFromRecordSorts();
|
||||||
|
|
||||||
const canResetView =
|
const { viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } =
|
||||||
(viewFiltersAreDifferentFromRecordFilters ||
|
useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter();
|
||||||
viewSortsAreDifferentFromRecordSorts ||
|
|
||||||
viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
|
|
||||||
!hasFiltersQueryParams;
|
|
||||||
|
|
||||||
const { checkIsSoftDeleteFilter } = useCheckIsSoftDeleteFilter();
|
const { checkIsSoftDeleteFilter } = useCheckIsSoftDeleteFilter();
|
||||||
|
|
||||||
@@ -170,6 +169,9 @@ export const ViewBarDetails = ({
|
|||||||
const { applyCurrentViewFiltersToCurrentRecordFilters } =
|
const { applyCurrentViewFiltersToCurrentRecordFilters } =
|
||||||
useApplyCurrentViewFiltersToCurrentRecordFilters();
|
useApplyCurrentViewFiltersToCurrentRecordFilters();
|
||||||
|
|
||||||
|
const { applyCurrentViewAnyFieldFilterToAnyFieldFilter } =
|
||||||
|
useApplyCurrentViewAnyFieldFilterToAnyFieldFilter();
|
||||||
|
|
||||||
const { applyCurrentViewSortsToCurrentRecordSorts } =
|
const { applyCurrentViewSortsToCurrentRecordSorts } =
|
||||||
useApplyCurrentViewSortsToCurrentRecordSorts();
|
useApplyCurrentViewSortsToCurrentRecordSorts();
|
||||||
|
|
||||||
@@ -177,6 +179,7 @@ export const ViewBarDetails = ({
|
|||||||
applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
|
applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
|
||||||
applyCurrentViewFiltersToCurrentRecordFilters();
|
applyCurrentViewFiltersToCurrentRecordFilters();
|
||||||
applyCurrentViewSortsToCurrentRecordSorts();
|
applyCurrentViewSortsToCurrentRecordSorts();
|
||||||
|
applyCurrentViewAnyFieldFilterToAnyFieldFilter();
|
||||||
toggleSoftDeleteFilterState(false);
|
toggleSoftDeleteFilterState(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,6 +191,13 @@ export const ViewBarDetails = ({
|
|||||||
ANY_FIELD_SEARCH_DROPDOWN_ID,
|
ANY_FIELD_SEARCH_DROPDOWN_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canResetView =
|
||||||
|
(viewFiltersAreDifferentFromRecordFilters ||
|
||||||
|
viewSortsAreDifferentFromRecordSorts ||
|
||||||
|
viewFilterGroupsAreDifferentFromRecordFilterGroups ||
|
||||||
|
viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter) &&
|
||||||
|
!hasFiltersQueryParams;
|
||||||
|
|
||||||
const shouldShowAnyFieldSearchChip =
|
const shouldShowAnyFieldSearchChip =
|
||||||
isNonEmptyString(anyFieldFilterValue) || isAnyFieldSearchDropdownOpen;
|
isNonEmptyString(anyFieldFilterValue) || isAnyFieldSearchDropdownOpen;
|
||||||
|
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||||
|
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||||
|
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const useApplyCurrentViewAnyFieldFilterToAnyFieldFilter = () => {
|
||||||
|
const currentViewId = useRecoilComponentValueV2(
|
||||||
|
contextStoreCurrentViewIdComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setAnyFieldFilterValue = useSetRecoilComponentStateV2(
|
||||||
|
anyFieldFilterValueComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyCurrentViewAnyFieldFilterToAnyFieldFilter = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
() => {
|
||||||
|
const currentView = snapshot
|
||||||
|
.getLoadable(
|
||||||
|
prefetchViewFromViewIdFamilySelector({
|
||||||
|
viewId: currentViewId ?? '',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (isDefined(currentView)) {
|
||||||
|
setAnyFieldFilterValue(currentView.anyFieldFilterValue ?? '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentViewId, setAnyFieldFilterValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyCurrentViewAnyFieldFilterToAnyFieldFilter,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||||
|
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||||
@@ -40,6 +41,10 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
objectNameSingular: CoreObjectNameSingular.View,
|
objectNameSingular: CoreObjectNameSingular.View,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const anyFieldFilterValue = useRecoilComponentValueV2(
|
||||||
|
anyFieldFilterValueComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
const { createViewFieldRecords } = usePersistViewFieldRecords();
|
const { createViewFieldRecords } = usePersistViewFieldRecords();
|
||||||
|
|
||||||
const { createViewSortRecords } = usePersistViewSortRecords();
|
const { createViewSortRecords } = usePersistViewSortRecords();
|
||||||
@@ -126,6 +131,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
type: type ?? sourceView.type,
|
type: type ?? sourceView.type,
|
||||||
objectMetadataId: sourceView.objectMetadataId,
|
objectMetadataId: sourceView.objectMetadataId,
|
||||||
openRecordIn: sourceView.openRecordIn,
|
openRecordIn: sourceView.openRecordIn,
|
||||||
|
anyFieldFilterValue: anyFieldFilterValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isUndefinedOrNull(newView)) {
|
if (isUndefinedOrNull(newView)) {
|
||||||
@@ -209,6 +215,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
|
|||||||
set(isPersistingViewFieldsState, false);
|
set(isPersistingViewFieldsState, false);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
anyFieldFilterValue,
|
||||||
currentViewIdCallbackState,
|
currentViewIdCallbackState,
|
||||||
createOneRecord,
|
createOneRecord,
|
||||||
createViewFieldRecords,
|
createViewFieldRecords,
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||||
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||||
|
import { compareNonEmptyStrings } from '~/utils/compareNonEmptyStrings';
|
||||||
|
|
||||||
|
export const useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter = () => {
|
||||||
|
const { currentView } = useGetCurrentViewOnly();
|
||||||
|
const anyFieldFilterValue = useRecoilComponentValueV2(
|
||||||
|
anyFieldFilterValueComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewAnyFieldFilterValue = currentView?.anyFieldFilterValue;
|
||||||
|
|
||||||
|
const viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter =
|
||||||
|
!compareNonEmptyStrings(viewAnyFieldFilterValue, anyFieldFilterValue);
|
||||||
|
|
||||||
|
return { viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter };
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
|
||||||
|
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||||
|
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||||
|
import { useUpdateView } from '@/views/hooks/useUpdateView';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
export const useSaveAnyFieldFilterToView = () => {
|
||||||
|
const { updateView } = useUpdateView();
|
||||||
|
|
||||||
|
const { currentView } = useGetCurrentViewOnly();
|
||||||
|
|
||||||
|
const anyFieldFilterValueCallbackState = useRecoilComponentCallbackStateV2(
|
||||||
|
anyFieldFilterValueComponentState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveAnyFieldFilterToView = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
async () => {
|
||||||
|
if (!isDefined(currentView)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentViewAnyFieldFilterValue = currentView?.anyFieldFilterValue;
|
||||||
|
|
||||||
|
const currentAnyFieldFilterValue = snapshot
|
||||||
|
.getLoadable(anyFieldFilterValueCallbackState)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
if (currentAnyFieldFilterValue !== currentViewAnyFieldFilterValue) {
|
||||||
|
await updateView({
|
||||||
|
...currentView,
|
||||||
|
anyFieldFilterValue: currentAnyFieldFilterValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[updateView, anyFieldFilterValueCallbackState, currentView],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
saveAnyFieldFilterToView,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useSaveAnyFieldFilterToView } from '@/views/hooks/useSaveAnyFieldFilterToView';
|
||||||
import { useSaveRecordFilterGroupsToViewFilterGroups } from '@/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups';
|
import { useSaveRecordFilterGroupsToViewFilterGroups } from '@/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups';
|
||||||
import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters';
|
import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters';
|
||||||
import { useSaveRecordSortsToViewSorts } from '@/views/hooks/useSaveRecordSortsToViewSorts';
|
import { useSaveRecordSortsToViewSorts } from '@/views/hooks/useSaveRecordSortsToViewSorts';
|
||||||
@@ -11,10 +12,13 @@ export const useSaveCurrentViewFiltersAndSorts = () => {
|
|||||||
|
|
||||||
const { saveRecordSortsToViewSorts } = useSaveRecordSortsToViewSorts();
|
const { saveRecordSortsToViewSorts } = useSaveRecordSortsToViewSorts();
|
||||||
|
|
||||||
|
const { saveAnyFieldFilterToView } = useSaveAnyFieldFilterToView();
|
||||||
|
|
||||||
const saveCurrentViewFilterAndSorts = async () => {
|
const saveCurrentViewFilterAndSorts = async () => {
|
||||||
await saveRecordSortsToViewSorts();
|
await saveRecordSortsToViewSorts();
|
||||||
await saveRecordFiltersToViewFilters();
|
await saveRecordFiltersToViewFilters();
|
||||||
await saveRecordFilterGroupsToViewFilterGroups();
|
await saveRecordFilterGroupsToViewFilterGroups();
|
||||||
|
await saveAnyFieldFilterToView();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
|
||||||
|
import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2';
|
||||||
|
|
||||||
|
export const hasInitializedAnyFieldFilterComponentFamilyState =
|
||||||
|
createComponentFamilyStateV2<boolean, { viewId?: string }>({
|
||||||
|
key: 'hasInitializedAnyFieldFilterComponentFamilyState',
|
||||||
|
defaultValue: false,
|
||||||
|
componentInstanceContext: RecordFiltersComponentInstanceContext,
|
||||||
|
});
|
||||||
@@ -29,4 +29,5 @@ export type GraphQLView = {
|
|||||||
viewGroups: ViewGroup[];
|
viewGroups: ViewGroup[];
|
||||||
position: number;
|
position: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
anyFieldFilterValue?: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ export type View = {
|
|||||||
position: number;
|
position: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
openRecordIn: ViewOpenRecordInType;
|
openRecordIn: ViewOpenRecordInType;
|
||||||
|
anyFieldFilterValue?: string | null;
|
||||||
__typename: 'View';
|
__typename: 'View';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { compareNonEmptyStrings } from '~/utils/compareNonEmptyStrings';
|
||||||
|
|
||||||
|
describe('compareNonEmptyStrings', () => {
|
||||||
|
it('should return true for undefined === null', () => {
|
||||||
|
expect(compareNonEmptyStrings(undefined, null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for null === undefined', () => {
|
||||||
|
expect(compareNonEmptyStrings(null, undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for undefined === undefined', () => {
|
||||||
|
expect(compareNonEmptyStrings(undefined, undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for null === null', () => {
|
||||||
|
expect(compareNonEmptyStrings(null, null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for "" === null', () => {
|
||||||
|
expect(compareNonEmptyStrings('', null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for "" === undefined', () => {
|
||||||
|
expect(compareNonEmptyStrings('', undefined)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for "" === ""', () => {
|
||||||
|
expect(compareNonEmptyStrings('', '')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for "a" === "a"', () => {
|
||||||
|
expect(compareNonEmptyStrings('a', 'a')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for "a" === "b"', () => {
|
||||||
|
expect(compareNonEmptyStrings('a', 'b')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for undefined === "a"', () => {
|
||||||
|
expect(compareNonEmptyStrings(undefined, 'a')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for null === "a"', () => {
|
||||||
|
expect(compareNonEmptyStrings(null, 'a')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for "" === "a"', () => {
|
||||||
|
expect(compareNonEmptyStrings('', 'a')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
import { Nullable } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
|
export const compareNonEmptyStrings = (
|
||||||
|
valueA: Nullable<string>,
|
||||||
|
valueB: Nullable<string>,
|
||||||
|
) => {
|
||||||
|
if (!isNonEmptyString(valueA) && !isNonEmptyString(valueB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueA === valueB;
|
||||||
|
};
|
||||||
+1
@@ -252,6 +252,7 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
|
|||||||
deletedAt: workspaceView.deletedAt
|
deletedAt: workspaceView.deletedAt
|
||||||
? new Date(workspaceView.deletedAt)
|
? new Date(workspaceView.deletedAt)
|
||||||
: null,
|
: null,
|
||||||
|
anyFieldFilterValue: workspaceView.anyFieldFilterValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
const repository = queryRunner.manager.getRepository(View);
|
const repository = queryRunner.manager.getRepository(View);
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateAnyFieldFilterValueColumnOnView1753349164408
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'CreateAnyFieldFilterValueColumnOnView1753349164408';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."view" ADD "anyFieldFilterValue" text`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."view" DROP COLUMN "anyFieldFilterValue"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,9 @@ export class View {
|
|||||||
@DeleteDateColumn({ type: 'timestamptz' })
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
deletedAt?: Date | null;
|
deletedAt?: Date | null;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'text', default: null })
|
||||||
|
anyFieldFilterValue?: string | null;
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, {
|
@ManyToOne(() => Workspace, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
|
|||||||
+1
@@ -444,6 +444,7 @@ export const VIEW_STANDARD_FIELD_IDS = {
|
|||||||
viewFilterGroups: '20202020-0318-474a-84a1-bac895ceaa5a',
|
viewFilterGroups: '20202020-0318-474a-84a1-bac895ceaa5a',
|
||||||
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
|
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
|
||||||
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
|
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
|
||||||
|
anyFieldFilterValue: '20202020-3143-46c0-bb05-034063ce0703',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WEBHOOK_STANDARD_FIELD_IDS = {
|
export const WEBHOOK_STANDARD_FIELD_IDS = {
|
||||||
|
|||||||
@@ -307,4 +307,14 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
})
|
})
|
||||||
@WorkspaceIsNullable()
|
@WorkspaceIsNullable()
|
||||||
kanbanAggregateOperationFieldMetadataId?: string | null;
|
kanbanAggregateOperationFieldMetadataId?: string | null;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: VIEW_STANDARD_FIELD_IDS.anyFieldFilterValue,
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
label: msg`Any field filter value`,
|
||||||
|
description: msg`Any field filter value`,
|
||||||
|
defaultValue: null,
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
anyFieldFilterValue?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user