diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx index e89b97691538..7c48a63a0b92 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx @@ -15,6 +15,7 @@ import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords'; import { FilterOperand } from '@/object-record/object-filter-dropdown/types/FilterOperand'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; @@ -52,10 +53,13 @@ export const useDeleteMultipleRecordsAction = ({ contextStoreFiltersComponentState, ); + const { filterValueDependencies } = useFilterValueDependencies(); + const graphqlFilter = computeContextStoreFilters( contextStoreTargetedRecordsRule, contextStoreFilters, objectMetadataItem, + filterValueDependencies, ); const deletedAtFieldMetadata = objectMetadataItem.fields.find( diff --git a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts index 30b095a30189..5743292a5633 100644 --- a/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts +++ b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts @@ -4,6 +4,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const useFindManyRecordsSelectedInContextStore = ({ @@ -30,10 +31,13 @@ export const useFindManyRecordsSelectedInContextStore = ({ instanceId, ); + const { filterValueDependencies } = useFilterValueDependencies(); + const queryFilter = computeContextStoreFilters( contextStoreTargetedRecordsRule, contextStoreFilters, objectMetadataItem, + filterValueDependencies, ); const { records, loading, totalCount } = useFindManyRecords({ diff --git a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts index 4ea9f4a7a35d..af1fe415130e 100644 --- a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts +++ b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts @@ -1,14 +1,20 @@ import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { expect } from '@storybook/test'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + describe('computeContextStoreFilters', () => { const personObjectMetadataItem = generatedMockObjectMetadataItems.find( (item) => item.nameSingular === 'person', )!; + const mockFilterValueDependencies: FilterValueDependencies = { + currentWorkspaceMemberId: '32219445-f587-4c40-b2b1-6d3205ed96da', + }; + it('should work for selection mode', () => { const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = { mode: 'selection', @@ -19,6 +25,7 @@ describe('computeContextStoreFilters', () => { contextStoreTargetedRecordsRule, [], personObjectMetadataItem, + mockFilterValueDependencies, ); expect(filters).toEqual({ @@ -61,6 +68,7 @@ describe('computeContextStoreFilters', () => { contextStoreTargetedRecordsRule, contextStoreFilters, personObjectMetadataItem, + mockFilterValueDependencies, ); expect(filters).toEqual({ diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts index e685c35f59aa..91ede0aab9a9 100644 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -2,6 +2,7 @@ import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextS import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; @@ -9,12 +10,14 @@ export const computeContextStoreFilters = ( contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule, contextStoreFilters: Filter[], objectMetadataItem: ObjectMetadataItem, + filterValueDependencies: FilterValueDependencies, ) => { let queryFilter: RecordGqlOperationFilter | undefined; if (contextStoreTargetedRecordsRule.mode === 'exclusion') { queryFilter = makeAndFilterVariables([ computeViewRecordGqlOperationFilter( + filterValueDependencies, contextStoreFilters, objectMetadataItem?.fields ?? [], [], @@ -39,6 +42,7 @@ export const computeContextStoreFilters = ( }, } : computeViewRecordGqlOperationFilter( + filterValueDependencies, contextStoreFilters, objectMetadataItem?.fields ?? [], [], diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems.tsx new file mode 100644 index 000000000000..fa975fabc28b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems.tsx @@ -0,0 +1,45 @@ +import { StyledMultipleSelectDropdownAvatarChip } from '@/object-record/select/components/StyledMultipleSelectDropdownAvatarChip'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import styled from '@emotion/styled'; +import { MenuItemMultiSelectAvatar } from 'twenty-ui'; + +const StyledPinnedItemsContainer = styled.div` + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spacing(1)}; +`; + +export const ObjectFilterDropdownRecordPinnedItems = (props: { + selectableItems: SelectableItem[]; + onChange: ( + selectableItem: SelectableItem, + isNewCheckedValue: boolean, + ) => void; +}) => { + return ( + + {props.selectableItems.map((selectableItem) => { + return ( + { + props.onChange(selectableItem, newCheckedValue); + }} + avatar={ + + } + /> + ); + })} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index 9a61611745bb..67f9ff7069d2 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -2,16 +2,27 @@ import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectFilterDropdownRecordPinnedItems } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordPinnedItems'; +import { CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID } from '@/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { RelationFilterValue } from '@/views/view-filter-value/types/RelationFilterValue'; +import { relationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/relationFilterValueSchema'; +import { IconUserCircle } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; -export const EMPTY_FILTER_VALUE = '[]'; +export const EMPTY_FILTER_VALUE: string = JSON.stringify({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: [], +} satisfies RelationFilterValue); + export const MAX_RECORDS_TO_DISPLAY = 3; type ObjectFilterDropdownRecordSelectProps = { @@ -54,9 +65,27 @@ export const ObjectFilterDropdownRecordSelect = ({ const selectedFilter = useRecoilValue(selectedFilterState); + const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = + relationFilterValueSchema + .catch({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: [], + }) + .parse(selectedFilter?.value); + const objectNameSingular = filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular; + if (!isDefined(objectNameSingular)) { + throw new Error('relationObjectMetadataNameSingular is not defined'); + } + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: objectNameSingular, + }); + + const objectLabelPlural = objectMetadataItem?.labelPlural; + if (!isDefined(objectNameSingular)) { throw new Error('objectNameSingular is not defined'); } @@ -69,27 +98,53 @@ export const ObjectFilterDropdownRecordSelect = ({ limit: 10, }); + const currentWorkspaceMemberSelectableItem: SelectableItem = { + id: CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID, + name: 'Me', + isSelected: isCurrentWorkspaceMemberSelected, + AvatarIcon: IconUserCircle, + }; + + const pinnedSelectableItems: SelectableItem[] = + objectNameSingular === 'workspaceMember' + ? [currentWorkspaceMemberSelectableItem] + : []; + + const filteredPinnedSelectableItems = pinnedSelectableItems.filter((item) => + item.name + .toLowerCase() + .includes(objectFilterDropdownSearchInput.toLowerCase()), + ); + const handleMultipleRecordSelectChange = ( - recordToSelect: SelectableItem, - newSelectedValue: boolean, + itemToSelect: SelectableItem, + isNewSelectedValue: boolean, ) => { if (loading) { return; } - const newSelectedRecordIds = newSelectedValue - ? [...objectFilterDropdownSelectedRecordIds, recordToSelect.id] - : objectFilterDropdownSelectedRecordIds.filter( - (id) => id !== recordToSelect.id, - ); + const isItemCurrentWorkspaceMember = + itemToSelect.id === CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID; - if (newSelectedRecordIds.length === 0) { - emptyFilterButKeepDefinition(); - deleteCombinedViewFilter(fieldId); - return; - } + const selectedRecordIdsWithAddedRecord = [ + ...objectFilterDropdownSelectedRecordIds, + itemToSelect.id, + ]; + const selectedRecordIdsWithRemovedRecord = + objectFilterDropdownSelectedRecordIds.filter( + (id) => id !== itemToSelect.id, + ); + + const newSelectedRecordIds = isItemCurrentWorkspaceMember + ? objectFilterDropdownSelectedRecordIds + : isNewSelectedValue + ? selectedRecordIdsWithAddedRecord + : selectedRecordIdsWithRemovedRecord; - setObjectFilterDropdownSelectedRecordIds(newSelectedRecordIds); + const newIsCurrentWorkspaceMemberSelected = isItemCurrentWorkspaceMember + ? isNewSelectedValue + : isCurrentWorkspaceMemberSelected; const selectedRecordNames = [ ...recordsToSelect, @@ -103,19 +158,32 @@ export const ObjectFilterDropdownRecordSelect = ({ .filter((record) => newSelectedRecordIds.includes(record.id)) .map((record) => record.name); + const selectedPinnedItemNames = newIsCurrentWorkspaceMemberSelected + ? [currentWorkspaceMemberSelectableItem.name] + : []; + + const selectedItemNames = [ + ...selectedPinnedItemNames, + ...selectedRecordNames, + ]; + const filterDisplayValue = - selectedRecordNames.length > MAX_RECORDS_TO_DISPLAY - ? `${selectedRecordNames.length} companies` - : selectedRecordNames.join(', '); + selectedItemNames.length > MAX_RECORDS_TO_DISPLAY + ? `${selectedItemNames.length} ${objectLabelPlural.toLowerCase()}` + : selectedItemNames.join(', '); if ( isDefined(filterDefinitionUsedInDropdown) && isDefined(selectedOperandInDropdown) ) { const newFilterValue = - newSelectedRecordIds.length > 0 - ? JSON.stringify(newSelectedRecordIds) - : EMPTY_FILTER_VALUE; + newSelectedRecordIds.length > 0 || newIsCurrentWorkspaceMemberSelected + ? JSON.stringify({ + isCurrentWorkspaceMemberSelected: + newIsCurrentWorkspaceMemberSelected, + selectedRecordIds: newSelectedRecordIds, + } satisfies RelationFilterValue) + : ''; const viewFilter = currentViewWithCombinedFiltersAndSorts?.viewFilters.find( @@ -139,15 +207,26 @@ export const ObjectFilterDropdownRecordSelect = ({ }; return ( - + <> + {filteredPinnedSelectableItems.length > 0 && ( + <> + + + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId.ts new file mode 100644 index 000000000000..b8f7455e347d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/constants/CurrentWorkspaceMemberSelectableItemId.ts @@ -0,0 +1,2 @@ +export const CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID = + 'CURRENT_WORKSPACE_MEMBER'; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts index eabd56cf6eba..2775359bde43 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAggregateRecordsForRecordBoardColumn.ts @@ -3,6 +3,7 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/Record import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { buildRecordGqlFieldsAggregateForRecordBoard } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForRecordBoard'; import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; @@ -53,7 +54,11 @@ export const useAggregateRecordsForRecordBoardColumn = () => { ); const recordIndexFilters = useRecoilValue(recordIndexFiltersState); + + const { filterValueDependencies } = useFilterValueDependencies(); + const requestFilters = computeViewRecordGqlOperationFilter( + filterValueDependencies, recordIndexFilters, objectMetadataItem.fields, recordIndexViewFilterGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterValueDependencies.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterValueDependencies.ts new file mode 100644 index 000000000000..e1f423041792 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useFilterValueDependencies.ts @@ -0,0 +1,16 @@ +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; +import { useRecoilValue } from 'recoil'; + +export const useFilterValueDependencies = (): { + filterValueDependencies: FilterValueDependencies; +} => { + const { id: currentWorkspaceMemberId } = + useRecoilValue(currentWorkspaceMemberState) ?? {}; + + return { + filterValueDependencies: { + currentWorkspaceMemberId, + }, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/types/FilterValueDependencies.ts b/packages/twenty-front/src/modules/object-record/record-filter/types/FilterValueDependencies.ts new file mode 100644 index 000000000000..0014490eb946 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/types/FilterValueDependencies.ts @@ -0,0 +1,3 @@ +export interface FilterValueDependencies { + currentWorkspaceMemberId?: string; +} diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts index 31e31f86afb3..694107ae3c12 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/computeViewRecordGqlOperationFilter.test.ts @@ -1,4 +1,5 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { getCompaniesMock } from '~/testing/mock-data/companies'; @@ -14,6 +15,10 @@ const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( (item) => item.nameSingular === 'person', )!; +const mockFilterValueDependencies: FilterValueDependencies = { + currentWorkspaceMemberId: '32219445-f587-4c40-b2b1-6d3205ed96da', +}; + jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); describe('computeViewRecordGqlOperationFilter', () => { @@ -38,6 +43,7 @@ describe('computeViewRecordGqlOperationFilter', () => { }; const result = computeViewRecordGqlOperationFilter( + mockFilterValueDependencies, [nameFilter], companyMockObjectMetadataItem.fields, [], @@ -90,6 +96,7 @@ describe('computeViewRecordGqlOperationFilter', () => { }; const result = computeViewRecordGqlOperationFilter( + mockFilterValueDependencies, [nameFilter, employeesFilter], companyMockObjectMetadataItem.fields, [], @@ -176,6 +183,7 @@ describe('should work as expected for the different field types', () => { }; const result = computeViewRecordGqlOperationFilter( + mockFilterValueDependencies, [ addressFilterContains, addressFilterDoesNotContain, @@ -558,6 +566,7 @@ describe('should work as expected for the different field types', () => { }; const result = computeViewRecordGqlOperationFilter( + mockFilterValueDependencies, [ phonesFilterContains, phonesFilterDoesNotContain, @@ -759,6 +768,7 @@ describe('should work as expected for the different field types', () => { }; const result = computeViewRecordGqlOperationFilter( + mockFilterValueDependencies, [ emailsFilterContains, emailsFilterDoesNotContain, @@ -914,6 +924,7 @@ describe('should work as expected for the different field types', () => { }; const result = computeViewRecordGqlOperationFilter( + mockFilterValueDependencies, [ dateFilterIsAfter, dateFilterIsBefore, @@ -1030,6 +1041,7 @@ describe('should work as expected for the different field types', () => { }; const result = computeViewRecordGqlOperationFilter( + mockFilterValueDependencies, [ employeesFilterIsGreaterThan, employeesFilterIsLessThan, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts index 3a342704439e..756ca6206cb4 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/computeViewRecordGqlOperationFilter.ts @@ -28,14 +28,17 @@ import { convertRatingToRatingValue, } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; import { getEmptyRecordGqlOperationFilter } from '@/object-record/record-filter/utils/getEmptyRecordGqlOperationFilter'; import { ViewFilterGroup } from '@/views/types/ViewFilterGroup'; import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator'; import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue'; +import { relationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/relationFilterValueSchema'; import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; import { z } from 'zod'; const computeFilterRecordGqlOperationFilter = ( + filterValueDependencies: FilterValueDependencies, filter: Filter, fields: Pick[], ): RecordGqlOperationFilter | undefined => { @@ -303,32 +306,41 @@ const computeFilterRecordGqlOperationFilter = ( } case 'RELATION': { if (!isEmptyOperand) { - try { - JSON.parse(filter.value); - } catch (e) { - throw new Error( - `Cannot parse filter value for RELATION filter : "${filter.value}"`, - ); - } + const { isCurrentWorkspaceMemberSelected, selectedRecordIds } = + relationFilterValueSchema.parse(filter.value); - const parsedRecordIds = JSON.parse(filter.value) as string[]; + const recordIds = isCurrentWorkspaceMemberSelected + ? [ + ...selectedRecordIds, + filterValueDependencies.currentWorkspaceMemberId, + ] + : selectedRecordIds; - if (parsedRecordIds.length === 0) return; + if (recordIds.length === 0) return; switch (filter.operand) { case ViewFilterOperand.Is: return { [correspondingField.name + 'Id']: { - in: parsedRecordIds, + in: recordIds, } as RelationFilter, }; case ViewFilterOperand.IsNot: { - if (parsedRecordIds.length === 0) return; + if (recordIds.length === 0) return; return { - not: { - [correspondingField.name + 'Id']: { - in: parsedRecordIds, - } as RelationFilter, - }, + or: [ + { + not: { + [correspondingField.name + 'Id']: { + in: recordIds, + } as RelationFilter, + }, + }, + { + [correspondingField.name + 'Id']: { + is: 'NULL', + } as RelationFilter, + }, + ], }; } default: @@ -869,6 +881,7 @@ const computeFilterRecordGqlOperationFilter = ( }; const computeViewFilterGroupRecordGqlOperationFilter = ( + filterValueDependencies: FilterValueDependencies, filters: Filter[], fields: Pick[], viewFilterGroups: ViewFilterGroup[], @@ -887,7 +900,13 @@ const computeViewFilterGroupRecordGqlOperationFilter = ( ); const groupRecordGqlOperationFilters = groupFilters - .map((filter) => computeFilterRecordGqlOperationFilter(filter, fields)) + .map((filter) => + computeFilterRecordGqlOperationFilter( + filterValueDependencies, + filter, + fields, + ), + ) .filter(isDefined); const subGroupRecordGqlOperationFilters = viewFilterGroups @@ -897,6 +916,7 @@ const computeViewFilterGroupRecordGqlOperationFilter = ( ) .map((subViewFilterGroup) => computeViewFilterGroupRecordGqlOperationFilter( + filterValueDependencies, filters, fields, viewFilterGroups, @@ -932,6 +952,7 @@ const computeViewFilterGroupRecordGqlOperationFilter = ( }; export const computeViewRecordGqlOperationFilter = ( + filterValueDependencies: FilterValueDependencies, filters: Filter[], fields: Pick[], viewFilterGroups: ViewFilterGroup[], @@ -939,7 +960,11 @@ export const computeViewRecordGqlOperationFilter = ( const regularRecordGqlOperationFilter: RecordGqlOperationFilter[] = filters .filter((filter) => !filter.viewFilterGroupId) .map((regularFilter) => - computeFilterRecordGqlOperationFilter(regularFilter, fields), + computeFilterRecordGqlOperationFilter( + filterValueDependencies, + regularFilter, + fields, + ), ) .filter(isDefined); @@ -949,6 +974,7 @@ export const computeViewRecordGqlOperationFilter = ( const advancedRecordGqlOperationFilter = computeViewFilterGroupRecordGqlOperationFilter( + filterValueDependencies, filters, fields, viewFilterGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx index 375e5cfc02c2..28445351179f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -5,6 +5,7 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -40,6 +41,8 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = contextStoreFiltersComponentState, ); + const { filterValueDependencies } = useFilterValueDependencies(); + const { totalCount } = useFindManyRecords({ ...findManyRecordsParams, recordGqlFields: { @@ -49,6 +52,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = contextStoreTargetedRecordsRule, contextStoreFilters, objectMetadataItem, + filterValueDependencies, ), limit: 1, skip: contextStoreTargetedRecordsRule.mode === 'selection', diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts index 985d2262846d..7acaf23599fc 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts @@ -8,6 +8,7 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; @@ -71,10 +72,13 @@ export const useExportFetchRecords = ({ contextStoreFiltersComponentState, ); + const { filterValueDependencies } = useFilterValueDependencies(); + const queryFilter = computeContextStoreFilters( contextStoreTargetedRecordsRule, contextStoreFilters, objectMetadataItem, + filterValueDependencies, ); const findManyRecordsParams = useFindManyRecordIndexTableParams( diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts index c05d39917037..df15ba3715c5 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts @@ -1,5 +1,6 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; @@ -35,7 +36,10 @@ export const useFindManyRecordIndexTableParams = ( recordTableId, ); + const { filterValueDependencies } = useFilterValueDependencies(); + const stateFilter = computeViewRecordGqlOperationFilter( + filterValueDependencies, tableFilters, objectMetadataItem?.fields ?? [], tableViewFilterGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index fee8cc97aa9a..9df7f7a2919f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -7,6 +7,7 @@ import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/useSetRecordBoardRecordIds'; import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; @@ -56,7 +57,11 @@ export const useLoadRecordIndexBoard = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); + + const { filterValueDependencies } = useFilterValueDependencies(); + const requestFilters = computeViewRecordGqlOperationFilter( + filterValueDependencies, recordIndexFilters, objectMetadataItem?.fields ?? [], recordIndexViewFilterGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index 2738118054fe..ff60d05d014c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -5,6 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; @@ -43,7 +44,10 @@ export const useLoadRecordIndexBoardColumn = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); + const { filterValueDependencies } = useFilterValueDependencies(); + const requestFilters = computeViewRecordGqlOperationFilter( + filterValueDependencies, recordIndexFilters, objectMetadataItem?.fields ?? [], recordIndexViewFilterGroups, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx index c9851f04d118..c9217a78aa94 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter.tsx @@ -1,5 +1,6 @@ import { useAggregateRecords } from '@/object-record/hooks/useAggregateRecords'; import { computeAggregateValueAndLabel } from '@/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { useRecordGroupFilter } from '@/object-record/record-group/hooks/useRecordGroupFilter'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; @@ -26,7 +27,11 @@ export const useAggregateRecordsForRecordTableColumnFooter = ( ); const recordIndexFilters = useRecoilValue(recordIndexFiltersState); + + const { filterValueDependencies } = useFilterValueDependencies(); + const requestFilters = computeViewRecordGqlOperationFilter( + filterValueDependencies, recordIndexFilters, objectMetadataItem.fields, recordIndexViewFilterGroups, diff --git a/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx index 3a96cc523630..bb88fbdecea3 100644 --- a/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx @@ -1,9 +1,9 @@ -import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { AvatarChip, MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui'; +import { MenuItem, MenuItemMultiSelectAvatar } from 'twenty-ui'; +import { StyledMultipleSelectDropdownAvatarChip } from '@/object-record/select/components/StyledMultipleSelectDropdownAvatarChip'; import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -13,16 +13,6 @@ import { useSelectableListStates } from '@/ui/layout/selectable-list/hooks/inter import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -const StyledAvatarChip = styled(AvatarChip)` - &.avatar-icon-container { - color: ${({ theme }) => theme.font.color.secondary}; - gap: ${({ theme }) => theme.spacing(2)}; - padding-left: 0px; - padding-right: 0px; - font-size: ${({ theme }) => theme.font.size.md}; - } -`; - export const MultipleSelectDropdown = ({ selectableListId, hotkeyScope, @@ -129,7 +119,7 @@ export const MultipleSelectDropdown = ({ handleItemSelectChange(item, newCheckedValue); }} avatar={ - theme.font.color.secondary}; + gap: ${({ theme }) => theme.spacing(2)}; + padding-left: 0px; + padding-right: 0px; + font-size: ${({ theme }) => theme.font.size.md}; + } +`; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx b/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx index 4e3836f6ef62..7d8817adf247 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarFilterEffect.tsx @@ -10,6 +10,7 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; +import { relationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/relationFilterValueSchema'; import { isDefined } from '~/utils/isDefined'; type ViewBarFilterEffectProps = { @@ -69,12 +70,14 @@ export const ViewBarFilterEffect = ({ filterDefinitionUsedInDropdown?.fieldMetadataId, ); - const viewFilterSelectedRecords = isNonEmptyString( - viewFilterUsedInDropdown?.value, - ) - ? JSON.parse(viewFilterUsedInDropdown.value) - : []; - setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecords); + const { selectedRecordIds } = relationFilterValueSchema + .catch({ + isCurrentWorkspaceMemberSelected: false, + selectedRecordIds: [], + }) + .parse(viewFilterUsedInDropdown?.value); + + setObjectFilterDropdownSelectedRecordIds(selectedRecordIds); } else if ( isDefined(filterDefinitionUsedInDropdown) && ['SELECT', 'MULTI_SELECT'].includes(filterDefinitionUsedInDropdown.type) diff --git a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts index 4b28e5e781f9..7e31b313f885 100644 --- a/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useQueryVariablesFromActiveFieldsOfViewOrDefaultView.ts @@ -1,5 +1,6 @@ import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies'; import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews'; import { getQueryVariablesFromView } from '@/views/utils/getQueryVariablesFromView'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; @@ -22,11 +23,14 @@ export const useQueryVariablesFromActiveFieldsOfViewOrDefaultView = ({ const isJsonFilterEnabled = useIsFeatureEnabled('IS_JSON_FILTER_ENABLED'); + const { filterValueDependencies } = useFilterValueDependencies(); + const { filter, orderBy } = getQueryVariablesFromView({ fieldMetadataItems: activeFieldMetadataItems, objectMetadataItem, view, isJsonFilterEnabled, + filterValueDependencies, }); return { diff --git a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts index 7768035b6359..75eb01302fd2 100644 --- a/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts +++ b/packages/twenty-front/src/modules/views/utils/getQueryVariablesFromView.ts @@ -3,6 +3,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; +import { FilterValueDependencies } from '@/object-record/record-filter/types/FilterValueDependencies'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; import { View } from '@/views/types/View'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; @@ -14,11 +15,13 @@ export const getQueryVariablesFromView = ({ fieldMetadataItems, objectMetadataItem, isJsonFilterEnabled, + filterValueDependencies, }: { view: View | null | undefined; fieldMetadataItems: FieldMetadataItem[]; objectMetadataItem: ObjectMetadataItem; isJsonFilterEnabled: boolean; + filterValueDependencies: FilterValueDependencies; }) => { if (!isDefined(view)) { return { @@ -39,6 +42,7 @@ export const getQueryVariablesFromView = ({ }); const filter = computeViewRecordGqlOperationFilter( + filterValueDependencies, mapViewFiltersToFilters(viewFilters, filterDefinitions), objectMetadataItem?.fields ?? [], viewFilterGroups ?? [], diff --git a/packages/twenty-front/src/modules/views/view-filter-value/types/RelationFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/types/RelationFilterValue.ts new file mode 100644 index 000000000000..321fa8a93432 --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/types/RelationFilterValue.ts @@ -0,0 +1,4 @@ +import { relationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/relationFilterValueSchema'; +import { z } from 'zod'; + +export type RelationFilterValue = z.infer; diff --git a/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/relationFilterValueSchema.ts b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/relationFilterValueSchema.ts new file mode 100644 index 000000000000..e8bbfa87fbbc --- /dev/null +++ b/packages/twenty-front/src/modules/views/view-filter-value/validation-schemas/relationFilterValueSchema.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +export const relationFilterValueSchema = z + .string() + .transform((value, ctx) => { + try { + return JSON.parse(value); + } catch (error) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: (error as Error).message, + }); + return z.NEVER; + } + }) + .pipe( + z.object({ + isCurrentWorkspaceMemberSelected: z.boolean(), + selectedRecordIds: z.array(z.string()), + }), + );