Current workspace member filter (#8016) (#9182)

New branch based on feedback in PR #8950 and issue #8016

---------

Co-authored-by: ad-elias <elias@autodiligence.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
eliasylonen
2024-12-23 18:55:13 +01:00
committed by GitHub
parent 49da7d2ca0
commit 86d74724fb
29 changed files with 347 additions and 155 deletions

View File

@ -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(

View File

@ -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({

View File

@ -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({

View File

@ -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 ?? [],
[],

View File

@ -8,10 +8,10 @@ import { InternalDatePicker } from '@/ui/input/components/internal/date/componen
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue';
import {
resolveDateViewFilterValue,
VariableDateViewFilterValueDirection,
VariableDateViewFilterValueUnit,
} from '@/views/view-filter-value/utils/resolveDateViewFilterValue';
import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue';
import { useState } from 'react';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@ -37,7 +37,7 @@ export const ObjectFilterDropdownDateInput = () => {
| undefined;
const initialFilterValue = selectedFilter
? resolveFilterValue(selectedFilter)
? resolveDateViewFilterValue(selectedFilter)
: null;
const [internalDate, setInternalDate] = useState<Date | null>(
initialFilterValue instanceof Date ? initialFilterValue : null,
@ -98,7 +98,7 @@ export const ObjectFilterDropdownDateInput = () => {
selectedOperandInDropdown === ViewFilterOperand.IsRelative;
const resolvedValue = selectedFilter
? resolveFilterValue(selectedFilter)
? resolveDateViewFilterValue(selectedFilter)
: null;
const relativeDate =

View File

@ -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 (
<StyledPinnedItemsContainer>
{props.selectableItems.map((selectableItem) => {
return (
<MenuItemMultiSelectAvatar
key={selectableItem.id}
selected={selectableItem.isSelected}
onSelectChange={(newCheckedValue) => {
props.onChange(selectableItem, newCheckedValue);
}}
avatar={
<StyledMultipleSelectDropdownAvatarChip
className="avatar-icon-container"
name={selectableItem.name}
avatarUrl={selectableItem.avatarUrl}
LeftIcon={selectableItem.AvatarIcon}
avatarType={selectableItem.avatarType}
isIconInverted={selectableItem.isIconInverted}
placeholderColorSeed={selectableItem.id}
/>
}
/>
);
})}
</StyledPinnedItemsContainer>
);
};

View File

@ -2,16 +2,26 @@ 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 { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
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 = {
@ -26,15 +36,10 @@ export const ObjectFilterDropdownRecordSelect = ({
objectFilterDropdownSearchInputState,
selectedOperandInDropdownState,
selectedFilterState,
setObjectFilterDropdownSelectedRecordIds,
objectFilterDropdownSelectedRecordIdsState,
selectFilter,
emptyFilterButKeepDefinition,
} = useFilterDropdown();
const { deleteCombinedViewFilter } =
useDeleteCombinedViewFilters(viewComponentId);
const { currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView(viewComponentId);
@ -54,9 +59,26 @@ export const ObjectFilterDropdownRecordSelect = ({
const selectedFilter = useRecoilValue(selectedFilterState);
const { isCurrentWorkspaceMemberSelected } = 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 +91,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,
);
setObjectFilterDropdownSelectedRecordIds(newSelectedRecordIds);
const newSelectedRecordIds = isItemCurrentWorkspaceMember
? objectFilterDropdownSelectedRecordIds
: isNewSelectedValue
? selectedRecordIdsWithAddedRecord
: selectedRecordIdsWithRemovedRecord;
const newIsCurrentWorkspaceMemberSelected = isItemCurrentWorkspaceMember
? isNewSelectedValue
: isCurrentWorkspaceMemberSelected;
const selectedRecordNames = [
...recordsToSelect,
@ -103,19 +151,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 +200,26 @@ export const ObjectFilterDropdownRecordSelect = ({
};
return (
<MultipleSelectDropdown
selectableListId="object-filter-record-select-id"
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
itemsToSelect={recordsToSelect}
filteredSelectedItems={filteredSelectedRecords}
selectedItems={selectedRecords}
onChange={handleMultipleRecordSelectChange}
searchFilter={objectFilterDropdownSearchInput}
loadingItems={loading}
/>
<>
{filteredPinnedSelectableItems.length > 0 && (
<>
<ObjectFilterDropdownRecordPinnedItems
selectableItems={filteredPinnedSelectableItems}
onChange={handleMultipleRecordSelectChange}
/>
<DropdownMenuSeparator />
</>
)}
<MultipleSelectDropdown
selectableListId="object-filter-record-select-id"
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
itemsToSelect={recordsToSelect}
filteredSelectedItems={filteredSelectedRecords}
selectedItems={selectedRecords}
onChange={handleMultipleRecordSelectChange}
searchFilter={objectFilterDropdownSearchInput}
loadingItems={loading}
/>
</>
);
};

View File

@ -0,0 +1,2 @@
export const CURRENT_WORKSPACE_MEMBER_SELECTABLE_ITEM_ID =
'CURRENT_WORKSPACE_MEMBER';

View File

@ -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,

View File

@ -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,
},
};
};

View File

@ -0,0 +1,3 @@
export interface FilterValueDependencies {
currentWorkspaceMemberId?: string;
}

View File

@ -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,

View File

@ -28,14 +28,18 @@ 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 { resolveDateViewFilterValue } from '@/views/view-filter-value/utils/resolveDateViewFilterValue';
import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue';
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<Field, 'id' | 'name'>[],
): RecordGqlOperationFilter | undefined => {
@ -124,7 +128,7 @@ const computeFilterRecordGqlOperationFilter = (
}
case 'DATE':
case 'DATE_TIME': {
const resolvedFilterValue = resolveFilterValue(filter);
const resolvedFilterValue = resolveDateViewFilterValue(filter);
const now = roundToNearestMinutes(new Date());
const date =
resolvedFilterValue instanceof Date ? resolvedFilterValue : now;
@ -157,11 +161,8 @@ const computeFilterRecordGqlOperationFilter = (
.object({ start: z.date(), end: z.date() })
.safeParse(resolvedFilterValue).data;
const defaultDateRange = resolveFilterValue({
const defaultDateRange = resolveDateViewFilterValue({
value: 'PAST_1_DAY',
definition: {
type: 'DATE',
},
operand: ViewFilterOperand.IsRelative,
});
@ -303,32 +304,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:
@ -611,9 +621,7 @@ const computeFilterRecordGqlOperationFilter = (
);
}
const options = resolveFilterValue(
filter as Filter & { definition: { type: 'MULTI_SELECT' } },
);
const options = resolveSelectViewFilterValue(filter);
if (options.length === 0) return;
@ -660,9 +668,7 @@ const computeFilterRecordGqlOperationFilter = (
filter.definition,
);
}
const options = resolveFilterValue(
filter as Filter & { definition: { type: 'SELECT' } },
);
const options = resolveSelectViewFilterValue(filter);
if (options.length === 0) return;
@ -869,6 +875,7 @@ const computeFilterRecordGqlOperationFilter = (
};
const computeViewFilterGroupRecordGqlOperationFilter = (
filterValueDependencies: FilterValueDependencies,
filters: Filter[],
fields: Pick<Field, 'id' | 'name'>[],
viewFilterGroups: ViewFilterGroup[],
@ -887,7 +894,13 @@ const computeViewFilterGroupRecordGqlOperationFilter = (
);
const groupRecordGqlOperationFilters = groupFilters
.map((filter) => computeFilterRecordGqlOperationFilter(filter, fields))
.map((filter) =>
computeFilterRecordGqlOperationFilter(
filterValueDependencies,
filter,
fields,
),
)
.filter(isDefined);
const subGroupRecordGqlOperationFilters = viewFilterGroups
@ -897,6 +910,7 @@ const computeViewFilterGroupRecordGqlOperationFilter = (
)
.map((subViewFilterGroup) =>
computeViewFilterGroupRecordGqlOperationFilter(
filterValueDependencies,
filters,
fields,
viewFilterGroups,
@ -932,6 +946,7 @@ const computeViewFilterGroupRecordGqlOperationFilter = (
};
export const computeViewRecordGqlOperationFilter = (
filterValueDependencies: FilterValueDependencies,
filters: Filter[],
fields: Pick<Field, 'id' | 'name'>[],
viewFilterGroups: ViewFilterGroup[],
@ -939,7 +954,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 +968,7 @@ export const computeViewRecordGqlOperationFilter = (
const advancedRecordGqlOperationFilter =
computeViewFilterGroupRecordGqlOperationFilter(
filterValueDependencies,
filters,
fields,
viewFilterGroups,

View File

@ -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',

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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={
<StyledAvatarChip
<StyledMultipleSelectDropdownAvatarChip
className="avatar-icon-container"
name={item.name}
avatarUrl={item.avatarUrl}

View File

@ -0,0 +1,12 @@
import styled from '@emotion/styled';
import { AvatarChip } from 'twenty-ui';
export const StyledMultipleSelectDropdownAvatarChip = 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};
}
`;

View File

@ -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)

View File

@ -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 {

View File

@ -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 ?? [],

View File

@ -0,0 +1,4 @@
import { relationFilterValueSchema } from '@/views/view-filter-value/validation-schemas/relationFilterValueSchema';
import { z } from 'zod';
export type RelationFilterValue = z.infer<typeof relationFilterValueSchema>;

View File

@ -1,7 +0,0 @@
import { ViewFilter } from '@/views/types/ViewFilter';
export const resolveBooleanViewFilterValue = (
viewFilter: Pick<ViewFilter, 'value'>,
) => {
return viewFilter.value === 'true';
};

View File

@ -1,53 +0,0 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue';
import { resolveSelectViewFilterValue } from '@/views/view-filter-value/utils/resolveSelectViewFilterValue';
import {
resolveDateViewFilterValue,
ResolvedDateViewFilterValue,
} from './resolveDateViewFilterValue';
import { resolveBooleanViewFilterValue } from '@/views/view-filter-value/utils/resolveBooleanViewFilterValue';
type ResolvedFilterValue<
T extends FilterableFieldType,
O extends ViewFilterOperand,
> = T extends 'DATE' | 'DATE_TIME'
? ResolvedDateViewFilterValue<O>
: T extends 'NUMBER'
? ReturnType<typeof resolveNumberViewFilterValue>
: T extends 'SELECT' | 'MULTI_SELECT'
? string[]
: T extends 'BOOLEAN'
? boolean
: string;
type PartialFilter<
T extends FilterableFieldType,
O extends ViewFilterOperand,
> = Pick<Filter, 'value'> & {
definition: { type: T };
operand: O;
};
export const resolveFilterValue = <
T extends FilterableFieldType,
O extends ViewFilterOperand,
>(
filter: PartialFilter<T, O>,
) => {
switch (filter.definition.type) {
case 'DATE':
case 'DATE_TIME':
return resolveDateViewFilterValue(filter) as ResolvedFilterValue<T, O>;
case 'NUMBER':
return resolveNumberViewFilterValue(filter) as ResolvedFilterValue<T, O>;
case 'SELECT':
case 'MULTI_SELECT':
return resolveSelectViewFilterValue(filter) as ResolvedFilterValue<T, O>;
case 'BOOLEAN':
return resolveBooleanViewFilterValue(filter) as ResolvedFilterValue<T, O>;
default:
return filter.value as ResolvedFilterValue<T, O>;
}
};

View File

@ -1,7 +0,0 @@
import { ViewFilter } from '@/views/types/ViewFilter';
export const resolveNumberViewFilterValue = (
viewFilter: Pick<ViewFilter, 'value'>,
) => {
return viewFilter.value === '' ? null : +viewFilter.value;
};

View File

@ -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()),
}),
);