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:
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
packages/twenty-front/src/utils/compareNonEmptyStrings.ts
Normal file
13
packages/twenty-front/src/utils/compareNonEmptyStrings.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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