diff --git a/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts b/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts
index 0238ab99b..0b96d95ad 100644
--- a/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts
+++ b/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts
@@ -25,5 +25,6 @@ export const findAllViewsOperationSignatureFactory: RecordGqlOperationSignatureF
viewSorts: true,
viewFields: true,
viewGroups: true,
+ anyFieldFilterValue: true,
},
});
diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
index 879e5a3f7..9778dfc92 100644
--- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
+++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx
@@ -14,6 +14,7 @@ import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/h
import { useAreViewFiltersDifferentFromRecordFilters } from '@/views/hooks/useAreViewFiltersDifferentFromRecordFilters';
import { useAreViewSortsDifferentFromRecordSorts } from '@/views/hooks/useAreViewSortsDifferentFromRecordSorts';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
+import { useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } from '@/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter';
import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
@@ -84,10 +85,14 @@ export const UpdateViewButtonGroup = () => {
const { viewSortsAreDifferentFromRecordSorts } =
useAreViewSortsDifferentFromRecordSorts();
+ const { viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } =
+ useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter();
+
const canShowButton =
(viewFiltersAreDifferentFromRecordFilters ||
viewSortsAreDifferentFromRecordSorts ||
- viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
+ viewFilterGroupsAreDifferentFromRecordFilterGroups ||
+ viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter) &&
!hasFiltersQueryParams;
if (!canShowButton) {
diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx
index 3aaa46c33..f78a212d5 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx
@@ -12,6 +12,7 @@ import { ViewPickerDropdown } from '@/views/view-picker/components/ViewPickerDro
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 { ObjectSortDropdownComponentInstanceContext } from '@/object-record/object-sort-dropdown/states/context/ObjectSortDropdownComponentInstanceContext';
+import { ViewBarAnyFieldFilterEffect } from '@/views/components/ViewBarAnyFieldFilterEffect';
import { ViewBarFilterDropdown } from '@/views/components/ViewBarFilterDropdown';
import { ViewBarRecordFilterEffect } from '@/views/components/ViewBarRecordFilterEffect';
import { ViewBarRecordFilterGroupEffect } from '@/views/components/ViewBarRecordFilterGroupEffect';
@@ -43,6 +44,7 @@ export const ViewBar = ({
value={{ instanceId: VIEW_SORT_DROPDOWN_ID }}
>
+
diff --git a/packages/twenty-front/src/modules/views/components/ViewBarAnyFieldFilterEffect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarAnyFieldFilterEffect.tsx
new file mode 100644
index 000000000..a3782042f
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/components/ViewBarAnyFieldFilterEffect.tsx
@@ -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;
+};
diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
index 8f0c5bd97..97475f9f4 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx
@@ -27,8 +27,10 @@ import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDrop
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { AnyFieldSearchDropdownButton } from '@/views/components/AnyFieldSearchDropdownButton';
import { ANY_FIELD_SEARCH_DROPDOWN_ID } from '@/views/constants/AnyFieldSearchDropdownId';
+import { useApplyCurrentViewAnyFieldFilterToAnyFieldFilter } from '@/views/hooks/useApplyCurrentViewAnyFieldFilterToAnyFieldFilter';
import { useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups } from '@/views/hooks/useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups';
import { useAreViewFilterGroupsDifferentFromRecordFilterGroups } from '@/views/hooks/useAreViewFilterGroupsDifferentFromRecordFilterGroups';
+import { useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } from '@/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter';
import { isViewBarExpandedComponentState } from '@/views/states/isViewBarExpandedComponentState';
import { t } from '@lingui/core/macro';
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
@@ -144,11 +146,8 @@ export const ViewBarDetails = ({
const { viewSortsAreDifferentFromRecordSorts } =
useAreViewSortsDifferentFromRecordSorts();
- const canResetView =
- (viewFiltersAreDifferentFromRecordFilters ||
- viewSortsAreDifferentFromRecordSorts ||
- viewFilterGroupsAreDifferentFromRecordFilterGroups) &&
- !hasFiltersQueryParams;
+ const { viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter } =
+ useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter();
const { checkIsSoftDeleteFilter } = useCheckIsSoftDeleteFilter();
@@ -170,6 +169,9 @@ export const ViewBarDetails = ({
const { applyCurrentViewFiltersToCurrentRecordFilters } =
useApplyCurrentViewFiltersToCurrentRecordFilters();
+ const { applyCurrentViewAnyFieldFilterToAnyFieldFilter } =
+ useApplyCurrentViewAnyFieldFilterToAnyFieldFilter();
+
const { applyCurrentViewSortsToCurrentRecordSorts } =
useApplyCurrentViewSortsToCurrentRecordSorts();
@@ -177,6 +179,7 @@ export const ViewBarDetails = ({
applyCurrentViewFilterGroupsToCurrentRecordFilterGroups();
applyCurrentViewFiltersToCurrentRecordFilters();
applyCurrentViewSortsToCurrentRecordSorts();
+ applyCurrentViewAnyFieldFilterToAnyFieldFilter();
toggleSoftDeleteFilterState(false);
};
@@ -188,6 +191,13 @@ export const ViewBarDetails = ({
ANY_FIELD_SEARCH_DROPDOWN_ID,
);
+ const canResetView =
+ (viewFiltersAreDifferentFromRecordFilters ||
+ viewSortsAreDifferentFromRecordSorts ||
+ viewFilterGroupsAreDifferentFromRecordFilterGroups ||
+ viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter) &&
+ !hasFiltersQueryParams;
+
const shouldShowAnyFieldSearchChip =
isNonEmptyString(anyFieldFilterValue) || isAnyFieldSearchDropdownOpen;
diff --git a/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewAnyFieldFilterToAnyFieldFilter.ts b/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewAnyFieldFilterToAnyFieldFilter.ts
new file mode 100644
index 000000000..1e56d826b
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useApplyCurrentViewAnyFieldFilterToAnyFieldFilter.ts
@@ -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,
+ };
+};
diff --git a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts
index fa04fd585..e5b851914 100644
--- a/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts
+++ b/packages/twenty-front/src/modules/views/hooks/useCreateViewFromCurrentView.ts
@@ -3,6 +3,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
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 { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
@@ -40,6 +41,10 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
objectNameSingular: CoreObjectNameSingular.View,
});
+ const anyFieldFilterValue = useRecoilComponentValueV2(
+ anyFieldFilterValueComponentState,
+ );
+
const { createViewFieldRecords } = usePersistViewFieldRecords();
const { createViewSortRecords } = usePersistViewSortRecords();
@@ -126,6 +131,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
type: type ?? sourceView.type,
objectMetadataId: sourceView.objectMetadataId,
openRecordIn: sourceView.openRecordIn,
+ anyFieldFilterValue: anyFieldFilterValue,
});
if (isUndefinedOrNull(newView)) {
@@ -209,6 +215,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
set(isPersistingViewFieldsState, false);
},
[
+ anyFieldFilterValue,
currentViewIdCallbackState,
createOneRecord,
createViewFieldRecords,
diff --git a/packages/twenty-front/src/modules/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter.ts b/packages/twenty-front/src/modules/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter.ts
new file mode 100644
index 000000000..79832d875
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter.ts
@@ -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 };
+};
diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveAnyFieldFilterToView.ts b/packages/twenty-front/src/modules/views/hooks/useSaveAnyFieldFilterToView.ts
new file mode 100644
index 000000000..465f16ffe
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/hooks/useSaveAnyFieldFilterToView.ts
@@ -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,
+ };
+};
diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
index 4e48feba0..1eebfa39f 100644
--- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
+++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewFiltersAndSorts.ts
@@ -1,3 +1,4 @@
+import { useSaveAnyFieldFilterToView } from '@/views/hooks/useSaveAnyFieldFilterToView';
import { useSaveRecordFilterGroupsToViewFilterGroups } from '@/views/hooks/useSaveRecordFilterGroupsToViewFilterGroups';
import { useSaveRecordFiltersToViewFilters } from '@/views/hooks/useSaveRecordFiltersToViewFilters';
import { useSaveRecordSortsToViewSorts } from '@/views/hooks/useSaveRecordSortsToViewSorts';
@@ -11,10 +12,13 @@ export const useSaveCurrentViewFiltersAndSorts = () => {
const { saveRecordSortsToViewSorts } = useSaveRecordSortsToViewSorts();
+ const { saveAnyFieldFilterToView } = useSaveAnyFieldFilterToView();
+
const saveCurrentViewFilterAndSorts = async () => {
await saveRecordSortsToViewSorts();
await saveRecordFiltersToViewFilters();
await saveRecordFilterGroupsToViewFilterGroups();
+ await saveAnyFieldFilterToView();
};
return {
diff --git a/packages/twenty-front/src/modules/views/states/hasInitializedAnyFieldFilterComponentFamilyState.ts b/packages/twenty-front/src/modules/views/states/hasInitializedAnyFieldFilterComponentFamilyState.ts
new file mode 100644
index 000000000..66c1943a2
--- /dev/null
+++ b/packages/twenty-front/src/modules/views/states/hasInitializedAnyFieldFilterComponentFamilyState.ts
@@ -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({
+ key: 'hasInitializedAnyFieldFilterComponentFamilyState',
+ defaultValue: false,
+ componentInstanceContext: RecordFiltersComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/views/types/GraphQLView.ts b/packages/twenty-front/src/modules/views/types/GraphQLView.ts
index 9fa643df2..3660b6a5c 100644
--- a/packages/twenty-front/src/modules/views/types/GraphQLView.ts
+++ b/packages/twenty-front/src/modules/views/types/GraphQLView.ts
@@ -29,4 +29,5 @@ export type GraphQLView = {
viewGroups: ViewGroup[];
position: number;
icon: string;
+ anyFieldFilterValue?: string | null;
};
diff --git a/packages/twenty-front/src/modules/views/types/View.ts b/packages/twenty-front/src/modules/views/types/View.ts
index 3d0c4ed3b..4c7afc064 100644
--- a/packages/twenty-front/src/modules/views/types/View.ts
+++ b/packages/twenty-front/src/modules/views/types/View.ts
@@ -29,5 +29,6 @@ export type View = {
position: number;
icon: string;
openRecordIn: ViewOpenRecordInType;
+ anyFieldFilterValue?: string | null;
__typename: 'View';
};
diff --git a/packages/twenty-front/src/utils/__tests__/compareNonEmptyString.test.ts b/packages/twenty-front/src/utils/__tests__/compareNonEmptyString.test.ts
new file mode 100644
index 000000000..54da87e00
--- /dev/null
+++ b/packages/twenty-front/src/utils/__tests__/compareNonEmptyString.test.ts
@@ -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);
+ });
+});
diff --git a/packages/twenty-front/src/utils/compareNonEmptyStrings.ts b/packages/twenty-front/src/utils/compareNonEmptyStrings.ts
new file mode 100644
index 000000000..f154426f0
--- /dev/null
+++ b/packages/twenty-front/src/utils/compareNonEmptyStrings.ts
@@ -0,0 +1,13 @@
+import { isNonEmptyString } from '@sniptt/guards';
+import { Nullable } from 'twenty-ui/utilities';
+
+export const compareNonEmptyStrings = (
+ valueA: Nullable,
+ valueB: Nullable,
+) => {
+ if (!isNonEmptyString(valueA) && !isNonEmptyString(valueB)) {
+ return true;
+ }
+
+ return valueA === valueB;
+};
diff --git a/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts b/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts
index 84ff82ec6..ac4829243 100644
--- a/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts
+++ b/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts
@@ -252,6 +252,7 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
deletedAt: workspaceView.deletedAt
? new Date(workspaceView.deletedAt)
: null,
+ anyFieldFilterValue: workspaceView.anyFieldFilterValue,
};
const repository = queryRunner.manager.getRepository(View);
diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1753349164408-createAnyFieldFilterValueColumnOnView.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753349164408-createAnyFieldFilterValueColumnOnView.ts
new file mode 100644
index 000000000..cfeb715c3
--- /dev/null
+++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1753349164408-createAnyFieldFilterValueColumnOnView.ts
@@ -0,0 +1,19 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class CreateAnyFieldFilterValueColumnOnView1753349164408
+ implements MigrationInterface
+{
+ name = 'CreateAnyFieldFilterValueColumnOnView1753349164408';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "core"."view" ADD "anyFieldFilterValue" text`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "core"."view" DROP COLUMN "anyFieldFilterValue"`,
+ );
+ }
+}
diff --git a/packages/twenty-server/src/engine/metadata-modules/view/view.entity.ts b/packages/twenty-server/src/engine/metadata-modules/view/view.entity.ts
index f92235fa0..d9b7e1694 100644
--- a/packages/twenty-server/src/engine/metadata-modules/view/view.entity.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/view/view.entity.ts
@@ -88,6 +88,9 @@ export class View {
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date | null;
+ @Column({ nullable: true, type: 'text', default: null })
+ anyFieldFilterValue?: string | null;
+
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
index 1ef67a125..ed3ffa247 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
@@ -444,6 +444,7 @@ export const VIEW_STANDARD_FIELD_IDS = {
viewFilterGroups: '20202020-0318-474a-84a1-bac895ceaa5a',
viewSorts: '20202020-891b-45c3-9fe1-80a75b4aa043',
favorites: '20202020-c818-4a86-8284-9ec0ef0a59a5',
+ anyFieldFilterValue: '20202020-3143-46c0-bb05-034063ce0703',
};
export const WEBHOOK_STANDARD_FIELD_IDS = {
diff --git a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts
index 5052d5f66..cadd69d03 100644
--- a/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/view/standard-objects/view.workspace-entity.ts
@@ -307,4 +307,14 @@ export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
})
@WorkspaceIsNullable()
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;
}