Added dashboard chart advanced filter components (#15095)

This PR adds the components for advanced filters on chart settings.

It also refactors what's in common between this new command menu page
and the workflow advanced filters.
This commit is contained in:
Lucas Bordeau
2025-10-15 16:06:15 +02:00
committed by GitHub
parent bd16c77fed
commit 7f8ec35b5d
24 changed files with 445 additions and 190 deletions
@@ -0,0 +1,10 @@
import styled from '@emotion/styled';
const StyledColumn = styled.div`
display: flex;
flex-direction: column;
width: 100%;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const AdvancedFilterCommandMenuColumn = StyledColumn;
@@ -0,0 +1,100 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AdvancedFilterCommandMenuCreateRootFilterButton } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuCreateRootFilterButton';
import { AdvancedFilterCommandMenuRecordFilterColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuRecordFilterColumn';
import { AdvancedFilterCommandMenuRecordFilterGroupColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuRecordFilterGroupColumn';
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector';
import { isRecordFilterGroupChildARecordFilterGroup } from '@/object-record/advanced-filter/utils/isRecordFilterGroupChildARecordFilterGroup';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
align-items: start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledChildContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(6)};
width: 100%;
`;
export type AdvancedFilterCommandMenuContainerProps = {
readonly?: boolean;
onUpdate?: () => void;
objectMetadataItem: ObjectMetadataItem;
VariablePicker?: VariablePickerComponent;
isWorkflowFindRecords?: boolean;
};
export const AdvancedFilterCommandMenuContainer = ({
readonly,
onUpdate,
objectMetadataItem,
VariablePicker,
isWorkflowFindRecords,
}: AdvancedFilterCommandMenuContainerProps) => {
const rootRecordFilterGroup = useRecoilComponentValue(
rootLevelRecordFilterGroupComponentSelector,
);
const { childRecordFiltersAndRecordFilterGroups } =
useChildRecordFiltersAndRecordFilterGroups({
recordFilterGroupId: rootRecordFilterGroup?.id,
});
return (
<AdvancedFilterContext.Provider
value={{
onUpdate: readonly ? undefined : onUpdate,
isWorkflowFindRecords,
readonly,
VariablePicker,
objectMetadataItem,
}}
>
{isDefined(rootRecordFilterGroup) ? (
<StyledContainer>
<StyledChildContainer>
{childRecordFiltersAndRecordFilterGroups.map(
(recordFilterGroupChild, recordFilterGroupChildIndex) =>
isRecordFilterGroupChildARecordFilterGroup(
recordFilterGroupChild,
) ? (
<AdvancedFilterCommandMenuRecordFilterGroupColumn
key={recordFilterGroupChild.id}
parentRecordFilterGroup={rootRecordFilterGroup}
recordFilterGroup={recordFilterGroupChild}
recordFilterGroupIndex={recordFilterGroupChildIndex}
/>
) : (
<AdvancedFilterCommandMenuRecordFilterColumn
key={recordFilterGroupChild.id}
recordFilterGroup={rootRecordFilterGroup}
recordFilter={recordFilterGroupChild}
recordFilterIndex={recordFilterGroupChildIndex}
/>
),
)}
</StyledChildContainer>
{!readonly && (
<AdvancedFilterAddFilterRuleSelect
recordFilterGroup={rootRecordFilterGroup}
/>
)}
</StyledContainer>
) : (
<AdvancedFilterCommandMenuCreateRootFilterButton
objectMetadataItem={objectMetadataItem}
/>
)}
</AdvancedFilterContext.Provider>
);
};
@@ -0,0 +1,94 @@
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { rootLevelRecordFilterGroupComponentSelector } from '@/object-record/advanced-filter/states/rootLevelRecordFilterGroupComponentSelector';
import { useUpsertRecordFilterGroup } from '@/object-record/record-filter-group/hooks/useUpsertRecordFilterGroup';
import { useCreateEmptyRecordFilterFromFieldMetadataItem } from '@/object-record/record-filter/hooks/useCreateEmptyRecordFilterFromFieldMetadataItem';
import { useUpsertRecordFilter } from '@/object-record/record-filter/hooks/useUpsertRecordFilter';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyState';
import { hasInitializedCurrentRecordFiltersComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFiltersComponentFamilyState';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecordFilterGroupLogicalOperator } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconFilter } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { v4 } from 'uuid';
export const AdvancedFilterCommandMenuCreateRootFilterButton = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const { readonly } = useContext(AdvancedFilterContext);
const rootRecordFilterGroup = useRecoilComponentValue(
rootLevelRecordFilterGroupComponentSelector,
);
const availableFieldMetadataItemsForFilter = useRecoilValue(
availableFieldMetadataItemsForFilterFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
const defaultFieldMetadataItem =
availableFieldMetadataItemsForFilter.find(
(fieldMetadataItem) =>
fieldMetadataItem.id ===
objectMetadataItem?.labelIdentifierFieldMetadataId,
) ?? availableFieldMetadataItemsForFilter[0];
const { upsertRecordFilterGroup } = useUpsertRecordFilterGroup();
const { upsertRecordFilter } = useUpsertRecordFilter();
const { createEmptyRecordFilterFromFieldMetadataItem } =
useCreateEmptyRecordFilterFromFieldMetadataItem();
const setHasInitializedCurrentRecordFilters =
useSetRecoilComponentFamilyState(
hasInitializedCurrentRecordFiltersComponentFamilyState,
{},
);
const addRootRecordFilterGroup = () => {
const alreadyHasAdvancedFilterGroup = isDefined(rootRecordFilterGroup);
if (!alreadyHasAdvancedFilterGroup) {
setHasInitializedCurrentRecordFilters(false);
const newRecordFilterGroup = {
id: v4(),
logicalOperator: RecordFilterGroupLogicalOperator.AND,
};
upsertRecordFilterGroup(newRecordFilterGroup);
if (!isDefined(defaultFieldMetadataItem)) {
throw new Error('Missing default filter definition');
}
const { newRecordFilter } = createEmptyRecordFilterFromFieldMetadataItem(
defaultFieldMetadataItem,
);
newRecordFilter.recordFilterGroupId = newRecordFilterGroup.id;
upsertRecordFilter(newRecordFilter);
}
};
return (
<Button
Icon={IconFilter}
size="small"
variant="secondary"
accent="default"
onClick={addRootRecordFilterGroup}
ariaLabel="Add filter"
title="Add filter"
disabled={readonly}
/>
);
};
@@ -0,0 +1,23 @@
import { useRecordFilterField } from '@/object-record/advanced-filter/hooks/useRecordFilterField';
import { SelectControl } from '@/ui/input/components/SelectControl';
type AdvancedFilterCommandMenuFieldSelectDisabledProps = {
recordFilterId: string;
};
export const AdvancedFilterCommandMenuFieldSelectDisabled = ({
recordFilterId,
}: AdvancedFilterCommandMenuFieldSelectDisabledProps) => {
const { label, icon } = useRecordFilterField(recordFilterId);
return (
<SelectControl
selectedOption={{
label,
value: null,
Icon: icon,
}}
isDisabled
/>
);
};
@@ -0,0 +1,64 @@
import { AdvancedFilterLogicalOperatorDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown';
import { ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS } from '@/object-record/advanced-filter/constants/AdvancedFilterLogicalOperatorOptions';
import { DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET } from '@/object-record/advanced-filter/constants/DefaultAdvancedFilterDropdownOffset';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { Select } from '@/ui/input/components/Select';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { capitalize } from 'twenty-shared/utils';
const StyledText = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
height: ${({ theme }) => theme.spacing(8)};
`;
const StyledContainer = styled.div`
align-items: start;
display: flex;
min-width: ${({ theme }) => theme.spacing(20)};
color: ${({ theme }) => theme.font.color.tertiary};
`;
type AdvancedFilterCommandMenuLogicalOperatorCellProps = {
index: number;
recordFilterGroup: RecordFilterGroup;
};
export const AdvancedFilterCommandMenuLogicalOperatorCell = ({
index,
recordFilterGroup,
}: AdvancedFilterCommandMenuLogicalOperatorCellProps) => {
const { readonly } = useContext(AdvancedFilterContext);
return (
<StyledContainer>
{index === 0 ? (
<StyledText>Where</StyledText>
) : index === 1 ? (
readonly ? (
<Select
dropdownWidth={GenericDropdownContentWidth.Narrow}
dropdownId={`advanced-filter-logical-operator-${recordFilterGroup.id}`}
value={recordFilterGroup.logicalOperator}
options={ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS}
dropdownOffset={DEFAULT_ADVANCED_FILTER_DROPDOWN_OFFSET}
disabled
/>
) : (
<AdvancedFilterLogicalOperatorDropdown
recordFilterGroup={recordFilterGroup}
/>
)
) : (
<StyledText>
{capitalize(recordFilterGroup.logicalOperator.toLowerCase())}
</StyledText>
)}
</StyledContainer>
);
};
@@ -0,0 +1,75 @@
import { AdvancedFilterCommandMenuColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuColumn';
import { AdvancedFilterCommandMenuFieldSelectDisabled } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuFieldSelectDisabled';
import { AdvancedFilterCommandMenuLogicalOperatorCell } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuLogicalOperatorCell';
import { AdvancedFilterCommandMenuRecordFilterOperandSelect } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuRecordFilterOperandSelect';
import { AdvancedFilterCommandMenuValueFormInput } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormInput';
import { AdvancedFilterFieldSelectDropdownButton } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectDropdownButton';
import { AdvancedFilterRecordFilterOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOptionsDropdown';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { getAdvancedFilterObjectFilterDropdownComponentInstanceId } from '@/object-record/advanced-filter/utils/getAdvancedFilterObjectFilterDropdownComponentInstanceId';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import styled from '@emotion/styled';
import { useContext } from 'react';
const StyledContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
gap: ${({ theme }) => theme.spacing(1)};
`;
type AdvancedFilterCommandMenuRecordFilterColumnProps = {
recordFilterGroup: RecordFilterGroup;
recordFilter: RecordFilter;
recordFilterIndex: number;
};
export const AdvancedFilterCommandMenuRecordFilterColumn = ({
recordFilterGroup,
recordFilter,
recordFilterIndex,
}: AdvancedFilterCommandMenuRecordFilterColumnProps) => {
const { readonly } = useContext(AdvancedFilterContext);
return (
<ObjectFilterDropdownComponentInstanceContext.Provider
value={{
instanceId: getAdvancedFilterObjectFilterDropdownComponentInstanceId(
recordFilter.id,
),
}}
>
<AdvancedFilterCommandMenuColumn>
<StyledContainer>
<AdvancedFilterCommandMenuLogicalOperatorCell
index={recordFilterIndex}
recordFilterGroup={recordFilterGroup}
/>
{!readonly && (
<AdvancedFilterRecordFilterOptionsDropdown
recordFilterId={recordFilter.id}
/>
)}
</StyledContainer>
{readonly ? (
<AdvancedFilterCommandMenuFieldSelectDisabled
recordFilterId={recordFilter.id}
/>
) : (
<AdvancedFilterFieldSelectDropdownButton
recordFilterId={recordFilter.id}
/>
)}
<AdvancedFilterCommandMenuRecordFilterOperandSelect
recordFilterId={recordFilter.id}
/>
<AdvancedFilterCommandMenuValueFormInput
recordFilterId={recordFilter.id}
/>
</AdvancedFilterCommandMenuColumn>
</ObjectFilterDropdownComponentInstanceContext.Provider>
);
};
@@ -0,0 +1,61 @@
import { AdvancedFilterCommandMenuRecordFilterColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuRecordFilterColumn';
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { useChildRecordFiltersAndRecordFilterGroups } from '@/object-record/advanced-filter/hooks/useChildRecordFiltersAndRecordFilterGroups';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
align-items: start;
background-color: ${({ theme, isGrayBackground }) =>
isGrayBackground ? theme.background.transparent.lighter : 'transparent'};
border: ${({ theme }) => `1px solid ${theme.border.color.medium}`};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex: 1;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(6)};
padding: ${({ theme }) => theme.spacing(2)};
`;
type AdvancedFilterCommandMenuRecordFilterGroupChildrenProps = {
recordFilterGroupId: string;
};
export const AdvancedFilterCommandMenuRecordFilterGroupChildren = ({
recordFilterGroupId,
}: AdvancedFilterCommandMenuRecordFilterGroupChildrenProps) => {
const { readonly } = useContext(AdvancedFilterContext);
const { currentRecordFilterGroup, childRecordFilters } =
useChildRecordFiltersAndRecordFilterGroups({
recordFilterGroupId,
});
if (!currentRecordFilterGroup) {
return null;
}
const hasParentRecordFilterGroup = isDefined(
currentRecordFilterGroup.parentRecordFilterGroupId,
);
return (
<StyledContainer isGrayBackground={hasParentRecordFilterGroup}>
{childRecordFilters.map((childRecordFilter, childRecordFilterIndex) => (
<AdvancedFilterCommandMenuRecordFilterColumn
key={childRecordFilter.id}
recordFilter={childRecordFilter}
recordFilterIndex={childRecordFilterIndex}
recordFilterGroup={currentRecordFilterGroup}
/>
))}
{!readonly && (
<AdvancedFilterAddFilterRuleSelect
recordFilterGroup={currentRecordFilterGroup}
/>
)}
</StyledContainer>
);
};
@@ -0,0 +1,47 @@
import { AdvancedFilterCommandMenuColumn } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuColumn';
import { AdvancedFilterCommandMenuLogicalOperatorCell } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuLogicalOperatorCell';
import { AdvancedFilterCommandMenuRecordFilterGroupChildren } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuRecordFilterGroupChildren';
import { AdvancedFilterRecordFilterGroupOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterGroupOptionsDropdown';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { type RecordFilterGroup } from '@/object-record/record-filter-group/types/RecordFilterGroup';
import styled from '@emotion/styled';
import { useContext } from 'react';
const StyledContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
gap: ${({ theme }) => theme.spacing(1)};
`;
export const AdvancedFilterCommandMenuRecordFilterGroupColumn = ({
parentRecordFilterGroup,
recordFilterGroup,
recordFilterGroupIndex,
}: {
parentRecordFilterGroup: RecordFilterGroup;
recordFilterGroup: RecordFilterGroup;
recordFilterGroupIndex: number;
}) => {
const { readonly } = useContext(AdvancedFilterContext);
return (
<AdvancedFilterCommandMenuColumn>
<StyledContainer>
<AdvancedFilterCommandMenuLogicalOperatorCell
index={recordFilterGroupIndex}
recordFilterGroup={parentRecordFilterGroup}
/>
{!readonly && (
<AdvancedFilterRecordFilterGroupOptionsDropdown
recordFilterGroupId={recordFilterGroup.id}
/>
)}
</StyledContainer>
<AdvancedFilterCommandMenuRecordFilterGroupChildren
recordFilterGroupId={recordFilterGroup.id}
/>
</AdvancedFilterCommandMenuColumn>
);
};
@@ -0,0 +1,60 @@
import { AdvancedFilterRecordFilterOperandSelectContent } from '@/object-record/advanced-filter/components/AdvancedFilterRecordFilterOperandSelectContent';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
import { getRecordFilterOperands } from '@/object-record/record-filter/utils/getRecordFilterOperands';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
type AdvancedFilterCommandMenuRecordFilterOperandSelectProps = {
recordFilterId: string;
};
export const AdvancedFilterCommandMenuRecordFilterOperandSelect = ({
recordFilterId,
}: AdvancedFilterCommandMenuRecordFilterOperandSelectProps) => {
const { readonly } = useContext(AdvancedFilterContext);
const currentRecordFilters = useRecoilComponentValue(
currentRecordFiltersComponentState,
);
const filter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const isDisabled = !filter?.fieldMetadataId || readonly;
const filterType = filter?.type;
const operandsForFilterType = isDefined(filterType)
? getRecordFilterOperands({
filterType,
subFieldName: filter?.subFieldName,
}).filter((operand) => operand !== RecordFilterOperand.IS_RELATIVE)
: [];
if (isDisabled === true) {
return (
<SelectControl
selectedOption={{
label: filter?.operand
? getOperandLabel(filter.operand)
: 'Select operand',
value: null,
}}
isDisabled
/>
);
}
return (
<AdvancedFilterRecordFilterOperandSelectContent
recordFilterId={recordFilterId}
filter={filter}
operandsForFilterType={operandsForFilterType}
/>
);
};
@@ -0,0 +1,91 @@
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { FormCountryMultiSelectInput } from '@/object-record/record-field/ui/form-types/components/FormCountryMultiSelectInput';
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
import { CURRENCIES } from '@/settings/data-model/constants/Currencies';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useContext } from 'react';
import { type JsonValue } from 'type-fest';
export const AdvancedFilterCommandMenuValueFormCompositeFieldInput = ({
recordFilter,
onChange,
}: {
recordFilter: RecordFilter;
onChange: (newValue: JsonValue) => void;
}) => {
const { VariablePicker } = useContext(AdvancedFilterContext);
const subFieldNameUsedInDropdown = useRecoilComponentValue(
subFieldNameUsedInDropdownComponentState,
);
const filterType = recordFilter.type;
const { readonly } = useContext(AdvancedFilterContext);
return (
<>
{filterType === 'ADDRESS' ? (
subFieldNameUsedInDropdown === 'addressCountry' ? (
<FormCountryMultiSelectInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
)
) : filterType === 'CURRENCY' ? (
recordFilter.subFieldName === 'currencyCode' ? (
<FormMultiSelectFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
options={CURRENCIES}
readonly={readonly}
/>
) : recordFilter.subFieldName === 'amountMicros' ? (
<FormNumberFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : null
) : filterType === 'PHONES' ? (
recordFilter.subFieldName === 'primaryPhoneNumber' ? (
<FormNumberFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
)
) : (
<FormTextFieldInput
defaultValue={recordFilter.value}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
)}
</>
);
};
@@ -0,0 +1,152 @@
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { AdvancedFilterCommandMenuValueFormCompositeFieldInput } from '@/object-record/advanced-filter/command-menu/components/AdvancedFilterCommandMenuValueFormCompositeFieldInput';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { shouldShowFilterTextInput } from '@/object-record/advanced-filter/utils/shouldShowFilterTextInput';
import { useApplyObjectFilterDropdownFilterValue } from '@/object-record/object-filter-dropdown/hooks/useApplyObjectFilterDropdownFilterValue';
import { fieldMetadataItemUsedInDropdownComponentSelector } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemUsedInDropdownComponentSelector';
import { subFieldNameUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/subFieldNameUsedInDropdownComponentState';
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
import { FormMultiSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
import {
type FieldMetadata,
type FieldMultiSelectMetadata,
type FieldSelectMetadata,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { isObject, isString } from '@sniptt/guards';
import { useContext } from 'react';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type JsonValue } from 'type-fest';
export const AdvancedFilterCommandMenuValueFormInput = ({
recordFilterId,
}: {
recordFilterId: string;
}) => {
const { readonly, VariablePicker, objectMetadataItem } = useContext(
AdvancedFilterContext,
);
const currentRecordFilters = useRecoilComponentValue(
currentRecordFiltersComponentState,
);
const subFieldNameUsedInDropdown = useRecoilComponentValue(
subFieldNameUsedInDropdownComponentState,
);
const recordFilter = currentRecordFilters.find(
(recordFilter) => recordFilter.id === recordFilterId,
);
const isDisabled = !recordFilter?.fieldMetadataId || !recordFilter.operand;
const operandHasNoInput =
(recordFilter &&
!configurableViewFilterOperands.has(recordFilter.operand)) ??
true;
const { applyObjectFilterDropdownFilterValue } =
useApplyObjectFilterDropdownFilterValue();
const handleChange = (newValue: JsonValue) => {
if (isString(newValue)) {
applyObjectFilterDropdownFilterValue(newValue);
} else if (Array.isArray(newValue) || isObject(newValue)) {
applyObjectFilterDropdownFilterValue(JSON.stringify(newValue));
} else {
applyObjectFilterDropdownFilterValue(String(newValue));
}
};
const fieldMetadataItemUsedInDropdown = useRecoilComponentValue(
fieldMetadataItemUsedInDropdownComponentSelector,
);
const fieldDefinition = fieldMetadataItemUsedInDropdown
? formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItemUsedInDropdown,
objectMetadataItem: objectMetadataItem,
})
: null;
if (!isDefined(recordFilter)) {
return null;
}
const isFilterableByTextValue = shouldShowFilterTextInput({
recordFilter,
subFieldNameUsedInDropdown,
});
const isFilterableByMultiSelectValue =
recordFilter.type === FieldMetadataType.MULTI_SELECT ||
recordFilter.type === FieldMetadataType.SELECT;
const isFilterableByDateValue =
recordFilter.type === FieldMetadataType.DATE ||
recordFilter.type === FieldMetadataType.DATE_TIME;
if (isDisabled || operandHasNoInput) {
return null;
}
if (isFilterableByTextValue) {
return (
<FormTextFieldInput
label=""
defaultValue={recordFilter.value}
onChange={handleChange}
readonly={readonly}
VariablePicker={VariablePicker}
/>
);
}
if (isDefined(subFieldNameUsedInDropdown)) {
return (
<AdvancedFilterCommandMenuValueFormCompositeFieldInput
recordFilter={recordFilter}
onChange={handleChange}
/>
);
}
if (isFilterableByMultiSelectValue) {
const metadata = fieldDefinition?.metadata as
| FieldMultiSelectMetadata
| FieldSelectMetadata
| undefined;
return (
<FormMultiSelectFieldInput
label=""
defaultValue={recordFilter.value}
onChange={handleChange}
readonly={readonly}
VariablePicker={VariablePicker}
options={metadata?.options ?? []}
/>
);
}
const field = {
type: recordFilter.type as FieldMetadataType,
label: '',
metadata: fieldDefinition?.metadata as FieldMetadata,
};
return (
<FormFieldInput
field={field}
defaultValue={recordFilter.value}
onChange={handleChange}
readonly={readonly}
// VariablePicker is not supported for date filters yet
VariablePicker={isFilterableByDateValue ? undefined : VariablePicker}
/>
);
};
@@ -2,7 +2,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
@@ -13,19 +12,22 @@ import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataIte
import { AdvancedFilterFieldSelectSearchInput } from '@/object-record/advanced-filter/components/AdvancedFilterFieldSelectSearchInput';
import { useAdvancedFilterFieldSelectDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterFieldSelectDropdown';
import { useSelectFieldUsedInAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useSelectFieldUsedInAdvancedFilterDropdown';
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSubMenuFieldTypeComponentState';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { useFilterableFieldMetadataItemsInRecordIndexContext } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItemsInRecordIndexContext';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { getFilterTypeFromFieldType } from 'twenty-shared/utils';
type AdvancedFilterFieldSelectMenuProps = {
@@ -35,8 +37,6 @@ type AdvancedFilterFieldSelectMenuProps = {
export const AdvancedFilterFieldSelectMenu = ({
recordFilterId,
}: AdvancedFilterFieldSelectMenuProps) => {
const { recordIndexId } = useRecordIndexContextOrThrow();
const {
closeAdvancedFilterFieldSelectDropdown,
advancedFilterFieldSelectDropdownId,
@@ -46,13 +46,27 @@ export const AdvancedFilterFieldSelectMenu = ({
objectFilterDropdownSearchInputComponentState,
);
const { filterableFieldMetadataItems } =
useFilterableFieldMetadataItemsInRecordIndexContext();
const { isWorkflowFindRecords, objectMetadataItem } = useContext(
AdvancedFilterContext,
);
const {
filterableFieldMetadataItems: filterableFieldMetadataItemsForRecordIndex,
} = useFilterableFieldMetadataItems(objectMetadataItem.id);
const filterableFieldMetadataItems = isWorkflowFindRecords
? filterableFieldMetadataItemsForRecordIndex.filter((fieldMetadataItem) =>
shouldDisplayFormField({
fieldMetadataItem,
actionType: 'FIND_RECORDS',
}),
)
: filterableFieldMetadataItemsForRecordIndex;
const visibleRecordFields = useRecoilComponentValue(
visibleRecordFieldsComponentSelector,
recordIndexId,
);
const visibleFieldMetadataItemIds = visibleRecordFields.map(
(recordField) => recordField.fieldMetadataItemId,
);
@@ -1,9 +1,13 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { createContext } from 'react';
type AdvancedFilterContextType = {
onUpdate?: () => void;
isWorkflowFindRecords?: boolean;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
objectMetadataItem: ObjectMetadataItem;
};
export const AdvancedFilterContext = createContext<AdvancedFilterContextType>(
@@ -1,25 +1,12 @@
import { AdvancedFilterContext } from '@/object-record/advanced-filter/states/context/AdvancedFilterContext';
import { useFilterableFieldMetadataItems } from '@/object-record/record-filter/hooks/useFilterableFieldMetadataItems';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { useContext } from 'react';
export const useFilterableFieldMetadataItemsInRecordIndexContext = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { isWorkflowFindRecords } = useContext(AdvancedFilterContext);
const {
filterableFieldMetadataItems: filterableFieldMetadataItemsForRecordIndex,
} = useFilterableFieldMetadataItems(objectMetadataItem.id);
const filterableFieldMetadataItems = isWorkflowFindRecords
? filterableFieldMetadataItemsForRecordIndex.filter((fieldMetadataItem) =>
shouldDisplayFormField({
fieldMetadataItem,
actionType: 'FIND_RECORDS',
}),
)
: filterableFieldMetadataItemsForRecordIndex;
const { filterableFieldMetadataItems } = useFilterableFieldMetadataItems(
objectMetadataItem.id,
);
return { filterableFieldMetadataItems };
};