mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 20:57:55 +00:00 
			
		
		
		
	Ability to filter by composite's subfields (#6832)
# This PR - Fix #6425 See https://github.com/twentyhq/twenty/issues/7188 because there's some more work to do. --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
		 Pacifique LINJANJA
					Pacifique LINJANJA
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							af4f3cebb0
						
					
				
				
					commit
					4156d7821c
				
			| @@ -6,6 +6,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | |||||||
| import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; | import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; | ||||||
| import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect'; | import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect'; | ||||||
| import { ObjectFilterDropdownTextSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput'; | import { ObjectFilterDropdownTextSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput'; | ||||||
|  | import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; | ||||||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||||||
| import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||||
| import { useRecoilValue } from 'recoil'; | import { useRecoilValue } from 'recoil'; | ||||||
| @@ -98,9 +99,10 @@ export const MultipleFiltersDropdownContent = ({ | |||||||
|                 'ACTOR', |                 'ACTOR', | ||||||
|                 'ARRAY', |                 'ARRAY', | ||||||
|                 'PHONES', |                 'PHONES', | ||||||
|               ].includes(filterDefinitionUsedInDropdown.type) && ( |               ].includes(filterDefinitionUsedInDropdown.type) && | ||||||
|                 <ObjectFilterDropdownTextSearchInput /> |                 !isActorSourceCompositeFilter( | ||||||
|               )} |                   filterDefinitionUsedInDropdown, | ||||||
|  |                 ) && <ObjectFilterDropdownTextSearchInput />} | ||||||
|               {['NUMBER', 'CURRENCY'].includes( |               {['NUMBER', 'CURRENCY'].includes( | ||||||
|                 filterDefinitionUsedInDropdown.type, |                 filterDefinitionUsedInDropdown.type, | ||||||
|               ) && <ObjectFilterDropdownNumberInput />} |               ) && <ObjectFilterDropdownNumberInput />} | ||||||
| @@ -116,7 +118,7 @@ export const MultipleFiltersDropdownContent = ({ | |||||||
|                   <ObjectFilterDropdownRecordSelect /> |                   <ObjectFilterDropdownRecordSelect /> | ||||||
|                 </> |                 </> | ||||||
|               )} |               )} | ||||||
|               {filterDefinitionUsedInDropdown.type === 'SOURCE' && ( |               {isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && ( | ||||||
|                 <> |                 <> | ||||||
|                   <DropdownMenuSeparator /> |                   <DropdownMenuSeparator /> | ||||||
|                   <ObjectFilterDropdownSourceSelect /> |                   <ObjectFilterDropdownSourceSelect /> | ||||||
|   | |||||||
| @@ -1,16 +1,27 @@ | |||||||
| import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||||
| import { useEffect, useState } from 'react'; | import { useState } from 'react'; | ||||||
|  |  | ||||||
| import { ObjectFilterSelectMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectMenu'; | import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||||
| import { ObjectFilterSelectSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu'; |  | ||||||
|  | import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu'; | ||||||
| import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; | import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; | ||||||
|  | import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | ||||||
| import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; | import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; | ||||||
| import { currentSubMenuState } from '@/object-record/object-filter-dropdown/states/subMenuStates'; | import { CompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/types/CompositeFilterableFieldType'; | ||||||
|  | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
|  | import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; | ||||||
|  | import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; | ||||||
|  | import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; | ||||||
|  | import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; | ||||||
|  | import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; | ||||||
| import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; | import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; | ||||||
|  | import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; | ||||||
|  | import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; | ||||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||||
| import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; | import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; | ||||||
| import { useRecoilState } from 'recoil'; | import { useRecoilValue } from 'recoil'; | ||||||
| import { isDefined } from 'twenty-ui'; | import { isDefined, useIcons } from 'twenty-ui'; | ||||||
|  | import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; | ||||||
|  |  | ||||||
| export const StyledInput = styled.input` | export const StyledInput = styled.input` | ||||||
|   background: transparent; |   background: transparent; | ||||||
| @@ -39,19 +50,33 @@ export const StyledInput = styled.input` | |||||||
| `; | `; | ||||||
|  |  | ||||||
| export const ObjectFilterDropdownFilterSelect = () => { | export const ObjectFilterDropdownFilterSelect = () => { | ||||||
|   const [searchText, setSearchText] = useState(''); |   const [subMenuFieldType, setSubMenuFieldType] = | ||||||
|  |     useState<CompositeFilterableFieldType | null>(null); | ||||||
|  |  | ||||||
|  |   const [firstLevelFilterDefinition, setFirstLevelFilterDefinition] = | ||||||
|  |     useState<FilterDefinition | null>(null); | ||||||
|  |  | ||||||
|  |   const { | ||||||
|  |     setFilterDefinitionUsedInDropdown, | ||||||
|  |     setSelectedOperandInDropdown, | ||||||
|  |     setObjectFilterDropdownSearchInput, | ||||||
|  |     objectFilterDropdownSearchInputState, | ||||||
|  |   } = useFilterDropdown(); | ||||||
|  |  | ||||||
|  |   const objectFilterDropdownSearchInput = useRecoilValue( | ||||||
|  |     objectFilterDropdownSearchInputState, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   const availableFilterDefinitions = useRecoilComponentValueV2( |   const availableFilterDefinitions = useRecoilComponentValueV2( | ||||||
|     availableFilterDefinitionsComponentState, |     availableFilterDefinitionsComponentState, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const [currentSubMenu, setCurrentSubMenu] = |  | ||||||
|     useRecoilState(currentSubMenuState); |  | ||||||
|  |  | ||||||
|   const sortedAvailableFilterDefinitions = [...availableFilterDefinitions] |   const sortedAvailableFilterDefinitions = [...availableFilterDefinitions] | ||||||
|     .sort((a, b) => a.label.localeCompare(b.label)) |     .sort((a, b) => a.label.localeCompare(b.label)) | ||||||
|     .filter((item) => |     .filter((item) => | ||||||
|       item.label.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()), |       item.label | ||||||
|  |         .toLocaleLowerCase() | ||||||
|  |         .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   const selectableListItemIds = sortedAvailableFilterDefinitions.map( |   const selectableListItemIds = sortedAvailableFilterDefinitions.map( | ||||||
| @@ -76,21 +101,96 @@ export const ObjectFilterDropdownFilterSelect = () => { | |||||||
|     selectFilter({ filterDefinition: selectedFilterDefinition }); |     selectFilter({ filterDefinition: selectedFilterDefinition }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   useEffect(() => { |   const setHotkeyScope = useSetHotkeyScope(); | ||||||
|     return () => { |   const { getIcon } = useIcons(); | ||||||
|       setCurrentSubMenu(null); |  | ||||||
|     }; |  | ||||||
|   }, [setCurrentSubMenu]); |  | ||||||
|  |  | ||||||
|   return !currentSubMenu ? ( |   const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => { | ||||||
|     <ObjectFilterSelectMenu |     setFilterDefinitionUsedInDropdown(availableFilterDefinition); | ||||||
|       searchText={searchText} |  | ||||||
|       setSearchText={setSearchText} |     if ( | ||||||
|       sortedAvailableFilterDefinitions={sortedAvailableFilterDefinitions} |       availableFilterDefinition.type === 'RELATION' || | ||||||
|       selectableListItemIds={selectableListItemIds} |       availableFilterDefinition.type === 'SELECT' | ||||||
|       handleEnter={handleEnter} |     ) { | ||||||
|  |       setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     setSelectedOperandInDropdown( | ||||||
|  |       getOperandsForFilterDefinition(availableFilterDefinition)[0], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     setObjectFilterDropdownSearchInput(''); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleSubMenuBack = () => { | ||||||
|  |     setSubMenuFieldType(null); | ||||||
|  |     setFirstLevelFilterDefinition(null); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const shouldShowFirstLevelMenu = !isDefined(subMenuFieldType); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       {shouldShowFirstLevelMenu ? ( | ||||||
|  |         <> | ||||||
|  |           <StyledInput | ||||||
|  |             value={objectFilterDropdownSearchInput} | ||||||
|  |             autoFocus | ||||||
|  |             placeholder="Search fields" | ||||||
|  |             onChange={(event: React.ChangeEvent<HTMLInputElement>) => | ||||||
|  |               setObjectFilterDropdownSearchInput(event.target.value) | ||||||
|  |             } | ||||||
|           /> |           /> | ||||||
|  |           <SelectableList | ||||||
|  |             hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton} | ||||||
|  |             selectableItemIdArray={selectableListItemIds} | ||||||
|  |             selectableListId={OBJECT_FILTER_DROPDOWN_ID} | ||||||
|  |             onEnter={handleEnter} | ||||||
|  |           > | ||||||
|  |             <DropdownMenuItemsContainer> | ||||||
|  |               {[...availableFilterDefinitions] | ||||||
|  |                 .sort((a, b) => a.label.localeCompare(b.label)) | ||||||
|  |                 .filter((item) => | ||||||
|  |                   item.label | ||||||
|  |                     .toLocaleLowerCase() | ||||||
|  |                     .includes( | ||||||
|  |                       objectFilterDropdownSearchInput.toLocaleLowerCase(), | ||||||
|  |                     ), | ||||||
|  |                 ) | ||||||
|  |                 .map((availableFilterDefinition, index) => ( | ||||||
|  |                   <SelectableItem | ||||||
|  |                     itemId={availableFilterDefinition.fieldMetadataId} | ||||||
|  |                   > | ||||||
|  |                     <MenuItem | ||||||
|  |                       key={`select-filter-${index}`} | ||||||
|  |                       testId={`select-filter-${index}`} | ||||||
|  |                       onClick={() => { | ||||||
|  |                         if (isCompositeField(availableFilterDefinition.type)) { | ||||||
|  |                           setSubMenuFieldType(availableFilterDefinition.type); | ||||||
|  |                           setFirstLevelFilterDefinition( | ||||||
|  |                             availableFilterDefinition, | ||||||
|  |                           ); | ||||||
|  |                         } else { | ||||||
|  |                           handleSelectFilter(availableFilterDefinition); | ||||||
|  |                         } | ||||||
|  |                       }} | ||||||
|  |                       LeftIcon={getIcon(availableFilterDefinition.iconName)} | ||||||
|  |                       text={availableFilterDefinition.label} | ||||||
|  |                       hasSubMenu={isCompositeField( | ||||||
|  |                         availableFilterDefinition.type, | ||||||
|  |                       )} | ||||||
|  |                     /> | ||||||
|  |                   </SelectableItem> | ||||||
|  |                 ))} | ||||||
|  |             </DropdownMenuItemsContainer> | ||||||
|  |           </SelectableList> | ||||||
|  |         </> | ||||||
|       ) : ( |       ) : ( | ||||||
|     <ObjectFilterSelectSubMenu /> |         <ObjectFilterDropdownFilterSelectCompositeFieldSubMenu | ||||||
|  |           fieldType={subMenuFieldType} | ||||||
|  |           firstLevelFieldDefinition={firstLevelFilterDefinition} | ||||||
|  |           onBack={handleSubMenuBack} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|  |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -0,0 +1,98 @@ | |||||||
|  | import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; | ||||||
|  | import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | ||||||
|  | import { CompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/types/CompositeFilterableFieldType'; | ||||||
|  | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
|  | import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; | ||||||
|  | import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel'; | ||||||
|  | import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; | ||||||
|  | import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; | ||||||
|  | import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||||
|  | import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||||
|  | import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; | ||||||
|  | import { useState } from 'react'; | ||||||
|  | import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui'; | ||||||
|  |  | ||||||
|  | type ObjectFilterDropdownFilterSelectCompositeFieldSubMenuProps = { | ||||||
|  |   fieldType: CompositeFilterableFieldType; | ||||||
|  |   firstLevelFieldDefinition: FilterDefinition | null; | ||||||
|  |   onBack: () => void; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = ({ | ||||||
|  |   fieldType, | ||||||
|  |   firstLevelFieldDefinition, | ||||||
|  |   onBack, | ||||||
|  | }: ObjectFilterDropdownFilterSelectCompositeFieldSubMenuProps) => { | ||||||
|  |   const [searchText, setSearchText] = useState(''); | ||||||
|  |  | ||||||
|  |   const { getIcon } = useIcons(); | ||||||
|  |  | ||||||
|  |   const { | ||||||
|  |     setFilterDefinitionUsedInDropdown, | ||||||
|  |     setSelectedOperandInDropdown, | ||||||
|  |     setObjectFilterDropdownSearchInput, | ||||||
|  |   } = useFilterDropdown(); | ||||||
|  |  | ||||||
|  |   const handleSelectFilter = (definition: FilterDefinition | null) => { | ||||||
|  |     if (definition !== null) { | ||||||
|  |       setFilterDefinitionUsedInDropdown(definition); | ||||||
|  |  | ||||||
|  |       setSelectedOperandInDropdown( | ||||||
|  |         getOperandsForFilterDefinition(definition)[0], | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       setObjectFilterDropdownSearchInput(''); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const options = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ | ||||||
|  |     fieldType | ||||||
|  |   ].filterableSubFields | ||||||
|  |     .sort((a, b) => a.localeCompare(b)) | ||||||
|  |     .filter((item) => | ||||||
|  |       item.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={onBack}> | ||||||
|  |         {getFilterableFieldTypeLabel(fieldType)} | ||||||
|  |       </DropdownMenuHeader> | ||||||
|  |       <StyledInput | ||||||
|  |         value={searchText} | ||||||
|  |         autoFocus | ||||||
|  |         placeholder="Search fields" | ||||||
|  |         onChange={(event: React.ChangeEvent<HTMLInputElement>) => | ||||||
|  |           setSearchText(event.target.value) | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|  |       <DropdownMenuItemsContainer> | ||||||
|  |         <MenuItem | ||||||
|  |           key={`select-filter-${-1}`} | ||||||
|  |           testId={`select-filter-${-1}`} | ||||||
|  |           onClick={() => { | ||||||
|  |             handleSelectFilter(firstLevelFieldDefinition); | ||||||
|  |           }} | ||||||
|  |           LeftIcon={IconApps} | ||||||
|  |           text={`Any ${getFilterableFieldTypeLabel(fieldType)} field`} | ||||||
|  |         /> | ||||||
|  |         {options.map((subFieldName, index) => ( | ||||||
|  |           <MenuItem | ||||||
|  |             key={`select-filter-${index}`} | ||||||
|  |             testId={`select-filter-${index}`} | ||||||
|  |             onClick={() => | ||||||
|  |               firstLevelFieldDefinition && | ||||||
|  |               handleSelectFilter({ | ||||||
|  |                 ...firstLevelFieldDefinition, | ||||||
|  |                 label: getCompositeSubFieldLabel(fieldType, subFieldName), | ||||||
|  |                 compositeFieldName: subFieldName, | ||||||
|  |               }) | ||||||
|  |             } | ||||||
|  |             text={getCompositeSubFieldLabel(fieldType, subFieldName)} | ||||||
|  |             LeftIcon={getIcon(firstLevelFieldDefinition?.iconName)} | ||||||
|  |           /> | ||||||
|  |         ))} | ||||||
|  |       </DropdownMenuItemsContainer> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -1,14 +1,10 @@ | |||||||
| import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; | import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; | ||||||
| import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; | import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; | ||||||
| import { |  | ||||||
|   currentParentFilterDefinitionState, |  | ||||||
|   currentSubMenuState, |  | ||||||
| } from '@/object-record/object-filter-dropdown/states/subMenuStates'; |  | ||||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
| import { hasSubMenuFilter } from '@/object-record/object-filter-dropdown/utils/hasSubMenuFilter'; |  | ||||||
| import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; | import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; | ||||||
| import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; | import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; | ||||||
| import { useRecoilValue, useSetRecoilState } from 'recoil'; | import { useRecoilValue } from 'recoil'; | ||||||
| import { useIcons } from 'twenty-ui'; | import { useIcons } from 'twenty-ui'; | ||||||
|  |  | ||||||
| export type ObjectFilterDropdownFilterSelectMenuItemProps = { | export type ObjectFilterDropdownFilterSelectMenuItemProps = { | ||||||
| @@ -28,24 +24,12 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ | |||||||
|     isSelectedItemIdSelector(filterDefinition.fieldMetadataId), |     isSelectedItemIdSelector(filterDefinition.fieldMetadataId), | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   const hasSubMenu = hasSubMenuFilter(filterDefinition.type); |  | ||||||
|  |  | ||||||
|   const { getIcon } = useIcons(); |   const { getIcon } = useIcons(); | ||||||
|  |  | ||||||
|   const setCurrentSubMenu = useSetRecoilState(currentSubMenuState); |  | ||||||
|   const setCurrentParentFilterDefinition = useSetRecoilState( |  | ||||||
|     currentParentFilterDefinitionState, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   const handleClick = () => { |   const handleClick = () => { | ||||||
|     resetSelectedItem(); |     resetSelectedItem(); | ||||||
|  |  | ||||||
|     if (hasSubMenu) { |  | ||||||
|       setCurrentSubMenu(filterDefinition.type); |  | ||||||
|       setCurrentParentFilterDefinition(filterDefinition); |  | ||||||
|     } else { |  | ||||||
|     selectFilter({ filterDefinition }); |     selectFilter({ filterDefinition }); | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
| @@ -55,7 +39,6 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ | |||||||
|       onClick={handleClick} |       onClick={handleClick} | ||||||
|       LeftIcon={getIcon(filterDefinition.iconName)} |       LeftIcon={getIcon(filterDefinition.iconName)} | ||||||
|       text={filterDefinition.label} |       text={filterDefinition.label} | ||||||
|       hasSubMenu={hasSubMenu} |  | ||||||
|     /> |     /> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import { isDefined } from '~/utils/isDefined'; | |||||||
|  |  | ||||||
| import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; | import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; | ||||||
| import { getOperandLabel } from '../utils/getOperandLabel'; | import { getOperandLabel } from '../utils/getOperandLabel'; | ||||||
| import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; | import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; | ||||||
|  |  | ||||||
| export const ObjectFilterDropdownOperandSelect = () => { | export const ObjectFilterDropdownOperandSelect = () => { | ||||||
|   const { |   const { | ||||||
| @@ -31,9 +31,9 @@ export const ObjectFilterDropdownOperandSelect = () => { | |||||||
|  |  | ||||||
|   const selectedFilter = useRecoilValue(selectedFilterState); |   const selectedFilter = useRecoilValue(selectedFilterState); | ||||||
|  |  | ||||||
|   const operandsForFilterType = getOperandsForFilterType( |   const operandsForFilterType = isDefined(filterDefinitionUsedInDropdown) | ||||||
|     filterDefinitionUsedInDropdown?.type, |     ? getOperandsForFilterDefinition(filterDefinitionUsedInDropdown) | ||||||
|   ); |     : []; | ||||||
|  |  | ||||||
|   const handleOperandChange = (newOperand: ViewFilterOperand) => { |   const handleOperandChange = (newOperand: ViewFilterOperand) => { | ||||||
|     const isValuelessOperand = [ |     const isValuelessOperand = [ | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil'; | |||||||
| import { v4 } from 'uuid'; | import { v4 } from 'uuid'; | ||||||
|  |  | ||||||
| import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | ||||||
| import { getSourceEnumOptions } from '@/object-record/object-filter-dropdown/utils/getSourceEnumOptions'; | import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions'; | ||||||
| import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; | import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; | ||||||
| import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; | import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; | ||||||
| import { SelectableItem } from '@/object-record/select/types/SelectableItem'; | import { SelectableItem } from '@/object-record/select/types/SelectableItem'; | ||||||
| @@ -55,7 +55,7 @@ export const ObjectFilterDropdownSourceSelect = ({ | |||||||
|  |  | ||||||
|   const selectedFilter = useRecoilValue(selectedFilterState); |   const selectedFilter = useRecoilValue(selectedFilterState); | ||||||
|  |  | ||||||
|   const sourceTypes = getSourceEnumOptions( |   const sourceTypes = getActorSourceMultiSelectOptions( | ||||||
|     objectFilterDropdownSelectedRecordIds, |     objectFilterDropdownSelectedRecordIds, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,87 +0,0 @@ | |||||||
| import styled from '@emotion/styled'; |  | ||||||
|  |  | ||||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; |  | ||||||
|  |  | ||||||
| import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; |  | ||||||
| import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; |  | ||||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; |  | ||||||
| import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; |  | ||||||
| import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; |  | ||||||
| import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; |  | ||||||
|  |  | ||||||
| export const StyledInput = styled.input` |  | ||||||
|   background: transparent; |  | ||||||
|   border: none; |  | ||||||
|   border-top: none; |  | ||||||
|   border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; |  | ||||||
|   border-radius: 0; |  | ||||||
|   border-top-left-radius: ${({ theme }) => theme.border.radius.md}; |  | ||||||
|   border-top-right-radius: ${({ theme }) => theme.border.radius.md}; |  | ||||||
|   color: ${({ theme }) => theme.font.color.primary}; |  | ||||||
|   margin: 0; |  | ||||||
|   outline: none; |  | ||||||
|   padding: ${({ theme }) => theme.spacing(2)}; |  | ||||||
|   height: 19px; |  | ||||||
|   font-family: inherit; |  | ||||||
|   font-size: ${({ theme }) => theme.font.size.sm}; |  | ||||||
|  |  | ||||||
|   font-weight: inherit; |  | ||||||
|   max-width: 100%; |  | ||||||
|   overflow: hidden; |  | ||||||
|   text-decoration: none; |  | ||||||
|  |  | ||||||
|   &::placeholder { |  | ||||||
|     color: ${({ theme }) => theme.font.color.light}; |  | ||||||
|   } |  | ||||||
| `; |  | ||||||
|  |  | ||||||
| type ObjectFilterSelectMenuProps = { |  | ||||||
|   searchText: string; |  | ||||||
|   setSearchText: (searchText: string) => void; |  | ||||||
|   sortedAvailableFilterDefinitions: FilterDefinition[]; |  | ||||||
|   selectableListItemIds: string[]; |  | ||||||
|   handleEnter: (itemId: string) => void; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const ObjectFilterSelectMenu = ({ |  | ||||||
|   searchText, |  | ||||||
|   setSearchText, |  | ||||||
|   sortedAvailableFilterDefinitions, |  | ||||||
|   selectableListItemIds, |  | ||||||
|   handleEnter, |  | ||||||
| }: ObjectFilterSelectMenuProps) => { |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <StyledInput |  | ||||||
|         value={searchText} |  | ||||||
|         autoFocus |  | ||||||
|         placeholder="Search fields" |  | ||||||
|         onChange={(event: React.ChangeEvent<HTMLInputElement>) => |  | ||||||
|           setSearchText(event.target.value) |  | ||||||
|         } |  | ||||||
|       /> |  | ||||||
|       <SelectableList |  | ||||||
|         hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton} |  | ||||||
|         selectableItemIdArray={selectableListItemIds} |  | ||||||
|         selectableListId={OBJECT_FILTER_DROPDOWN_ID} |  | ||||||
|         onEnter={handleEnter} |  | ||||||
|       > |  | ||||||
|         <DropdownMenuItemsContainer> |  | ||||||
|           {sortedAvailableFilterDefinitions.map( |  | ||||||
|             (availableFilterDefinition: FilterDefinition, index: number) => ( |  | ||||||
|               <SelectableItem |  | ||||||
|                 key={`selectable-item-${availableFilterDefinition.fieldMetadataId}`} |  | ||||||
|                 itemId={availableFilterDefinition.fieldMetadataId} |  | ||||||
|               > |  | ||||||
|                 <ObjectFilterDropdownFilterSelectMenuItem |  | ||||||
|                   key={`select-filter-${index}`} |  | ||||||
|                   filterDefinition={availableFilterDefinition} |  | ||||||
|                 /> |  | ||||||
|               </SelectableItem> |  | ||||||
|             ), |  | ||||||
|           )} |  | ||||||
|         </DropdownMenuItemsContainer> |  | ||||||
|       </SelectableList> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,102 +0,0 @@ | |||||||
| import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; |  | ||||||
| import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; |  | ||||||
| import { |  | ||||||
|   currentParentFilterDefinitionState, |  | ||||||
|   currentSubMenuState, |  | ||||||
| } from '@/object-record/object-filter-dropdown/states/subMenuStates'; |  | ||||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; |  | ||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; |  | ||||||
| import { getHeaderTitle } from '@/object-record/object-filter-dropdown/utils/getHeaderTitle'; |  | ||||||
| import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; |  | ||||||
| import { getSubMenuOptions } from '@/object-record/object-filter-dropdown/utils/getSubMenuOptions'; |  | ||||||
| import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; |  | ||||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; |  | ||||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; |  | ||||||
| import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; |  | ||||||
| import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; |  | ||||||
| import { useState } from 'react'; |  | ||||||
| import { useRecoilState, useRecoilValue } from 'recoil'; |  | ||||||
| import { IconChevronLeft, useIcons } from 'twenty-ui'; |  | ||||||
|  |  | ||||||
| export const ObjectFilterSelectSubMenu = () => { |  | ||||||
|   const [searchText, setSearchText] = useState(''); |  | ||||||
|   const { getIcon } = useIcons(); |  | ||||||
|  |  | ||||||
|   const [currentSubMenu, setCurrentSubMenu] = |  | ||||||
|     useRecoilState(currentSubMenuState); |  | ||||||
|  |  | ||||||
|   const currentParentFilterDefinition = useRecoilValue( |  | ||||||
|     currentParentFilterDefinitionState, |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   const { |  | ||||||
|     setFilterDefinitionUsedInDropdown, |  | ||||||
|     setSelectedOperandInDropdown, |  | ||||||
|     setObjectFilterDropdownSearchInput, |  | ||||||
|   } = useFilterDropdown(); |  | ||||||
|  |  | ||||||
|   const setHotkeyScope = useSetHotkeyScope(); |  | ||||||
|  |  | ||||||
|   const handleSelectFilter = (definition: FilterDefinition | null) => { |  | ||||||
|     if (definition !== null) { |  | ||||||
|       setFilterDefinitionUsedInDropdown(definition); |  | ||||||
|       if (definition.type === 'SOURCE') { |  | ||||||
|         setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       setSelectedOperandInDropdown( |  | ||||||
|         getOperandsForFilterType(definition.type)?.[0], |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       setObjectFilterDropdownSearchInput(''); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <DropdownMenuHeader |  | ||||||
|         StartIcon={IconChevronLeft} |  | ||||||
|         onClick={() => { |  | ||||||
|           setCurrentSubMenu(null); |  | ||||||
|         }} |  | ||||||
|       > |  | ||||||
|         {getHeaderTitle(currentSubMenu)} |  | ||||||
|       </DropdownMenuHeader> |  | ||||||
|       <StyledInput |  | ||||||
|         value={searchText} |  | ||||||
|         autoFocus |  | ||||||
|         placeholder="Search fields" |  | ||||||
|         onChange={(event: React.ChangeEvent<HTMLInputElement>) => |  | ||||||
|           setSearchText(event.target.value) |  | ||||||
|         } |  | ||||||
|       /> |  | ||||||
|       <DropdownMenuItemsContainer> |  | ||||||
|         {getSubMenuOptions(currentSubMenu) |  | ||||||
|           .sort((a, b) => a.name.localeCompare(b.name)) |  | ||||||
|           .filter((item) => |  | ||||||
|             item.name |  | ||||||
|               .toLocaleLowerCase() |  | ||||||
|               .includes(searchText.toLocaleLowerCase()), |  | ||||||
|           ) |  | ||||||
|           .map((menuOption, index) => ( |  | ||||||
|             <MenuItem |  | ||||||
|               key={`select-filter-${index}`} |  | ||||||
|               testId={`select-filter-${index}`} |  | ||||||
|               onClick={() => { |  | ||||||
|                 currentParentFilterDefinition && |  | ||||||
|                   handleSelectFilter({ |  | ||||||
|                     ...currentParentFilterDefinition, |  | ||||||
|                     label: menuOption.name, |  | ||||||
|                     type: menuOption.type as FilterType, |  | ||||||
|                   }); |  | ||||||
|               }} |  | ||||||
|               text={menuOption.name} |  | ||||||
|               LeftIcon={getIcon( |  | ||||||
|                 menuOption.icon || currentParentFilterDefinition?.iconName, |  | ||||||
|               )} |  | ||||||
|             /> |  | ||||||
|           ))} |  | ||||||
|       </DropdownMenuItemsContainer> |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -13,7 +13,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | |||||||
|  |  | ||||||
| import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; | ||||||
| import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; | import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; | ||||||
| import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; | import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; | ||||||
| import { GenericEntityFilterChip } from './GenericEntityFilterChip'; | import { GenericEntityFilterChip } from './GenericEntityFilterChip'; | ||||||
| import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; | import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; | ||||||
| import { ObjectFilterDropdownSearchInput } from './ObjectFilterDropdownSearchInput'; | import { ObjectFilterDropdownSearchInput } from './ObjectFilterDropdownSearchInput'; | ||||||
| @@ -36,14 +36,16 @@ export const SingleEntityObjectFilterDropdownButton = ({ | |||||||
|   ); |   ); | ||||||
|   const selectedFilter = useRecoilValue(selectedFilterState); |   const selectedFilter = useRecoilValue(selectedFilterState); | ||||||
|  |  | ||||||
|   const availableFilter = availableFilterDefinitions[0]; |   const availableFilterDefinition = availableFilterDefinitions[0]; | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     setFilterDefinitionUsedInDropdown(availableFilter); |     setFilterDefinitionUsedInDropdown(availableFilterDefinition); | ||||||
|     const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0]; |     const defaultOperand = getOperandsForFilterDefinition( | ||||||
|  |       availableFilterDefinition, | ||||||
|  |     )[0]; | ||||||
|     setSelectedOperandInDropdown(defaultOperand); |     setSelectedOperandInDropdown(defaultOperand); | ||||||
|   }, [ |   }, [ | ||||||
|     availableFilter, |     availableFilterDefinition, | ||||||
|     setFilterDefinitionUsedInDropdown, |     setFilterDefinitionUsedInDropdown, | ||||||
|     setSelectedOperandInDropdown, |     setSelectedOperandInDropdown, | ||||||
|   ]); |   ]); | ||||||
| @@ -62,7 +64,7 @@ export const SingleEntityObjectFilterDropdownButton = ({ | |||||||
|               filter={selectedFilter} |               filter={selectedFilter} | ||||||
|               Icon={ |               Icon={ | ||||||
|                 selectedFilter.operand === ViewFilterOperand.IsNotNull |                 selectedFilter.operand === ViewFilterOperand.IsNotNull | ||||||
|                   ? availableFilter.SelectAllIcon |                   ? availableFilterDefinition.SelectAllIcon | ||||||
|                   : undefined |                   : undefined | ||||||
|               } |               } | ||||||
|             /> |             /> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | ||||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
| import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; | import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; | ||||||
| import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; | import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; | ||||||
| import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; | import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; | ||||||
| import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; | import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; | ||||||
| import { v4 } from 'uuid'; | import { v4 } from 'uuid'; | ||||||
| @@ -31,12 +31,12 @@ export const useSelectFilter = () => { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     setSelectedOperandInDropdown( |     setSelectedOperandInDropdown( | ||||||
|       getOperandsForFilterType(filterDefinition.type)?.[0], |       getOperandsForFilterDefinition(filterDefinition)[0], | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const { value, displayValue } = getInitialFilterValue( |     const { value, displayValue } = getInitialFilterValue( | ||||||
|       filterDefinition.type, |       filterDefinition.type, | ||||||
|       getOperandsForFilterType(filterDefinition.type)?.[0], |       getOperandsForFilterDefinition(filterDefinition)[0], | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (value !== '') { |     if (value !== '') { | ||||||
| @@ -44,7 +44,7 @@ export const useSelectFilter = () => { | |||||||
|         id: v4(), |         id: v4(), | ||||||
|         fieldMetadataId: filterDefinition.fieldMetadataId, |         fieldMetadataId: filterDefinition.fieldMetadataId, | ||||||
|         displayValue, |         displayValue, | ||||||
|         operand: getOperandsForFilterType(filterDefinition.type)?.[0], |         operand: getOperandsForFilterDefinition(filterDefinition)[0], | ||||||
|         value, |         value, | ||||||
|         definition: filterDefinition, |         definition: filterDefinition, | ||||||
|       }); |       }); | ||||||
|   | |||||||
| @@ -1,15 +0,0 @@ | |||||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; |  | ||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; |  | ||||||
| import { atom } from 'recoil'; |  | ||||||
|  |  | ||||||
| export const currentSubMenuState = atom<FilterType | null>({ |  | ||||||
|   key: 'currentSubMenuState', |  | ||||||
|   default: null, |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| export const currentParentFilterDefinitionState = atom<FilterDefinition | null>( |  | ||||||
|   { |  | ||||||
|     key: 'currentParentFilterDefinitionState', |  | ||||||
|     default: null, |  | ||||||
|   }, |  | ||||||
| ); |  | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; | ||||||
|  | import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; | ||||||
|  |  | ||||||
|  | export type CompositeFilterableFieldType = FilterableFieldType & | ||||||
|  |   CompositeFieldType; | ||||||
| @@ -1,15 +1,15 @@ | |||||||
| import { IconComponent } from 'twenty-ui'; | import { IconComponent } from 'twenty-ui'; | ||||||
|  |  | ||||||
| import { FilterType } from './FilterType'; | import { FilterableFieldType } from './FilterableFieldType'; | ||||||
|  |  | ||||||
| export type FilterDefinition = { | export type FilterDefinition = { | ||||||
|   fieldMetadataId: string; |   fieldMetadataId: string; | ||||||
|   label: string; |   label: string; | ||||||
|   iconName: string; |   iconName: string; | ||||||
|   type: FilterType; |   type: FilterableFieldType; | ||||||
|   relationObjectMetadataNamePlural?: string; |   relationObjectMetadataNamePlural?: string; | ||||||
|   relationObjectMetadataNameSingular?: string; |   relationObjectMetadataNameSingular?: string; | ||||||
|   selectAllLabel?: string; |   selectAllLabel?: string; | ||||||
|   SelectAllIcon?: IconComponent; |   SelectAllIcon?: IconComponent; | ||||||
|   subFieldType?: FilterType; |   compositeFieldName?: string; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
| export type FilterType = | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
|  | import { PickLiteral } from '~/types/PickLiteral'; | ||||||
|  | 
 | ||||||
|  | export type FilterableFieldType = PickLiteral< | ||||||
|  |   FieldType, | ||||||
|   | 'TEXT' |   | 'TEXT' | ||||||
|   | 'PHONE' |   | 'PHONE' | ||||||
|   | 'PHONES' |   | 'PHONES' | ||||||
| @@ -18,4 +22,4 @@ export type FilterType = | |||||||
|   | 'MULTI_SELECT' |   | 'MULTI_SELECT' | ||||||
|   | 'ACTOR' |   | 'ACTOR' | ||||||
|   | 'ARRAY' |   | 'ARRAY' | ||||||
|   | 'SOURCE'; | >; | ||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; |  | ||||||
| import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | ||||||
|  |  | ||||||
| import { getOperandsForFilterType } from '../getOperandsForFilterType'; | import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; | ||||||
|  | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
|  | import { getOperandsForFilterDefinition } from '../getOperandsForFilterType'; | ||||||
|  |  | ||||||
| describe('getOperandsForFilterType', () => { | describe('getOperandsForFilterType', () => { | ||||||
|   const emptyOperands = [ |   const emptyOperands = [ | ||||||
| @@ -51,7 +52,9 @@ describe('getOperandsForFilterType', () => { | |||||||
|  |  | ||||||
|   testCases.forEach(([filterType, expectedOperands]) => { |   testCases.forEach(([filterType, expectedOperands]) => { | ||||||
|     it(`should return correct operands for FilterType.${filterType}`, () => { |     it(`should return correct operands for FilterType.${filterType}`, () => { | ||||||
|       const result = getOperandsForFilterType(filterType as FilterType); |       const result = getOperandsForFilterDefinition({ | ||||||
|  |         type: filterType as FilterableFieldType, | ||||||
|  |       } as FilterDefinition); | ||||||
|       expect(result).toEqual(expectedOperands); |       expect(result).toEqual(expectedOperands); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -9,54 +9,54 @@ import { | |||||||
|   IconUserCircle, |   IconUserCircle, | ||||||
| } from 'twenty-ui'; | } from 'twenty-ui'; | ||||||
| 
 | 
 | ||||||
| export const getSourceEnumOptions = ( | export const getActorSourceMultiSelectOptions = ( | ||||||
|   selectedItemIds: string[], |   selectedSourceNames: string[], | ||||||
| ): SelectableItem[] => { | ): SelectableItem[] => { | ||||||
|   return [ |   return [ | ||||||
|     { |     { | ||||||
|       id: 'MANUAL', |       id: 'MANUAL', | ||||||
|       name: 'User', |       name: 'User', | ||||||
|       isSelected: selectedItemIds.includes('MANUAL'), |       isSelected: selectedSourceNames.includes('MANUAL'), | ||||||
|       AvatarIcon: IconUserCircle, |       AvatarIcon: IconUserCircle, | ||||||
|       isIconInverted: true, |       isIconInverted: true, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 'IMPORT', |       id: 'IMPORT', | ||||||
|       name: 'Import', |       name: 'Import', | ||||||
|       isSelected: selectedItemIds.includes('IMPORT'), |       isSelected: selectedSourceNames.includes('IMPORT'), | ||||||
|       AvatarIcon: IconCsv, |       AvatarIcon: IconCsv, | ||||||
|       isIconInverted: true, |       isIconInverted: true, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 'API', |       id: 'API', | ||||||
|       name: 'Api', |       name: 'Api', | ||||||
|       isSelected: selectedItemIds.includes('API'), |       isSelected: selectedSourceNames.includes('API'), | ||||||
|       AvatarIcon: IconApi, |       AvatarIcon: IconApi, | ||||||
|       isIconInverted: true, |       isIconInverted: true, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 'EMAIL', |       id: 'EMAIL', | ||||||
|       name: 'Email', |       name: 'Email', | ||||||
|       isSelected: selectedItemIds.includes('EMAIL'), |       isSelected: selectedSourceNames.includes('EMAIL'), | ||||||
|       AvatarIcon: IconGmail, |       AvatarIcon: IconGmail, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 'CALENDAR', |       id: 'CALENDAR', | ||||||
|       name: 'Calendar', |       name: 'Calendar', | ||||||
|       isSelected: selectedItemIds.includes('CALENDAR'), |       isSelected: selectedSourceNames.includes('CALENDAR'), | ||||||
|       AvatarIcon: IconGoogleCalendar, |       AvatarIcon: IconGoogleCalendar, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 'WORKFLOW', |       id: 'WORKFLOW', | ||||||
|       name: 'Workflow', |       name: 'Workflow', | ||||||
|       isSelected: selectedItemIds.includes('WORKFLOW'), |       isSelected: selectedSourceNames.includes('WORKFLOW'), | ||||||
|       AvatarIcon: IconSettingsAutomation, |       AvatarIcon: IconSettingsAutomation, | ||||||
|       isIconInverted: true, |       isIconInverted: true, | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       id: 'SYSTEM', |       id: 'SYSTEM', | ||||||
|       name: 'System', |       name: 'System', | ||||||
|       isSelected: selectedItemIds.includes('SYSTEM'), |       isSelected: selectedSourceNames.includes('SYSTEM'), | ||||||
|       AvatarIcon: IconRobot, |       AvatarIcon: IconRobot, | ||||||
|       isIconInverted: true, |       isIconInverted: true, | ||||||
|     }, |     }, | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; | ||||||
|  | import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; | ||||||
|  |  | ||||||
|  | export const getCompositeSubFieldLabel = ( | ||||||
|  |   compositeFieldType: CompositeFieldType, | ||||||
|  |   subFieldName: (typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS)[CompositeFieldType]['subFields'][number], | ||||||
|  | ): string => { | ||||||
|  |   return ( | ||||||
|  |     SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType] | ||||||
|  |       .labelBySubField as any | ||||||
|  |   )[subFieldName]; | ||||||
|  | }; | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; | ||||||
|  | import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | ||||||
|  |  | ||||||
|  | export const getFilterableFieldTypeLabel = ( | ||||||
|  |   filterableFieldType: FilterableFieldType, | ||||||
|  | ) => { | ||||||
|  |   return SETTINGS_FIELD_TYPE_CONFIGS[filterableFieldType].label; | ||||||
|  | }; | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; |  | ||||||
|  |  | ||||||
| export const getHeaderTitle = ( |  | ||||||
|   subMenu: FilterType | null, |  | ||||||
| ): string | undefined => { |  | ||||||
|   switch (subMenu) { |  | ||||||
|     case 'ACTOR': |  | ||||||
|       return 'Actor'; |  | ||||||
|     case 'SOURCE': |  | ||||||
|       return 'Creation Source'; |  | ||||||
|     default: |  | ||||||
|       return undefined; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | ||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; | import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; | ||||||
| import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | ||||||
| import { z } from 'zod'; | import { z } from 'zod'; | ||||||
|  |  | ||||||
| export const getInitialFilterValue = ( | export const getInitialFilterValue = ( | ||||||
|   newType: FilterType, |   newType: FilterableFieldType, | ||||||
|   newOperand: ViewFilterOperand, |   newOperand: ViewFilterOperand, | ||||||
|   oldValue?: string, |   oldValue?: string, | ||||||
|   oldDisplayValue?: string, |   oldDisplayValue?: string, | ||||||
| @@ -35,6 +35,7 @@ export const getInitialFilterValue = ( | |||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     value: oldValue ?? '', |     value: oldValue ?? '', | ||||||
|     displayValue: oldDisplayValue ?? '', |     displayValue: oldDisplayValue ?? '', | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
|  | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
|  | import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; | ||||||
| import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | ||||||
|  |  | ||||||
| import { FilterType } from '../types/FilterType'; | export const getOperandsForFilterDefinition = ( | ||||||
|  |   filterDefinition: FilterDefinition, | ||||||
| export const getOperandsForFilterType = ( |  | ||||||
|   filterType: FilterType | null | undefined, |  | ||||||
| ): ViewFilterOperand[] => { | ): ViewFilterOperand[] => { | ||||||
|   const emptyOperands = [ |   const emptyOperands = [ | ||||||
|     ViewFilterOperand.IsEmpty, |     ViewFilterOperand.IsEmpty, | ||||||
| @@ -12,7 +12,7 @@ export const getOperandsForFilterType = ( | |||||||
|  |  | ||||||
|   const relationOperands = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; |   const relationOperands = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; | ||||||
|  |  | ||||||
|   switch (filterType) { |   switch (filterDefinition.type) { | ||||||
|     case 'TEXT': |     case 'TEXT': | ||||||
|     case 'EMAIL': |     case 'EMAIL': | ||||||
|     case 'EMAILS': |     case 'EMAILS': | ||||||
| @@ -21,7 +21,6 @@ export const getOperandsForFilterType = ( | |||||||
|     case 'PHONE': |     case 'PHONE': | ||||||
|     case 'LINK': |     case 'LINK': | ||||||
|     case 'LINKS': |     case 'LINKS': | ||||||
|     case 'ACTOR': |  | ||||||
|     case 'ARRAY': |     case 'ARRAY': | ||||||
|     case 'PHONES': |     case 'PHONES': | ||||||
|       return [ |       return [ | ||||||
| @@ -57,10 +56,23 @@ export const getOperandsForFilterType = ( | |||||||
|       ]; |       ]; | ||||||
|     case 'RELATION': |     case 'RELATION': | ||||||
|       return [...relationOperands, ...emptyOperands]; |       return [...relationOperands, ...emptyOperands]; | ||||||
|     case 'SOURCE': |  | ||||||
|       return [...relationOperands]; |  | ||||||
|     case 'SELECT': |     case 'SELECT': | ||||||
|       return [...relationOperands]; |       return [...relationOperands]; | ||||||
|  |     case 'ACTOR': { | ||||||
|  |       if (isActorSourceCompositeFilter(filterDefinition)) { | ||||||
|  |         return [ | ||||||
|  |           ViewFilterOperand.Is, | ||||||
|  |           ViewFilterOperand.IsNot, | ||||||
|  |           ...emptyOperands, | ||||||
|  |         ]; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return [ | ||||||
|  |         ViewFilterOperand.Contains, | ||||||
|  |         ViewFilterOperand.DoesNotContain, | ||||||
|  |         ...emptyOperands, | ||||||
|  |       ]; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return []; |       return []; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; | ||||||
|  | import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType'; | ||||||
|  |  | ||||||
|  | export const getSettingsNonCompositeFieldTypeLabel = ( | ||||||
|  |   settingsNonCompositeFieldType: SettingsNonCompositeFieldType, | ||||||
|  | ) => { | ||||||
|  |   return SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[ | ||||||
|  |     settingsNonCompositeFieldType | ||||||
|  |   ].label; | ||||||
|  | }; | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; | import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; | ||||||
|  |  | ||||||
| export const getSubMenuOptions = (subMenu: FilterType | null) => { | export const getSubMenuOptions = (subMenu: FilterableFieldType | null) => { | ||||||
|   switch (subMenu) { |   switch (subMenu) { | ||||||
|     case 'ACTOR': |     case 'ACTOR': | ||||||
|       return [ |       return [ | ||||||
|   | |||||||
| @@ -1,3 +0,0 @@ | |||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; |  | ||||||
|  |  | ||||||
| export const hasSubMenuFilter = (type: FilterType) => ['ACTOR'].includes(type); |  | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
|  | import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata'; | ||||||
|  |  | ||||||
|  | export const isActorSourceCompositeFilter = ( | ||||||
|  |   filterDefinition: FilterDefinition, | ||||||
|  | ) => { | ||||||
|  |   return ( | ||||||
|  |     filterDefinition.compositeFieldName === | ||||||
|  |     ('source' satisfies keyof FieldActorValue) | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | import { | ||||||
|  |   COMPOSITE_FIELD_TYPES, | ||||||
|  |   CompositeFieldType, | ||||||
|  | } from '@/settings/data-model/types/CompositeFieldType'; | ||||||
|  | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
|  |  | ||||||
|  | export const isCompositeField = (type: FieldType): type is CompositeFieldType => | ||||||
|  |   COMPOSITE_FIELD_TYPES.includes(type as any); | ||||||
| @@ -177,7 +177,7 @@ export type FieldMetadata = | |||||||
|   | FieldArrayMetadata; |   | FieldArrayMetadata; | ||||||
|  |  | ||||||
| export type FieldTextValue = string; | export type FieldTextValue = string; | ||||||
| export type FieldUUidValue = string; | export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ? | ||||||
| export type FieldDateTimeValue = string | null; | export type FieldDateTimeValue = string | null; | ||||||
| export type FieldDateValue = string | null; | export type FieldDateValue = string | null; | ||||||
| export type FieldNumberValue = number | null; | export type FieldNumberValue = number | null; | ||||||
| @@ -225,6 +225,8 @@ export type FieldRelationValue< | |||||||
| export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[]; | export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[]; | ||||||
| export type FieldJsonValue = Record<string, Json> | Json[] | null; | export type FieldJsonValue = Record<string, Json> | Json[] | null; | ||||||
|  |  | ||||||
|  | export type FieldRichTextValue = Record<string, Json> | Json[] | null; | ||||||
|  |  | ||||||
| export type FieldActorValue = { | export type FieldActorValue = { | ||||||
|   source: string; |   source: string; | ||||||
|   workspaceMemberId?: string; |   workspaceMemberId?: string; | ||||||
|   | |||||||
| @@ -0,0 +1,339 @@ | |||||||
|  | import { | ||||||
|  |   ActorFilter, | ||||||
|  |   AddressFilter, | ||||||
|  |   CurrencyFilter, | ||||||
|  |   DateFilter, | ||||||
|  |   EmailsFilter, | ||||||
|  |   FloatFilter, | ||||||
|  |   RecordGqlOperationFilter, | ||||||
|  |   RelationFilter, | ||||||
|  |   StringFilter, | ||||||
|  |   URLFilter, | ||||||
|  |   UUIDFilter, | ||||||
|  | } from '@/object-record/graphql/types/RecordGqlOperationFilter'; | ||||||
|  | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
|  | import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | ||||||
|  | import { isNonEmptyString } from '@sniptt/guards'; | ||||||
|  | import { Field } from '~/generated/graphql'; | ||||||
|  | import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; | ||||||
|  |  | ||||||
|  | // TODO: fix this | ||||||
|  | export const applyEmptyFilters = ( | ||||||
|  |   operand: ViewFilterOperand, | ||||||
|  |   correspondingField: Pick<Field, 'id' | 'name'>, | ||||||
|  |   objectRecordFilters: RecordGqlOperationFilter[], | ||||||
|  |   definition: FilterDefinition, | ||||||
|  | ) => { | ||||||
|  |   let emptyRecordFilter: RecordGqlOperationFilter = {}; | ||||||
|  |  | ||||||
|  |   const compositeFieldName = definition.compositeFieldName; | ||||||
|  |  | ||||||
|  |   const isCompositeField = isNonEmptyString(compositeFieldName); | ||||||
|  |  | ||||||
|  |   switch (definition.type) { | ||||||
|  |     case 'TEXT': | ||||||
|  |     case 'EMAIL': | ||||||
|  |     case 'PHONE': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         or: [ | ||||||
|  |           { [correspondingField.name]: { ilike: '' } as StringFilter }, | ||||||
|  |           { [correspondingField.name]: { is: 'NULL' } as StringFilter }, | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'PHONES': { | ||||||
|  |       if (!isCompositeField) { | ||||||
|  |         const phonesFilter = generateILikeFiltersForCompositeFields( | ||||||
|  |           '', | ||||||
|  |           correspondingField.name, | ||||||
|  |           ['primaryPhoneNumber', 'primaryPhoneCountryCode'], | ||||||
|  |           true, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           and: phonesFilter, | ||||||
|  |         }; | ||||||
|  |         break; | ||||||
|  |       } else { | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           or: [ | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { ilike: '' }, | ||||||
|  |               } as StringFilter, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { is: 'NULL' }, | ||||||
|  |               } as StringFilter, | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     case 'CURRENCY': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         or: [ | ||||||
|  |           { | ||||||
|  |             [correspondingField.name]: { | ||||||
|  |               amountMicros: { is: 'NULL' }, | ||||||
|  |             } as CurrencyFilter, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'FULL_NAME': { | ||||||
|  |       if (!isCompositeField) { | ||||||
|  |         const fullNameFilters = generateILikeFiltersForCompositeFields( | ||||||
|  |           '', | ||||||
|  |           correspondingField.name, | ||||||
|  |           ['firstName', 'lastName'], | ||||||
|  |           true, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           and: fullNameFilters, | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           or: [ | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { ilike: '' }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { is: 'NULL' }, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case 'LINK': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         or: [ | ||||||
|  |           { [correspondingField.name]: { url: { ilike: '' } } as URLFilter }, | ||||||
|  |           { | ||||||
|  |             [correspondingField.name]: { url: { is: 'NULL' } } as URLFilter, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'LINKS': { | ||||||
|  |       if (!isCompositeField) { | ||||||
|  |         const linksFilters = generateILikeFiltersForCompositeFields( | ||||||
|  |           '', | ||||||
|  |           correspondingField.name, | ||||||
|  |           ['primaryLinkLabel', 'primaryLinkUrl'], | ||||||
|  |           true, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           and: linksFilters, | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           or: [ | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { ilike: '' }, | ||||||
|  |               } as URLFilter, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { is: 'NULL' }, | ||||||
|  |               } as URLFilter, | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case 'ADDRESS': | ||||||
|  |       if (!isCompositeField) { | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           and: [ | ||||||
|  |             { | ||||||
|  |               or: [ | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressStreet1: { ilike: '' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressStreet1: { is: 'NULL' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |               ], | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               or: [ | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressStreet2: { ilike: '' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressStreet2: { is: 'NULL' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |               ], | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               or: [ | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressCity: { ilike: '' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressCity: { is: 'NULL' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |               ], | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               or: [ | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressState: { ilike: '' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressState: { is: 'NULL' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |               ], | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               or: [ | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressCountry: { ilike: '' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressCountry: { is: 'NULL' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |               ], | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               or: [ | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressPostcode: { ilike: '' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     addressPostcode: { is: 'NULL' }, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |               ], | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |       } else { | ||||||
|  |         emptyRecordFilter = { | ||||||
|  |           or: [ | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { ilike: '' }, | ||||||
|  |               } as AddressFilter, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 [compositeFieldName]: { is: 'NULL' }, | ||||||
|  |               } as AddressFilter, | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case 'NUMBER': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         [correspondingField.name]: { is: 'NULL' } as FloatFilter, | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'RATING': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         [correspondingField.name]: { is: 'NULL' } as StringFilter, | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'DATE': | ||||||
|  |     case 'DATE_TIME': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         [correspondingField.name]: { is: 'NULL' } as DateFilter, | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'SELECT': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         [correspondingField.name]: { is: 'NULL' } as UUIDFilter, | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'RELATION': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         [correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter, | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'ACTOR': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         or: [ | ||||||
|  |           { | ||||||
|  |             [correspondingField.name]: { | ||||||
|  |               name: { ilike: '' }, | ||||||
|  |             } as ActorFilter, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             [correspondingField.name]: { | ||||||
|  |               name: { is: 'NULL' }, | ||||||
|  |             } as ActorFilter, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     case 'EMAILS': | ||||||
|  |       emptyRecordFilter = { | ||||||
|  |         or: [ | ||||||
|  |           { | ||||||
|  |             [correspondingField.name]: { | ||||||
|  |               primaryEmail: { ilike: '' }, | ||||||
|  |             } as EmailsFilter, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             [correspondingField.name]: { | ||||||
|  |               primaryEmail: { is: 'NULL' }, | ||||||
|  |             } as EmailsFilter, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  |       break; | ||||||
|  |     default: | ||||||
|  |       throw new Error(`Unsupported empty filter type ${definition.type}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   switch (operand) { | ||||||
|  |     case ViewFilterOperand.IsEmpty: | ||||||
|  |       objectRecordFilters.push(emptyRecordFilter); | ||||||
|  |       break; | ||||||
|  |     case ViewFilterOperand.IsNotEmpty: | ||||||
|  |       objectRecordFilters.push({ | ||||||
|  |         not: emptyRecordFilter, | ||||||
|  |       }); | ||||||
|  |       break; | ||||||
|  |     default: | ||||||
|  |       throw new Error( | ||||||
|  |         `Unknown operand ${operand} for ${definition.type} filter`, | ||||||
|  |       ); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -10,9 +10,9 @@ import { | |||||||
|   RecordGqlOperationFilter, |   RecordGqlOperationFilter, | ||||||
|   RelationFilter, |   RelationFilter, | ||||||
|   StringFilter, |   StringFilter, | ||||||
|  |   URLFilter, | ||||||
|   UUIDFilter, |   UUIDFilter, | ||||||
| } from '@/object-record/graphql/types/RecordGqlOperationFilter'; | } from '@/object-record/graphql/types/RecordGqlOperationFilter'; | ||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; |  | ||||||
| import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; | import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; | ||||||
| import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | ||||||
| import { Field } from '~/generated/graphql'; | import { Field } from '~/generated/graphql'; | ||||||
| @@ -24,244 +24,15 @@ import { | |||||||
|   convertLessThanRatingToArrayOfRatingValues, |   convertLessThanRatingToArrayOfRatingValues, | ||||||
|   convertRatingToRatingValue, |   convertRatingToRatingValue, | ||||||
| } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; | } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; | ||||||
|  | import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | ||||||
|  | import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; | ||||||
|  | import { applyEmptyFilters } from '@/object-record/record-filter/utils/applyEmptyFilters'; | ||||||
| import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; | import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; | ||||||
| import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; | import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; | ||||||
| import { z } from 'zod'; | import { z } from 'zod'; | ||||||
| import { Filter } from '../../object-filter-dropdown/types/Filter'; |  | ||||||
|  |  | ||||||
| const applyEmptyFilters = ( |  | ||||||
|   operand: ViewFilterOperand, |  | ||||||
|   correspondingField: Pick<Field, 'id' | 'name'>, |  | ||||||
|   objectRecordFilters: RecordGqlOperationFilter[], |  | ||||||
|   filterType: FilterType, |  | ||||||
| ) => { |  | ||||||
|   let emptyRecordFilter: RecordGqlOperationFilter = {}; |  | ||||||
|  |  | ||||||
|   switch (filterType) { |  | ||||||
|     case 'TEXT': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         or: [ |  | ||||||
|           { [correspondingField.name]: { ilike: '' } as StringFilter }, |  | ||||||
|           { [correspondingField.name]: { is: 'NULL' } as StringFilter }, |  | ||||||
|         ], |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'PHONES': { |  | ||||||
|       const phonesFilter = generateILikeFiltersForCompositeFields( |  | ||||||
|         '', |  | ||||||
|         correspondingField.name, |  | ||||||
|         ['primaryPhoneNumber', 'primaryPhoneCountryCode'], |  | ||||||
|         true, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         and: phonesFilter, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     case 'CURRENCY': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         or: [ |  | ||||||
|           { |  | ||||||
|             [correspondingField.name]: { |  | ||||||
|               amountMicros: { is: 'NULL' }, |  | ||||||
|             } as CurrencyFilter, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'FULL_NAME': { |  | ||||||
|       const fullNameFilters = generateILikeFiltersForCompositeFields( |  | ||||||
|         '', |  | ||||||
|         correspondingField.name, |  | ||||||
|         ['firstName', 'lastName'], |  | ||||||
|         true, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         and: fullNameFilters, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     case 'LINKS': { |  | ||||||
|       const linksFilters = generateILikeFiltersForCompositeFields( |  | ||||||
|         '', |  | ||||||
|         correspondingField.name, |  | ||||||
|         ['primaryLinkLabel', 'primaryLinkUrl'], |  | ||||||
|         true, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         and: linksFilters, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     case 'ADDRESS': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         and: [ |  | ||||||
|           { |  | ||||||
|             or: [ |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressStreet1: { ilike: '' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressStreet1: { is: 'NULL' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             or: [ |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressStreet2: { ilike: '' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressStreet2: { is: 'NULL' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             or: [ |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressCity: { ilike: '' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressCity: { is: 'NULL' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             or: [ |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressState: { ilike: '' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressState: { is: 'NULL' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             or: [ |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressCountry: { ilike: '' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressCountry: { is: 'NULL' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             or: [ |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressPostcode: { ilike: '' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 [correspondingField.name]: { |  | ||||||
|                   addressPostcode: { is: 'NULL' }, |  | ||||||
|                 } as AddressFilter, |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'NUMBER': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         [correspondingField.name]: { is: 'NULL' } as FloatFilter, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'RATING': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         [correspondingField.name]: { is: 'NULL' } as StringFilter, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'DATE': |  | ||||||
|     case 'DATE_TIME': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         [correspondingField.name]: { is: 'NULL' } as DateFilter, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'SELECT': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         [correspondingField.name]: { is: 'NULL' } as UUIDFilter, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'RELATION': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         [correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter, |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'ACTOR': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         or: [ |  | ||||||
|           { |  | ||||||
|             [correspondingField.name]: { |  | ||||||
|               name: { ilike: '' }, |  | ||||||
|             } as ActorFilter, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             [correspondingField.name]: { |  | ||||||
|               name: { is: 'NULL' }, |  | ||||||
|             } as ActorFilter, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     case 'EMAILS': |  | ||||||
|       emptyRecordFilter = { |  | ||||||
|         or: [ |  | ||||||
|           { |  | ||||||
|             [correspondingField.name]: { |  | ||||||
|               primaryEmail: { ilike: '' }, |  | ||||||
|             } as EmailsFilter, |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             [correspondingField.name]: { |  | ||||||
|               primaryEmail: { is: 'NULL' }, |  | ||||||
|             } as EmailsFilter, |  | ||||||
|           }, |  | ||||||
|         ], |  | ||||||
|       }; |  | ||||||
|       break; |  | ||||||
|     default: |  | ||||||
|       throw new Error(`Unsupported empty filter type ${filterType}`); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   switch (operand) { |  | ||||||
|     case ViewFilterOperand.IsEmpty: |  | ||||||
|       objectRecordFilters.push(emptyRecordFilter); |  | ||||||
|       break; |  | ||||||
|     case ViewFilterOperand.IsNotEmpty: |  | ||||||
|       objectRecordFilters.push({ |  | ||||||
|         not: emptyRecordFilter, |  | ||||||
|       }); |  | ||||||
|       break; |  | ||||||
|     default: |  | ||||||
|       throw new Error(`Unknown operand ${operand} for ${filterType} filter`); |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
|  |  | ||||||
|  | // TODO: break this down into smaller functions and make the whole thing immutable | ||||||
|  | // Especially applyEmptyFilters | ||||||
| export const turnObjectDropdownFilterIntoQueryFilter = ( | export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||||
|   rawUIFilters: Filter[], |   rawUIFilters: Filter[], | ||||||
|   fields: Pick<Field, 'id' | 'name'>[], |   fields: Pick<Field, 'id' | 'name'>[], | ||||||
| @@ -273,7 +44,11 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|       (field) => field.id === rawUIFilter.fieldMetadataId, |       (field) => field.id === rawUIFilter.fieldMetadataId, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const isValuelessOperand = [ |     const compositeFieldName = rawUIFilter.definition.compositeFieldName; | ||||||
|  |  | ||||||
|  |     const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); | ||||||
|  |  | ||||||
|  |     const isEmptyOperand = [ | ||||||
|       ViewFilterOperand.IsEmpty, |       ViewFilterOperand.IsEmpty, | ||||||
|       ViewFilterOperand.IsNotEmpty, |       ViewFilterOperand.IsNotEmpty, | ||||||
|       ViewFilterOperand.IsInPast, |       ViewFilterOperand.IsInPast, | ||||||
| @@ -285,7 +60,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|       continue; |       continue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!isValuelessOperand) { |     if (!isEmptyOperand) { | ||||||
|       if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') { |       if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') { | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
| @@ -316,7 +91,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -355,7 +130,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
| @@ -372,8 +147,9 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               operand: ViewFilterOperand.IsRelative, |               operand: ViewFilterOperand.IsRelative, | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             if (!defaultDateRange) |             if (!defaultDateRange) { | ||||||
|               throw new Error('Failed to resolve default date range'); |               throw new Error('Failed to resolve default date range'); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             const { start, end } = dateRange ?? defaultDateRange; |             const { start, end } = dateRange ?? defaultDateRange; | ||||||
|  |  | ||||||
| @@ -484,7 +260,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -515,7 +291,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -525,7 +301,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case 'RELATION': { |       case 'RELATION': { | ||||||
|         if (!isValuelessOperand) { |         if (!isEmptyOperand) { | ||||||
|           try { |           try { | ||||||
|             JSON.parse(rawUIFilter.value); |             JSON.parse(rawUIFilter.value); | ||||||
|           } catch (e) { |           } catch (e) { | ||||||
| @@ -570,7 +346,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|                 rawUIFilter.operand, |                 rawUIFilter.operand, | ||||||
|                 correspondingField, |                 correspondingField, | ||||||
|                 objectRecordFilters, |                 objectRecordFilters, | ||||||
|                 rawUIFilter.definition.type, |                 rawUIFilter.definition, | ||||||
|               ); |               ); | ||||||
|               break; |               break; | ||||||
|             default: |             default: | ||||||
| @@ -603,7 +379,44 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|  |             ); | ||||||
|  |             break; | ||||||
|  |           default: | ||||||
|  |             throw new Error( | ||||||
|  |               `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |       case 'LINK': | ||||||
|  |         switch (rawUIFilter.operand) { | ||||||
|  |           case ViewFilterOperand.Contains: | ||||||
|  |             objectRecordFilters.push({ | ||||||
|  |               [correspondingField.name]: { | ||||||
|  |                 url: { | ||||||
|  |                   ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                 }, | ||||||
|  |               } as URLFilter, | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           case ViewFilterOperand.DoesNotContain: | ||||||
|  |             objectRecordFilters.push({ | ||||||
|  |               not: { | ||||||
|  |                 [correspondingField.name]: { | ||||||
|  |                   url: { | ||||||
|  |                     ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                   }, | ||||||
|  |                 } as URLFilter, | ||||||
|  |               }, | ||||||
|  |             }); | ||||||
|  |             break; | ||||||
|  |           case ViewFilterOperand.IsEmpty: | ||||||
|  |           case ViewFilterOperand.IsNotEmpty: | ||||||
|  |             applyEmptyFilters( | ||||||
|  |               rawUIFilter.operand, | ||||||
|  |               correspondingField, | ||||||
|  |               objectRecordFilters, | ||||||
|  |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -618,13 +431,25 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|           correspondingField.name, |           correspondingField.name, | ||||||
|           ['primaryLinkLabel', 'primaryLinkUrl'], |           ['primaryLinkLabel', 'primaryLinkUrl'], | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         switch (rawUIFilter.operand) { |         switch (rawUIFilter.operand) { | ||||||
|           case ViewFilterOperand.Contains: |           case ViewFilterOperand.Contains: | ||||||
|  |             if (!isCompositeFieldFiter) { | ||||||
|               objectRecordFilters.push({ |               objectRecordFilters.push({ | ||||||
|                 or: linksFilters, |                 or: linksFilters, | ||||||
|               }); |               }); | ||||||
|  |             } else { | ||||||
|  |               objectRecordFilters.push({ | ||||||
|  |                 [correspondingField.name]: { | ||||||
|  |                   [compositeFieldName]: { | ||||||
|  |                     ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|             break; |             break; | ||||||
|           case ViewFilterOperand.DoesNotContain: |           case ViewFilterOperand.DoesNotContain: | ||||||
|  |             if (!isCompositeFieldFiter) { | ||||||
|               objectRecordFilters.push({ |               objectRecordFilters.push({ | ||||||
|                 and: linksFilters.map((filter) => { |                 and: linksFilters.map((filter) => { | ||||||
|                   return { |                   return { | ||||||
| @@ -632,6 +457,17 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|                   }; |                   }; | ||||||
|                 }), |                 }), | ||||||
|               }); |               }); | ||||||
|  |             } else { | ||||||
|  |               objectRecordFilters.push({ | ||||||
|  |                 not: { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     [compositeFieldName]: { | ||||||
|  |                       ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|             break; |             break; | ||||||
|           case ViewFilterOperand.IsEmpty: |           case ViewFilterOperand.IsEmpty: | ||||||
|           case ViewFilterOperand.IsNotEmpty: |           case ViewFilterOperand.IsNotEmpty: | ||||||
| @@ -639,7 +475,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -657,11 +493,22 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|         ); |         ); | ||||||
|         switch (rawUIFilter.operand) { |         switch (rawUIFilter.operand) { | ||||||
|           case ViewFilterOperand.Contains: |           case ViewFilterOperand.Contains: | ||||||
|  |             if (!isCompositeFieldFiter) { | ||||||
|               objectRecordFilters.push({ |               objectRecordFilters.push({ | ||||||
|                 or: fullNameFilters, |                 or: fullNameFilters, | ||||||
|               }); |               }); | ||||||
|  |             } else { | ||||||
|  |               objectRecordFilters.push({ | ||||||
|  |                 [correspondingField.name]: { | ||||||
|  |                   [compositeFieldName]: { | ||||||
|  |                     ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|             break; |             break; | ||||||
|           case ViewFilterOperand.DoesNotContain: |           case ViewFilterOperand.DoesNotContain: | ||||||
|  |             if (!isCompositeFieldFiter) { | ||||||
|               objectRecordFilters.push({ |               objectRecordFilters.push({ | ||||||
|                 and: fullNameFilters.map((filter) => { |                 and: fullNameFilters.map((filter) => { | ||||||
|                   return { |                   return { | ||||||
| @@ -669,6 +516,17 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|                   }; |                   }; | ||||||
|                 }), |                 }), | ||||||
|               }); |               }); | ||||||
|  |             } else { | ||||||
|  |               objectRecordFilters.push({ | ||||||
|  |                 not: { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     [compositeFieldName]: { | ||||||
|  |                       ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                     }, | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|             break; |             break; | ||||||
|           case ViewFilterOperand.IsEmpty: |           case ViewFilterOperand.IsEmpty: | ||||||
|           case ViewFilterOperand.IsNotEmpty: |           case ViewFilterOperand.IsNotEmpty: | ||||||
| @@ -676,7 +534,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -689,6 +547,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|       case 'ADDRESS': |       case 'ADDRESS': | ||||||
|         switch (rawUIFilter.operand) { |         switch (rawUIFilter.operand) { | ||||||
|           case ViewFilterOperand.Contains: |           case ViewFilterOperand.Contains: | ||||||
|  |             if (!isCompositeFieldFiter) { | ||||||
|               objectRecordFilters.push({ |               objectRecordFilters.push({ | ||||||
|                 or: [ |                 or: [ | ||||||
|                   { |                   { | ||||||
| @@ -735,8 +594,18 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|                   }, |                   }, | ||||||
|                 ], |                 ], | ||||||
|               }); |               }); | ||||||
|  |             } else { | ||||||
|  |               objectRecordFilters.push({ | ||||||
|  |                 [correspondingField.name]: { | ||||||
|  |                   [compositeFieldName]: { | ||||||
|  |                     ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                   } as AddressFilter, | ||||||
|  |                 }, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|             break; |             break; | ||||||
|           case ViewFilterOperand.DoesNotContain: |           case ViewFilterOperand.DoesNotContain: | ||||||
|  |             if (!isCompositeFieldFiter) { | ||||||
|               objectRecordFilters.push({ |               objectRecordFilters.push({ | ||||||
|                 and: [ |                 and: [ | ||||||
|                   { |                   { | ||||||
| @@ -768,6 +637,17 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|                   }, |                   }, | ||||||
|                 ], |                 ], | ||||||
|               }); |               }); | ||||||
|  |             } else { | ||||||
|  |               objectRecordFilters.push({ | ||||||
|  |                 not: { | ||||||
|  |                   [correspondingField.name]: { | ||||||
|  |                     [compositeFieldName]: { | ||||||
|  |                       ilike: `%${rawUIFilter.value}%`, | ||||||
|  |                     } as AddressFilter, | ||||||
|  |                   }, | ||||||
|  |                 }, | ||||||
|  |               }); | ||||||
|  |             } | ||||||
|             break; |             break; | ||||||
|           case ViewFilterOperand.IsEmpty: |           case ViewFilterOperand.IsEmpty: | ||||||
|           case ViewFilterOperand.IsNotEmpty: |           case ViewFilterOperand.IsNotEmpty: | ||||||
| @@ -775,7 +655,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -785,12 +665,12 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case 'SELECT': { |       case 'SELECT': { | ||||||
|         if (isValuelessOperand) { |         if (isEmptyOperand) { | ||||||
|           applyEmptyFilters( |           applyEmptyFilters( | ||||||
|             rawUIFilter.operand, |             rawUIFilter.operand, | ||||||
|             correspondingField, |             correspondingField, | ||||||
|             objectRecordFilters, |             objectRecordFilters, | ||||||
|             rawUIFilter.definition.type, |             rawUIFilter.definition, | ||||||
|           ); |           ); | ||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
| @@ -836,10 +716,9 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       case 'ACTOR': |       case 'ACTOR': | ||||||
|         if (rawUIFilter.definition.subFieldType !== undefined) { |         if (isActorSourceCompositeFilter(rawUIFilter.definition)) { | ||||||
|           const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[]; |           const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[]; | ||||||
|           switch (rawUIFilter.definition.subFieldType) { |  | ||||||
|             case 'SOURCE': |  | ||||||
|           switch (rawUIFilter.operand) { |           switch (rawUIFilter.operand) { | ||||||
|             case ViewFilterOperand.Is: |             case ViewFilterOperand.Is: | ||||||
|               objectRecordFilters.push({ |               objectRecordFilters.push({ | ||||||
| @@ -856,21 +735,14 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|                 objectRecordFilters.push({ |                 objectRecordFilters.push({ | ||||||
|                   not: { |                   not: { | ||||||
|                     [correspondingField.name]: { |                     [correspondingField.name]: { | ||||||
|                           [rawUIFilter.definition.subFieldType.toLowerCase()]: { |                       source: { | ||||||
|                         in: parsedRecordIds, |                         in: parsedRecordIds, | ||||||
|                       } as RelationFilter, |                       } as RelationFilter, | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
|                 }); |                 }); | ||||||
|               } |               } | ||||||
|  |  | ||||||
|               break; |               break; | ||||||
|  |  | ||||||
|                 default: |  | ||||||
|                   throw new Error( |  | ||||||
|                     `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.subFieldType} filter`, |  | ||||||
|                   ); |  | ||||||
|               } |  | ||||||
|           } |           } | ||||||
|         } else { |         } else { | ||||||
|           switch (rawUIFilter.operand) { |           switch (rawUIFilter.operand) { | ||||||
| @@ -908,15 +780,14 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|                 rawUIFilter.operand, |                 rawUIFilter.operand, | ||||||
|                 correspondingField, |                 correspondingField, | ||||||
|                 objectRecordFilters, |                 objectRecordFilters, | ||||||
|                 rawUIFilter.definition.type, |                 rawUIFilter.definition, | ||||||
|               ); |               ); | ||||||
|               break; |               break; | ||||||
|             default: |             default: | ||||||
|               throw new Error( |               throw new Error( | ||||||
|                 `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, |                 `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.label} filter`, | ||||||
|               ); |               ); | ||||||
|           } |           } | ||||||
|           break; |  | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|       case 'EMAILS': |       case 'EMAILS': | ||||||
| @@ -955,7 +826,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
| @@ -991,7 +862,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | |||||||
|               rawUIFilter.operand, |               rawUIFilter.operand, | ||||||
|               correspondingField, |               correspondingField, | ||||||
|               objectRecordFilters, |               objectRecordFilters, | ||||||
|               rawUIFilter.definition.type, |               rawUIFilter.definition, | ||||||
|             ); |             ); | ||||||
|             break; |             break; | ||||||
|           default: |           default: | ||||||
|   | |||||||
| @@ -5,7 +5,8 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u | |||||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||||
| import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; | import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; | ||||||
| import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | ||||||
| import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
|  | import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; | ||||||
| import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; | import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; | ||||||
| import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; | import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; | ||||||
| import { isDefined } from '~/utils/isDefined'; | import { isDefined } from '~/utils/isDefined'; | ||||||
| @@ -42,7 +43,15 @@ export const useHandleToggleColumnFilter = ({ | |||||||
|         correspondingColumnDefinition?.type, |         correspondingColumnDefinition?.type, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|       const availableOperandsForFilter = getOperandsForFilterType(filterType); |       const filterDefinition = { | ||||||
|  |         label: correspondingColumnDefinition.label, | ||||||
|  |         iconName: correspondingColumnDefinition.iconName, | ||||||
|  |         fieldMetadataId, | ||||||
|  |         type: filterType, | ||||||
|  |       } satisfies FilterDefinition; | ||||||
|  |  | ||||||
|  |       const availableOperandsForFilter = | ||||||
|  |         getOperandsForFilterDefinition(filterDefinition); | ||||||
|  |  | ||||||
|       const defaultOperand = availableOperandsForFilter[0]; |       const defaultOperand = availableOperandsForFilter[0]; | ||||||
|  |  | ||||||
| @@ -51,12 +60,7 @@ export const useHandleToggleColumnFilter = ({ | |||||||
|         fieldMetadataId, |         fieldMetadataId, | ||||||
|         operand: defaultOperand, |         operand: defaultOperand, | ||||||
|         displayValue: '', |         displayValue: '', | ||||||
|         definition: { |         definition: filterDefinition, | ||||||
|           label: correspondingColumnDefinition.label, |  | ||||||
|           iconName: correspondingColumnDefinition.iconName, |  | ||||||
|           fieldMetadataId, |  | ||||||
|           type: filterType, |  | ||||||
|         }, |  | ||||||
|         value: '', |         value: '', | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,190 @@ | |||||||
|  | import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; | ||||||
|  | import { | ||||||
|  |   FieldActorValue, | ||||||
|  |   FieldAddressValue, | ||||||
|  |   FieldCurrencyValue, | ||||||
|  |   FieldEmailsValue, | ||||||
|  |   FieldFullNameValue, | ||||||
|  |   FieldLinksValue, | ||||||
|  |   FieldLinkValue, | ||||||
|  |   FieldPhonesValue, | ||||||
|  | } from '@/object-record/record-field/types/FieldMetadata'; | ||||||
|  | import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; | ||||||
|  | import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; | ||||||
|  | import { | ||||||
|  |   IllustrationIconCurrency, | ||||||
|  |   IllustrationIconLink, | ||||||
|  |   IllustrationIconMail, | ||||||
|  |   IllustrationIconMap, | ||||||
|  |   IllustrationIconPhone, | ||||||
|  |   IllustrationIconSetting, | ||||||
|  |   IllustrationIconUser, | ||||||
|  | } from 'twenty-ui'; | ||||||
|  | import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||||
|  |  | ||||||
|  | export type SettingsCompositeFieldTypeConfig<T> = SettingsFieldTypeConfig<T> & { | ||||||
|  |   subFields: (keyof T)[]; | ||||||
|  |   filterableSubFields: (keyof T)[]; | ||||||
|  |   labelBySubField: Record<keyof T, string>; | ||||||
|  |   exampleValue: T; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type SettingsCompositeFieldTypeConfigArray = Record< | ||||||
|  |   CompositeFieldType, | ||||||
|  |   SettingsCompositeFieldTypeConfig<any> | ||||||
|  | >; | ||||||
|  |  | ||||||
|  | export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { | ||||||
|  |   [FieldMetadataType.Currency]: { | ||||||
|  |     label: 'Currency', | ||||||
|  |     Icon: IllustrationIconCurrency, | ||||||
|  |     subFields: ['amountMicros', 'currencyCode'], | ||||||
|  |     filterableSubFields: ['amountMicros', 'currencyCode'], | ||||||
|  |     labelBySubField: { | ||||||
|  |       amountMicros: 'Amount', | ||||||
|  |       currencyCode: 'Currency', | ||||||
|  |     }, | ||||||
|  |     exampleValue: { | ||||||
|  |       amountMicros: 2000000000, | ||||||
|  |       currencyCode: CurrencyCode.USD, | ||||||
|  |     }, | ||||||
|  |     category: 'Basic', | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldCurrencyValue>, | ||||||
|  |   [FieldMetadataType.Emails]: { | ||||||
|  |     label: 'Emails', | ||||||
|  |     Icon: IllustrationIconMail, | ||||||
|  |     subFields: ['primaryEmail', 'additionalEmails'], | ||||||
|  |     filterableSubFields: ['primaryEmail'], | ||||||
|  |     labelBySubField: { | ||||||
|  |       primaryEmail: 'Primary Email', | ||||||
|  |       additionalEmails: 'Additional Emails', | ||||||
|  |     }, | ||||||
|  |     exampleValue: { | ||||||
|  |       primaryEmail: 'john@twenty.com', | ||||||
|  |       additionalEmails: [ | ||||||
|  |         'tim@twenty.com', | ||||||
|  |         'timapple@twenty.com', | ||||||
|  |         'johnappletim@twenty.com', | ||||||
|  |       ], | ||||||
|  |     }, | ||||||
|  |     category: 'Basic', | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldEmailsValue>, | ||||||
|  |   [FieldMetadataType.Link]: { | ||||||
|  |     label: 'Link', | ||||||
|  |     Icon: IllustrationIconLink, | ||||||
|  |     exampleValue: { url: 'www.twenty.com', label: '' }, | ||||||
|  |     category: 'Basic', | ||||||
|  |     subFields: ['url', 'label'], | ||||||
|  |     filterableSubFields: ['url', 'label'], | ||||||
|  |     labelBySubField: { | ||||||
|  |       url: 'URL', | ||||||
|  |       label: 'Label', | ||||||
|  |     }, | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldLinkValue>, | ||||||
|  |   [FieldMetadataType.Links]: { | ||||||
|  |     label: 'Links', | ||||||
|  |     Icon: IllustrationIconLink, | ||||||
|  |     exampleValue: { | ||||||
|  |       primaryLinkUrl: 'twenty.com', | ||||||
|  |       primaryLinkLabel: '', | ||||||
|  |       secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }], | ||||||
|  |     }, | ||||||
|  |     category: 'Basic', | ||||||
|  |     subFields: ['primaryLinkUrl', 'primaryLinkLabel', 'secondaryLinks'], | ||||||
|  |     filterableSubFields: ['primaryLinkUrl', 'primaryLinkLabel'], | ||||||
|  |     labelBySubField: { | ||||||
|  |       primaryLinkUrl: 'Link URL', | ||||||
|  |       primaryLinkLabel: 'Link Label', | ||||||
|  |       secondaryLinks: 'Secondary Links', | ||||||
|  |     }, | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldLinksValue>, | ||||||
|  |   [FieldMetadataType.Phones]: { | ||||||
|  |     label: 'Phones', | ||||||
|  |     Icon: IllustrationIconPhone, | ||||||
|  |     exampleValue: { | ||||||
|  |       primaryPhoneNumber: '234-567-890', | ||||||
|  |       primaryPhoneCountryCode: '+1', | ||||||
|  |       additionalPhones: [{ number: '234-567-890', countryCode: '+1' }], | ||||||
|  |     }, | ||||||
|  |     subFields: [ | ||||||
|  |       'primaryPhoneNumber', | ||||||
|  |       'primaryPhoneCountryCode', | ||||||
|  |       'additionalPhones', | ||||||
|  |     ], | ||||||
|  |     filterableSubFields: ['primaryPhoneNumber', 'primaryPhoneCountryCode'], | ||||||
|  |     labelBySubField: { | ||||||
|  |       primaryPhoneNumber: 'Primary Phone Number', | ||||||
|  |       primaryPhoneCountryCode: 'Primary Phone Country Code', | ||||||
|  |       additionalPhones: 'Additional Phones', | ||||||
|  |     }, | ||||||
|  |     category: 'Basic', | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldPhonesValue>, | ||||||
|  |   [FieldMetadataType.FullName]: { | ||||||
|  |     label: 'Full Name', | ||||||
|  |     Icon: IllustrationIconUser, | ||||||
|  |     exampleValue: { firstName: 'John', lastName: 'Doe' }, | ||||||
|  |     category: 'Advanced', | ||||||
|  |     subFields: ['firstName', 'lastName'], | ||||||
|  |     filterableSubFields: ['firstName', 'lastName'], | ||||||
|  |     labelBySubField: { | ||||||
|  |       firstName: 'First Name', | ||||||
|  |       lastName: 'Last Name', | ||||||
|  |     }, | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldFullNameValue>, | ||||||
|  |   [FieldMetadataType.Address]: { | ||||||
|  |     label: 'Address', | ||||||
|  |     Icon: IllustrationIconMap, | ||||||
|  |     subFields: [ | ||||||
|  |       'addressStreet1', | ||||||
|  |       'addressStreet2', | ||||||
|  |       'addressCity', | ||||||
|  |       'addressState', | ||||||
|  |       'addressCountry', | ||||||
|  |       'addressPostcode', | ||||||
|  |       'addressLat', | ||||||
|  |       'addressLng', | ||||||
|  |     ], | ||||||
|  |     filterableSubFields: [ | ||||||
|  |       'addressStreet1', | ||||||
|  |       'addressStreet2', | ||||||
|  |       'addressCity', | ||||||
|  |       'addressState', | ||||||
|  |       'addressCountry', | ||||||
|  |       'addressPostcode', | ||||||
|  |     ], | ||||||
|  |     labelBySubField: { | ||||||
|  |       addressStreet1: 'Address 1', | ||||||
|  |       addressStreet2: 'Address 2', | ||||||
|  |       addressCity: 'City', | ||||||
|  |       addressState: 'State', | ||||||
|  |       addressCountry: 'Country', | ||||||
|  |       addressPostcode: 'Post Code', | ||||||
|  |       addressLat: 'Latitude', | ||||||
|  |       addressLng: 'Longitude', | ||||||
|  |     }, | ||||||
|  |     exampleValue: { | ||||||
|  |       addressStreet1: '456 Oak Street', | ||||||
|  |       addressStreet2: 'Unit 3B', | ||||||
|  |       addressCity: 'Springfield', | ||||||
|  |       addressState: 'California', | ||||||
|  |       addressCountry: 'United States', | ||||||
|  |       addressPostcode: '90210', | ||||||
|  |       addressLat: 34.0522, | ||||||
|  |       addressLng: -118.2437, | ||||||
|  |     }, | ||||||
|  |     category: 'Basic', | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldAddressValue>, | ||||||
|  |   [FieldMetadataType.Actor]: { | ||||||
|  |     label: 'Actor', | ||||||
|  |     Icon: IllustrationIconSetting, | ||||||
|  |     category: 'Basic', | ||||||
|  |     subFields: ['source', 'name', 'workspaceMemberId'], | ||||||
|  |     filterableSubFields: ['source', 'name', 'workspaceMemberId'], | ||||||
|  |     labelBySubField: { | ||||||
|  |       source: 'Source', | ||||||
|  |       name: 'Name', | ||||||
|  |       workspaceMemberId: 'Workspace Member ID', | ||||||
|  |     }, | ||||||
|  |     exampleValue: { source: 'source', name: 'name', workspaceMemberId: 'id' }, | ||||||
|  |   } as const satisfies SettingsCompositeFieldTypeConfig<FieldActorValue>, | ||||||
|  | } as const satisfies SettingsCompositeFieldTypeConfigArray; | ||||||
| @@ -1,196 +1,7 @@ | |||||||
| import { | import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; | ||||||
|   IconComponent, | import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; | ||||||
|   IllustrationIconArray, |  | ||||||
|   IllustrationIconCalendarEvent, |  | ||||||
|   IllustrationIconCalendarTime, |  | ||||||
|   IllustrationIconCurrency, |  | ||||||
|   IllustrationIconJson, |  | ||||||
|   IllustrationIconLink, |  | ||||||
|   IllustrationIconMail, |  | ||||||
|   IllustrationIconMap, |  | ||||||
|   IllustrationIconNumbers, |  | ||||||
|   IllustrationIconOneToMany, |  | ||||||
|   IllustrationIconPhone, |  | ||||||
|   IllustrationIconSetting, |  | ||||||
|   IllustrationIconStar, |  | ||||||
|   IllustrationIconTag, |  | ||||||
|   IllustrationIconTags, |  | ||||||
|   IllustrationIconText, |  | ||||||
|   IllustrationIconToggle, |  | ||||||
|   IllustrationIconUid, |  | ||||||
|   IllustrationIconUser, |  | ||||||
| } from 'twenty-ui'; |  | ||||||
|  |  | ||||||
| import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; |  | ||||||
| import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue'; |  | ||||||
| import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType'; |  | ||||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; |  | ||||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; |  | ||||||
|  |  | ||||||
| DEFAULT_DATE_VALUE.setFullYear(DEFAULT_DATE_VALUE.getFullYear() + 2); |  | ||||||
|  |  | ||||||
| export type SettingsFieldTypeConfig = { |  | ||||||
|   label: string; |  | ||||||
|   Icon: IconComponent; |  | ||||||
|   exampleValue?: unknown; |  | ||||||
|   category: SettingsFieldTypeCategoryType; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const SETTINGS_FIELD_TYPE_CONFIGS = { | export const SETTINGS_FIELD_TYPE_CONFIGS = { | ||||||
|   [FieldMetadataType.Uuid]: { |   ...SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS, | ||||||
|     label: 'Unique ID', |   ...SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS, | ||||||
|     Icon: IllustrationIconUid, | }; | ||||||
|     exampleValue: '00000000-0000-0000-0000-000000000000', |  | ||||||
|     category: 'Advanced', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Text]: { |  | ||||||
|     label: 'Text', |  | ||||||
|     Icon: IllustrationIconText, |  | ||||||
|     exampleValue: |  | ||||||
|       'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.', |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Numeric]: { |  | ||||||
|     label: 'Numeric', |  | ||||||
|     Icon: IllustrationIconNumbers, |  | ||||||
|     exampleValue: 2000, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Number]: { |  | ||||||
|     label: 'Number', |  | ||||||
|     Icon: IllustrationIconNumbers, |  | ||||||
|     exampleValue: 2000, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Link]: { |  | ||||||
|     label: 'Link', |  | ||||||
|     Icon: IllustrationIconLink, |  | ||||||
|     exampleValue: { url: 'www.twenty.com', label: '' }, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Links]: { |  | ||||||
|     label: 'Links', |  | ||||||
|     Icon: IllustrationIconLink, |  | ||||||
|     exampleValue: { primaryLinkUrl: 'twenty.com', primaryLinkLabel: '' }, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Boolean]: { |  | ||||||
|     label: 'True/False', |  | ||||||
|     Icon: IllustrationIconToggle, |  | ||||||
|     exampleValue: true, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.DateTime]: { |  | ||||||
|     label: 'Date and Time', |  | ||||||
|     Icon: IllustrationIconCalendarTime, |  | ||||||
|     exampleValue: DEFAULT_DATE_VALUE.toISOString(), |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Date]: { |  | ||||||
|     label: 'Date', |  | ||||||
|     Icon: IllustrationIconCalendarEvent, |  | ||||||
|     exampleValue: DEFAULT_DATE_VALUE.toISOString(), |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Select]: { |  | ||||||
|     label: 'Select', |  | ||||||
|     Icon: IllustrationIconTag, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.MultiSelect]: { |  | ||||||
|     label: 'Multi-select', |  | ||||||
|     Icon: IllustrationIconTags, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Currency]: { |  | ||||||
|     label: 'Currency', |  | ||||||
|     Icon: IllustrationIconCurrency, |  | ||||||
|     exampleValue: { amountMicros: 2000000000, currencyCode: CurrencyCode.USD }, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Relation]: { |  | ||||||
|     label: 'Relation', |  | ||||||
|     Icon: IllustrationIconOneToMany, |  | ||||||
|     category: 'Relation', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Email]: { |  | ||||||
|     label: 'Email', |  | ||||||
|     Icon: IllustrationIconMail, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Emails]: { |  | ||||||
|     label: 'Emails', |  | ||||||
|     Icon: IllustrationIconMail, |  | ||||||
|     exampleValue: { primaryEmail: 'john@twenty.com' }, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Phone]: { |  | ||||||
|     label: 'Phone', |  | ||||||
|     Icon: IllustrationIconPhone, |  | ||||||
|     exampleValue: '+1234-567-890', |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Phones]: { |  | ||||||
|     label: 'Phones', |  | ||||||
|     Icon: IllustrationIconPhone, |  | ||||||
|     exampleValue: { |  | ||||||
|       primaryPhoneNumber: '234-567-890', |  | ||||||
|       primaryPhoneCountryCode: '+1', |  | ||||||
|     }, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Rating]: { |  | ||||||
|     label: 'Rating', |  | ||||||
|     Icon: IllustrationIconStar, |  | ||||||
|     exampleValue: '3', |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.FullName]: { |  | ||||||
|     label: 'Full Name', |  | ||||||
|     Icon: IllustrationIconUser, |  | ||||||
|     exampleValue: { firstName: 'John', lastName: 'Doe' }, |  | ||||||
|     category: 'Advanced', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Address]: { |  | ||||||
|     label: 'Address', |  | ||||||
|     Icon: IllustrationIconMap, |  | ||||||
|     exampleValue: { |  | ||||||
|       addressStreet1: '456 Oak Street', |  | ||||||
|       addressStreet2: 'Unit 3B', |  | ||||||
|       addressCity: 'Springfield', |  | ||||||
|       addressState: 'California', |  | ||||||
|       addressCountry: 'United States', |  | ||||||
|       addressPostcode: '90210', |  | ||||||
|       addressLat: 34.0522, |  | ||||||
|       addressLng: -118.2437, |  | ||||||
|     }, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.RawJson]: { |  | ||||||
|     label: 'JSON', |  | ||||||
|     Icon: IllustrationIconJson, |  | ||||||
|     exampleValue: { key: 'value' }, |  | ||||||
|  |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.RichText]: { |  | ||||||
|     label: 'System', |  | ||||||
|     Icon: IllustrationIconSetting, |  | ||||||
|     exampleValue: { key: 'value' }, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Actor]: { |  | ||||||
|     label: 'System', |  | ||||||
|     Icon: IllustrationIconSetting, |  | ||||||
|     category: 'Basic', |  | ||||||
|   }, |  | ||||||
|   [FieldMetadataType.Array]: { |  | ||||||
|     label: 'Array', |  | ||||||
|     Icon: IllustrationIconArray, |  | ||||||
|     category: 'Basic', |  | ||||||
|     exampleValue: ['value1', 'value2'], |  | ||||||
|   }, |  | ||||||
| } as const satisfies Record< |  | ||||||
|   SettingsSupportedFieldType, |  | ||||||
|   SettingsFieldTypeConfig |  | ||||||
| >; |  | ||||||
|   | |||||||
| @@ -0,0 +1,152 @@ | |||||||
|  | import { | ||||||
|  |   IconComponent, | ||||||
|  |   IllustrationIconArray, | ||||||
|  |   IllustrationIconCalendarEvent, | ||||||
|  |   IllustrationIconCalendarTime, | ||||||
|  |   IllustrationIconJson, | ||||||
|  |   IllustrationIconMail, | ||||||
|  |   IllustrationIconNumbers, | ||||||
|  |   IllustrationIconOneToMany, | ||||||
|  |   IllustrationIconPhone, | ||||||
|  |   IllustrationIconSetting, | ||||||
|  |   IllustrationIconStar, | ||||||
|  |   IllustrationIconTag, | ||||||
|  |   IllustrationIconTags, | ||||||
|  |   IllustrationIconText, | ||||||
|  |   IllustrationIconToggle, | ||||||
|  |   IllustrationIconUid, | ||||||
|  | } from 'twenty-ui'; | ||||||
|  |  | ||||||
|  | import { | ||||||
|  |   FieldArrayValue, | ||||||
|  |   FieldBooleanValue, | ||||||
|  |   FieldDateTimeValue, | ||||||
|  |   FieldDateValue, | ||||||
|  |   FieldEmailValue, | ||||||
|  |   FieldJsonValue, | ||||||
|  |   FieldMultiSelectValue, | ||||||
|  |   FieldNumberValue, | ||||||
|  |   FieldPhoneValue, | ||||||
|  |   FieldRatingValue, | ||||||
|  |   FieldRelationValue, | ||||||
|  |   FieldRichTextValue, | ||||||
|  |   FieldSelectValue, | ||||||
|  |   FieldTextValue, | ||||||
|  |   FieldUUidValue, | ||||||
|  | } from '@/object-record/record-field/types/FieldMetadata'; | ||||||
|  | import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue'; | ||||||
|  | import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType'; | ||||||
|  | import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType'; | ||||||
|  | import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||||
|  |  | ||||||
|  | DEFAULT_DATE_VALUE.setFullYear(DEFAULT_DATE_VALUE.getFullYear() + 2); | ||||||
|  |  | ||||||
|  | export type SettingsFieldTypeConfig<T> = { | ||||||
|  |   label: string; | ||||||
|  |   Icon: IconComponent; | ||||||
|  |   exampleValue?: T; | ||||||
|  |   category: SettingsFieldTypeCategoryType; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | type SettingsNonCompositeFieldTypeConfigArray = Record< | ||||||
|  |   SettingsNonCompositeFieldType, | ||||||
|  |   SettingsFieldTypeConfig<any> | ||||||
|  | >; | ||||||
|  |  | ||||||
|  | // TODO: can we derive this from backend definitions ? | ||||||
|  | export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFieldTypeConfigArray = | ||||||
|  |   { | ||||||
|  |     [FieldMetadataType.Uuid]: { | ||||||
|  |       label: 'Unique ID', | ||||||
|  |       Icon: IllustrationIconUid, | ||||||
|  |       exampleValue: '00000000-0000-0000-0000-000000000000', | ||||||
|  |       category: 'Advanced', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldUUidValue>, | ||||||
|  |     [FieldMetadataType.Text]: { | ||||||
|  |       label: 'Text', | ||||||
|  |       Icon: IllustrationIconText, | ||||||
|  |       exampleValue: | ||||||
|  |         'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.', | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldTextValue>, | ||||||
|  |     [FieldMetadataType.Numeric]: { | ||||||
|  |       label: 'Numeric', | ||||||
|  |       Icon: IllustrationIconNumbers, | ||||||
|  |       exampleValue: 2000, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldNumberValue>, | ||||||
|  |     [FieldMetadataType.Number]: { | ||||||
|  |       label: 'Number', | ||||||
|  |       Icon: IllustrationIconNumbers, | ||||||
|  |       exampleValue: 2000, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldNumberValue>, | ||||||
|  |     [FieldMetadataType.Boolean]: { | ||||||
|  |       label: 'True/False', | ||||||
|  |       Icon: IllustrationIconToggle, | ||||||
|  |       exampleValue: true, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldBooleanValue>, | ||||||
|  |     [FieldMetadataType.DateTime]: { | ||||||
|  |       label: 'Date and Time', | ||||||
|  |       Icon: IllustrationIconCalendarTime, | ||||||
|  |       exampleValue: DEFAULT_DATE_VALUE.toISOString(), | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldDateTimeValue>, | ||||||
|  |     [FieldMetadataType.Date]: { | ||||||
|  |       label: 'Date', | ||||||
|  |       Icon: IllustrationIconCalendarEvent, | ||||||
|  |       exampleValue: DEFAULT_DATE_VALUE.toISOString(), | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldDateValue>, | ||||||
|  |     [FieldMetadataType.Select]: { | ||||||
|  |       label: 'Select', | ||||||
|  |       Icon: IllustrationIconTag, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldSelectValue>, | ||||||
|  |     [FieldMetadataType.MultiSelect]: { | ||||||
|  |       label: 'Multi-select', | ||||||
|  |       Icon: IllustrationIconTags, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldMultiSelectValue>, | ||||||
|  |     [FieldMetadataType.Relation]: { | ||||||
|  |       label: 'Relation', | ||||||
|  |       Icon: IllustrationIconOneToMany, | ||||||
|  |       category: 'Relation', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldRelationValue<any>>, | ||||||
|  |     [FieldMetadataType.Email]: { | ||||||
|  |       label: 'Email', | ||||||
|  |       Icon: IllustrationIconMail, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldEmailValue>, | ||||||
|  |     [FieldMetadataType.Phone]: { | ||||||
|  |       label: 'Phone', | ||||||
|  |       Icon: IllustrationIconPhone, | ||||||
|  |       exampleValue: '+1234-567-890', | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldPhoneValue>, | ||||||
|  |     [FieldMetadataType.Rating]: { | ||||||
|  |       label: 'Rating', | ||||||
|  |       Icon: IllustrationIconStar, | ||||||
|  |       exampleValue: 'RATING_3', | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldRatingValue>, | ||||||
|  |     [FieldMetadataType.RawJson]: { | ||||||
|  |       label: 'JSON', | ||||||
|  |       Icon: IllustrationIconJson, | ||||||
|  |       exampleValue: { key: 'value' }, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldJsonValue>, | ||||||
|  |     [FieldMetadataType.RichText]: { | ||||||
|  |       label: 'Rich Text', | ||||||
|  |       Icon: IllustrationIconSetting, | ||||||
|  |       exampleValue: { key: 'value' }, | ||||||
|  |       category: 'Basic', | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldRichTextValue>, | ||||||
|  |     [FieldMetadataType.Array]: { | ||||||
|  |       label: 'Array', | ||||||
|  |       Icon: IllustrationIconArray, | ||||||
|  |       category: 'Basic', | ||||||
|  |       exampleValue: ['value1', 'value2'], | ||||||
|  |     } as const satisfies SettingsFieldTypeConfig<FieldArrayValue>, | ||||||
|  |   }; | ||||||
| @@ -2,14 +2,13 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | |||||||
| import { SettingsCard } from '@/settings/components/SettingsCard'; | import { SettingsCard } from '@/settings/components/SettingsCard'; | ||||||
| import { SETTINGS_FIELD_TYPE_CATEGORIES } from '@/settings/data-model/constants/SettingsFieldTypeCategories'; | import { SETTINGS_FIELD_TYPE_CATEGORIES } from '@/settings/data-model/constants/SettingsFieldTypeCategories'; | ||||||
| import { SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS } from '@/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions'; | import { SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS } from '@/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions'; | ||||||
| import { | import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | ||||||
|   SETTINGS_FIELD_TYPE_CONFIGS, | import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; | ||||||
|   SettingsFieldTypeConfig, |  | ||||||
| } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; |  | ||||||
| import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues'; | import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues'; | ||||||
| import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; | import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; | ||||||
| import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues'; | import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues'; | ||||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
| import { TextInput } from '@/ui/input/components/TextInput'; | import { TextInput } from '@/ui/input/components/TextInput'; | ||||||
| import { useTheme } from '@emotion/react'; | import { useTheme } from '@emotion/react'; | ||||||
| import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||||
| @@ -23,8 +22,8 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; | |||||||
| export const settingsDataModelFieldTypeFormSchema = z.object({ | export const settingsDataModelFieldTypeFormSchema = z.object({ | ||||||
|   type: z.enum( |   type: z.enum( | ||||||
|     Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [ |     Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [ | ||||||
|       SettingsSupportedFieldType, |       SettingsFieldType, | ||||||
|       ...SettingsSupportedFieldType[], |       ...SettingsFieldType[], | ||||||
|     ], |     ], | ||||||
|   ), |   ), | ||||||
| }); | }); | ||||||
| @@ -35,7 +34,7 @@ export type SettingsDataModelFieldTypeFormValues = z.infer< | |||||||
|  |  | ||||||
| type SettingsDataModelFieldTypeSelectProps = { | type SettingsDataModelFieldTypeSelectProps = { | ||||||
|   className?: string; |   className?: string; | ||||||
|   excludedFieldTypes?: SettingsSupportedFieldType[]; |   excludedFieldTypes?: SettingsFieldType[]; | ||||||
|   fieldMetadataItem?: Pick< |   fieldMetadataItem?: Pick< | ||||||
|     FieldMetadataItem, |     FieldMetadataItem, | ||||||
|     'defaultValue' | 'options' | 'type' |     'defaultValue' | 'options' | 'type' | ||||||
| @@ -78,11 +77,11 @@ export const SettingsDataModelFieldTypeSelect = ({ | |||||||
|   const theme = useTheme(); |   const theme = useTheme(); | ||||||
|   const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>(); |   const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>(); | ||||||
|   const [searchQuery, setSearchQuery] = useState(''); |   const [searchQuery, setSearchQuery] = useState(''); | ||||||
|   const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig>( |   const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig<any>>( | ||||||
|     SETTINGS_FIELD_TYPE_CONFIGS, |     SETTINGS_FIELD_TYPE_CONFIGS, | ||||||
|   ).filter( |   ).filter( | ||||||
|     ([key, config]) => |     ([key, config]) => | ||||||
|       !excludedFieldTypes.includes(key as SettingsSupportedFieldType) && |       !excludedFieldTypes.includes(key as SettingsFieldType) && | ||||||
|       config.label.toLowerCase().includes(searchQuery.toLowerCase()), |       config.label.toLowerCase().includes(searchQuery.toLowerCase()), | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -95,7 +94,7 @@ export const SettingsDataModelFieldTypeSelect = ({ | |||||||
|   const { resetDefaultValueField: resetSelectDefaultValueField } = |   const { resetDefaultValueField: resetSelectDefaultValueField } = | ||||||
|     useSelectSettingsFormInitialValues({ fieldMetadataItem }); |     useSelectSettingsFormInitialValues({ fieldMetadataItem }); | ||||||
|  |  | ||||||
|   const resetDefaultValueField = (nextValue: SettingsSupportedFieldType) => { |   const resetDefaultValueField = (nextValue: SettingsFieldType) => { | ||||||
|     switch (nextValue) { |     switch (nextValue) { | ||||||
|       case FieldMetadataType.Boolean: |       case FieldMetadataType.Boolean: | ||||||
|         resetBooleanDefaultValueField(); |         resetBooleanDefaultValueField(); | ||||||
| @@ -118,7 +117,7 @@ export const SettingsDataModelFieldTypeSelect = ({ | |||||||
|       control={control} |       control={control} | ||||||
|       defaultValue={ |       defaultValue={ | ||||||
|         fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs |         fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs | ||||||
|           ? (fieldMetadataItem.type as SettingsSupportedFieldType) |           ? (fieldMetadataItem.type as SettingsFieldType) | ||||||
|           : FieldMetadataType.Text |           : FieldMetadataType.Text | ||||||
|       } |       } | ||||||
|       render={({ field: { onChange } }) => ( |       render={({ field: { onChange } }) => ( | ||||||
| @@ -147,10 +146,8 @@ export const SettingsDataModelFieldTypeSelect = ({ | |||||||
|                       <SettingsCard |                       <SettingsCard | ||||||
|                         key={key} |                         key={key} | ||||||
|                         onClick={() => { |                         onClick={() => { | ||||||
|                           onChange(key as SettingsSupportedFieldType); |                           onChange(key as SettingsFieldType); | ||||||
|                           resetDefaultValueField( |                           resetDefaultValueField(key as SettingsFieldType); | ||||||
|                             key as SettingsSupportedFieldType, |  | ||||||
|                           ); |  | ||||||
|                           onFieldTypeSelect(); |                           onFieldTypeSelect(); | ||||||
|                         }} |                         }} | ||||||
|                         Icon={ |                         Icon={ | ||||||
|   | |||||||
| @@ -16,9 +16,11 @@ export const getCurrencyFieldPreviewValue = ({ | |||||||
| }): FieldCurrencyValue | null => { | }): FieldCurrencyValue | null => { | ||||||
|   if (fieldMetadataItem.type !== FieldMetadataType.Currency) return null; |   if (fieldMetadataItem.type !== FieldMetadataType.Currency) return null; | ||||||
|  |  | ||||||
|   const placeholderDefaultValue = getSettingsFieldTypeConfig( |   const currencyFieldTypeConfig = getSettingsFieldTypeConfig( | ||||||
|     FieldMetadataType.Currency, |     FieldMetadataType.Currency, | ||||||
|   ).exampleValue; |   ); | ||||||
|  |  | ||||||
|  |   const placeholderDefaultValue = currencyFieldTypeConfig.exampleValue; | ||||||
|  |  | ||||||
|   return currencyFieldDefaultValueSchema |   return currencyFieldDefaultValueSchema | ||||||
|     .transform((value) => ({ |     .transform((value) => ({ | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; | |||||||
| import { Link } from 'react-router-dom'; | import { Link } from 'react-router-dom'; | ||||||
| import { IconComponent, IconTwentyStar } from 'twenty-ui'; | import { IconComponent, IconTwentyStar } from 'twenty-ui'; | ||||||
|  |  | ||||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
| import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; | import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; | ||||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||||
|  |  | ||||||
| @@ -11,11 +11,11 @@ type SettingsObjectFieldDataTypeProps = { | |||||||
|   to?: string; |   to?: string; | ||||||
|   Icon?: IconComponent; |   Icon?: IconComponent; | ||||||
|   label?: string; |   label?: string; | ||||||
|   value: SettingsSupportedFieldType; |   value: SettingsFieldType; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const StyledDataType = styled.div<{ | const StyledDataType = styled.div<{ | ||||||
|   value: SettingsSupportedFieldType; |   value: SettingsFieldType; | ||||||
|   to?: string; |   to?: string; | ||||||
| }>` | }>` | ||||||
|   align-items: center; |   align-items: center; | ||||||
|   | |||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
|  | import { PickLiteral } from '~/types/PickLiteral'; | ||||||
|  |  | ||||||
|  | // TODO: add to future fullstack shared package | ||||||
|  | export const COMPOSITE_FIELD_TYPES = [ | ||||||
|  |   'CURRENCY', | ||||||
|  |   'EMAILS', | ||||||
|  |   'LINK', | ||||||
|  |   'LINKS', | ||||||
|  |   'ADDRESS', | ||||||
|  |   'PHONES', | ||||||
|  |   'FULL_NAME', | ||||||
|  |   'ACTOR', | ||||||
|  | ] as const; | ||||||
|  |  | ||||||
|  | type CompositeFieldTypeBaseLiteral = (typeof COMPOSITE_FIELD_TYPES)[number]; | ||||||
|  |  | ||||||
|  | export type CompositeFieldType = PickLiteral< | ||||||
|  |   FieldType, | ||||||
|  |   CompositeFieldTypeBaseLiteral | ||||||
|  | >; | ||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||||
|  |  | ||||||
|  | export type FieldType = `${FieldMetadataType}`; | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; | ||||||
|  | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
|  | import { ExcludeLiteral } from '~/types/ExcludeLiteral'; | ||||||
|  |  | ||||||
|  | export type NonCompositeFieldType = ExcludeLiteral< | ||||||
|  |   FieldType, | ||||||
|  |   CompositeFieldType | ||||||
|  | >; | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; | ||||||
|  | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
|  | import { PickLiteral } from '~/types/PickLiteral'; | ||||||
|  |  | ||||||
|  | export type SettingsCompositeFieldType = PickLiteral< | ||||||
|  |   SettingsFieldType, | ||||||
|  |   CompositeFieldType | ||||||
|  | >; | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
|  | import { PickLiteral } from '~/types/PickLiteral'; | ||||||
|  |  | ||||||
|  | export type SettingsExcludedFieldType = PickLiteral< | ||||||
|  |   FieldType, | ||||||
|  |   'POSITION' | 'TS_VECTOR' | ||||||
|  | >; | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
|  | import { SettingsExcludedFieldType } from '@/settings/data-model/types/SettingsExcludedFieldType'; | ||||||
|  | import { ExcludeLiteral } from '~/types/ExcludeLiteral'; | ||||||
|  |  | ||||||
|  | export type SettingsFieldType = ExcludeLiteral< | ||||||
|  |   FieldType, | ||||||
|  |   SettingsExcludedFieldType | ||||||
|  | >; | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | import { NonCompositeFieldType } from '@/settings/data-model/types/NonCompositeFieldType'; | ||||||
|  | import { SettingsExcludedFieldType } from '@/settings/data-model/types/SettingsExcludedFieldType'; | ||||||
|  | import { ExcludeLiteral } from '~/types/ExcludeLiteral'; | ||||||
|  |  | ||||||
|  | export type SettingsNonCompositeFieldType = ExcludeLiteral< | ||||||
|  |   NonCompositeFieldType, | ||||||
|  |   SettingsExcludedFieldType | ||||||
|  | >; | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; |  | ||||||
|  |  | ||||||
| export type SettingsSupportedFieldType = Exclude< |  | ||||||
|   FieldMetadataType, |  | ||||||
|   FieldMetadataType.Position | FieldMetadataType.TsVector |  | ||||||
| >; |  | ||||||
| @@ -1,13 +1,6 @@ | |||||||
| import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | ||||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
| import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; |  | ||||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; |  | ||||||
|  |  | ||||||
| export const getSettingsFieldTypeConfig = <T extends FieldMetadataType>( | export const getSettingsFieldTypeConfig = (fieldType: SettingsFieldType) => { | ||||||
|   fieldType: T, |   return SETTINGS_FIELD_TYPE_CONFIGS[fieldType as SettingsFieldType]; | ||||||
| ) => | }; | ||||||
|   (isFieldTypeSupportedInSettings(fieldType) |  | ||||||
|     ? SETTINGS_FIELD_TYPE_CONFIGS[fieldType] |  | ||||||
|     : undefined) as T extends SettingsSupportedFieldType |  | ||||||
|     ? (typeof SETTINGS_FIELD_TYPE_CONFIGS)[T] |  | ||||||
|     : undefined; |  | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | ||||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
|  |  | ||||||
| export const isFieldTypeSupportedInSettings = ( | export const isFieldTypeSupportedInSettings = ( | ||||||
|   fieldType: FieldMetadataType, |   fieldType: FieldType, | ||||||
| ): fieldType is SettingsSupportedFieldType => | ): fieldType is SettingsFieldType => fieldType in SETTINGS_FIELD_TYPE_CONFIGS; | ||||||
|   fieldType in SETTINGS_FIELD_TYPE_CONFIGS; |  | ||||||
|   | |||||||
| @@ -106,15 +106,18 @@ export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => { | |||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         set( |         const newValue = [ | ||||||
|           unsavedToUpsertViewFiltersCallbackState({ viewId: currentViewId }), |  | ||||||
|           [ |  | ||||||
|           ...unsavedToUpsertViewFilters, |           ...unsavedToUpsertViewFilters, | ||||||
|           { |           { | ||||||
|             ...upsertedFilter, |             ...upsertedFilter, | ||||||
|  |             id: upsertedFilter.id, | ||||||
|             __typename: 'ViewFilter', |             __typename: 'ViewFilter', | ||||||
|           } satisfies ViewFilter, |           } satisfies ViewFilter, | ||||||
|           ], |         ] satisfies ViewFilter[]; | ||||||
|  |  | ||||||
|  |         set( | ||||||
|  |           unsavedToUpsertViewFiltersCallbackState({ viewId: currentViewId }), | ||||||
|  |           newValue, | ||||||
|         ); |         ); | ||||||
|       }, |       }, | ||||||
|     [ |     [ | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ const baseDefinition = { | |||||||
|   fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482', |   fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482', | ||||||
|   label: 'label', |   label: 'label', | ||||||
|   iconName: 'iconName', |   iconName: 'iconName', | ||||||
|  |   fieldName: 'fieldName', | ||||||
| }; | }; | ||||||
|  |  | ||||||
| describe('mapViewSortsToSorts', () => { | describe('mapViewSortsToSorts', () => { | ||||||
|   | |||||||
| @@ -1,17 +0,0 @@ | |||||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; |  | ||||||
| import { hasSubMenuFilter } from '@/object-record/object-filter-dropdown/utils/hasSubMenuFilter'; |  | ||||||
| import { ViewFilter } from '../types/ViewFilter'; |  | ||||||
|  |  | ||||||
| export const getFilterDefinitionForViewFilter = ( |  | ||||||
|   viewFilter: ViewFilter, |  | ||||||
|   availableFilterDefinition: FilterDefinition, |  | ||||||
| ): FilterDefinition => { |  | ||||||
|   return { |  | ||||||
|     ...availableFilterDefinition, |  | ||||||
|     subFieldType: |  | ||||||
|       hasSubMenuFilter(availableFilterDefinition.type) && |  | ||||||
|       viewFilter.definition?.type !== availableFilterDefinition.type |  | ||||||
|         ? viewFilter.definition?.type |  | ||||||
|         : undefined, |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| @@ -2,7 +2,6 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | |||||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||||
| import { isDefined } from '~/utils/isDefined'; | import { isDefined } from '~/utils/isDefined'; | ||||||
|  |  | ||||||
| import { getFilterDefinitionForViewFilter } from '@/views/utils/getFilterDefinitionForViewFilter'; |  | ||||||
| import { ViewFilter } from '../types/ViewFilter'; | import { ViewFilter } from '../types/ViewFilter'; | ||||||
|  |  | ||||||
| export const mapViewFiltersToFilters = ( | export const mapViewFiltersToFilters = ( | ||||||
| @@ -24,10 +23,7 @@ export const mapViewFiltersToFilters = ( | |||||||
|         value: viewFilter.value, |         value: viewFilter.value, | ||||||
|         displayValue: viewFilter.displayValue, |         displayValue: viewFilter.displayValue, | ||||||
|         operand: viewFilter.operand, |         operand: viewFilter.operand, | ||||||
|         definition: getFilterDefinitionForViewFilter( |         definition: viewFilter.definition ?? availableFilterDefinition, | ||||||
|           viewFilter, |  | ||||||
|           availableFilterDefinition, |  | ||||||
|         ), |  | ||||||
|       }; |       }; | ||||||
|     }) |     }) | ||||||
|     .filter(isDefined); |     .filter(isDefined); | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; | ||||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; | import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; | ||||||
| import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | ||||||
| import { resolveNumberViewFilterValue } from '@/views/utils/view-filter-value/resolveNumberViewFilterValue'; | import { resolveNumberViewFilterValue } from '@/views/utils/view-filter-value/resolveNumberViewFilterValue'; | ||||||
| import { | import { | ||||||
| @@ -8,7 +8,7 @@ import { | |||||||
| } from './resolveDateViewFilterValue'; | } from './resolveDateViewFilterValue'; | ||||||
|  |  | ||||||
| type ResolvedFilterValue< | type ResolvedFilterValue< | ||||||
|   T extends FilterType, |   T extends FilterableFieldType, | ||||||
|   O extends ViewFilterOperand, |   O extends ViewFilterOperand, | ||||||
| > = T extends 'DATE' | 'DATE_TIME' | > = T extends 'DATE' | 'DATE_TIME' | ||||||
|   ? ResolvedDateViewFilterValue<O> |   ? ResolvedDateViewFilterValue<O> | ||||||
| @@ -16,16 +16,16 @@ type ResolvedFilterValue< | |||||||
|     ? ReturnType<typeof resolveNumberViewFilterValue> |     ? ReturnType<typeof resolveNumberViewFilterValue> | ||||||
|     : string; |     : string; | ||||||
|  |  | ||||||
| type PartialFilter<T extends FilterType, O extends ViewFilterOperand> = Pick< | type PartialFilter< | ||||||
|   Filter, |   T extends FilterableFieldType, | ||||||
|   'value' |   O extends ViewFilterOperand, | ||||||
| > & { | > = Pick<Filter, 'value'> & { | ||||||
|   definition: { type: T }; |   definition: { type: T }; | ||||||
|   operand: O; |   operand: O; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const resolveFilterValue = < | export const resolveFilterValue = < | ||||||
|   T extends FilterType, |   T extends FilterableFieldType, | ||||||
|   O extends ViewFilterOperand, |   O extends ViewFilterOperand, | ||||||
| >( | >( | ||||||
|   filter: PartialFilter<T, O>, |   filter: PartialFilter<T, O>, | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fie | |||||||
| import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm'; | import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm'; | ||||||
| import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; | import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; | ||||||
| import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; | import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; | ||||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
| import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; | import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; | ||||||
| import { AppPath } from '@/types/AppPath'; | import { AppPath } from '@/types/AppPath'; | ||||||
| import { SettingsPath } from '@/types/SettingsPath'; | import { SettingsPath } from '@/types/SettingsPath'; | ||||||
| @@ -42,9 +42,11 @@ import { Section } from '@/ui/layout/section/components/Section'; | |||||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||||
| import { isDefined } from '~/utils/isDefined'; | import { isDefined } from '~/utils/isDefined'; | ||||||
|  |  | ||||||
|  | //TODO: fix this type | ||||||
| type SettingsDataModelFieldEditFormValues = z.infer< | type SettingsDataModelFieldEditFormValues = z.infer< | ||||||
|   ReturnType<typeof settingsFieldFormSchema> |   ReturnType<typeof settingsFieldFormSchema> | ||||||
| >; | > & | ||||||
|  |   any; | ||||||
|  |  | ||||||
| const canPersistFieldMetadataItemUpdate = ( | const canPersistFieldMetadataItemUpdate = ( | ||||||
|   fieldMetadataItem: FieldMetadataItem, |   fieldMetadataItem: FieldMetadataItem, | ||||||
| @@ -94,7 +96,7 @@ export const SettingsObjectFieldEdit = () => { | |||||||
|     resolver: zodResolver(settingsFieldFormSchema()), |     resolver: zodResolver(settingsFieldFormSchema()), | ||||||
|     values: { |     values: { | ||||||
|       icon: fieldMetadataItem?.icon ?? 'Icon', |       icon: fieldMetadataItem?.icon ?? 'Icon', | ||||||
|       type: fieldMetadataItem?.type as SettingsSupportedFieldType, |       type: fieldMetadataItem?.type as SettingsFieldType, | ||||||
|       label: fieldMetadataItem?.label ?? '', |       label: fieldMetadataItem?.label ?? '', | ||||||
|       description: fieldMetadataItem?.description, |       description: fieldMetadataItem?.description, | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/field | |||||||
| import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; | import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; | ||||||
| import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect'; | import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect'; | ||||||
| import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; | import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; | ||||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
| import { AppPath } from '@/types/AppPath'; | import { AppPath } from '@/types/AppPath'; | ||||||
| import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; | import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; | ||||||
| import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; | import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; | ||||||
| @@ -34,9 +34,11 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; | |||||||
| import { isDefined } from '~/utils/isDefined'; | import { isDefined } from '~/utils/isDefined'; | ||||||
| import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; | import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; | ||||||
|  |  | ||||||
|  | // TODO: fix this type | ||||||
| type SettingsDataModelNewFieldFormValues = z.infer< | type SettingsDataModelNewFieldFormValues = z.infer< | ||||||
|   ReturnType<typeof settingsFieldFormSchema> |   ReturnType<typeof settingsFieldFormSchema> | ||||||
| >; | > & | ||||||
|  |   any; | ||||||
|  |  | ||||||
| const StyledH1Title = styled(H1Title)` | const StyledH1Title = styled(H1Title)` | ||||||
|   margin-bottom: 0; |   margin-bottom: 0; | ||||||
| @@ -46,7 +48,7 @@ export const SettingsObjectNewFieldStep2 = () => { | |||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|   const { objectSlug = '' } = useParams(); |   const { objectSlug = '' } = useParams(); | ||||||
|   const [searchParams] = useSearchParams(); |   const [searchParams] = useSearchParams(); | ||||||
|   const fieldType = searchParams.get('fieldType') as SettingsSupportedFieldType; |   const fieldType = searchParams.get('fieldType') as SettingsFieldType; | ||||||
|   const { enqueueSnackBar } = useSnackBar(); |   const { enqueueSnackBar } = useSnackBar(); | ||||||
|  |  | ||||||
|   const [isConfigureStep, setIsConfigureStep] = useState(false); |   const [isConfigureStep, setIsConfigureStep] = useState(false); | ||||||
| @@ -159,7 +161,7 @@ export const SettingsObjectNewFieldStep2 = () => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const excludedFieldTypes: SettingsSupportedFieldType[] = ( |   const excludedFieldTypes: SettingsFieldType[] = ( | ||||||
|     [ |     [ | ||||||
|       FieldMetadataType.Link, |       FieldMetadataType.Link, | ||||||
|       FieldMetadataType.Numeric, |       FieldMetadataType.Numeric, | ||||||
| @@ -226,7 +228,7 @@ export const SettingsObjectNewFieldStep2 = () => { | |||||||
|               <SettingsDataModelFieldTypeSelect |               <SettingsDataModelFieldTypeSelect | ||||||
|                 excludedFieldTypes={excludedFieldTypes} |                 excludedFieldTypes={excludedFieldTypes} | ||||||
|                 fieldMetadataItem={{ |                 fieldMetadataItem={{ | ||||||
|                   type: fieldType, |                   type: fieldType as FieldMetadataType, | ||||||
|                 }} |                 }} | ||||||
|                 onFieldTypeSelect={() => setIsConfigureStep(true)} |                 onFieldTypeSelect={() => setIsConfigureStep(true)} | ||||||
|               /> |               /> | ||||||
|   | |||||||
| @@ -1,8 +1,11 @@ | |||||||
| import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; | import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; | ||||||
| import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | ||||||
| import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; | ||||||
|  | import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||||
|  | import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||||
| import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType'; | import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType'; | ||||||
| import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; | import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; | ||||||
|  | import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; | ||||||
| import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem'; | import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem'; | ||||||
| import { getSettingsObjectFieldType } from '~/pages/settings/data-model/utils/getSettingsObjectFieldType'; | import { getSettingsObjectFieldType } from '~/pages/settings/data-model/utils/getSettingsObjectFieldType'; | ||||||
|  |  | ||||||
| @@ -29,13 +32,17 @@ export const useMapFieldMetadataItemToSettingsObjectDetailTableItem = ( | |||||||
|       objectMetadataItem, |       objectMetadataItem, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     const fieldMetadataType = fieldMetadataItem.type as FieldType; | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       fieldMetadataItem, |       fieldMetadataItem, | ||||||
|       fieldType: fieldType ?? '', |       fieldType: fieldType ?? '', | ||||||
|       dataType: |       dataType: | ||||||
|         relationObjectMetadataItem?.labelPlural ?? |         (relationObjectMetadataItem?.labelPlural ?? | ||||||
|         getSettingsFieldTypeConfig(fieldMetadataItem.type)?.label ?? |         isFieldTypeSupportedInSettings(fieldMetadataType)) | ||||||
|         '', |           ? getSettingsFieldTypeConfig(fieldMetadataType as SettingsFieldType) | ||||||
|  |               ?.label | ||||||
|  |           : '', | ||||||
|       label: fieldMetadataItem.label, |       label: fieldMetadataItem.label, | ||||||
|       identifierType: identifierType, |       identifierType: identifierType, | ||||||
|       objectMetadataItem, |       objectMetadataItem, | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								packages/twenty-front/src/types/ExcludeLiteral.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/twenty-front/src/types/ExcludeLiteral.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export type ExcludeLiteral<T, U extends T> = T extends U ? never : T; | ||||||
							
								
								
									
										1
									
								
								packages/twenty-front/src/types/PickLiteral.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								packages/twenty-front/src/types/PickLiteral.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export type PickLiteral<T, U extends T> = U; | ||||||
| @@ -18,6 +18,7 @@ import { | |||||||
|   WorkspaceMigrationExceptionCode, |   WorkspaceMigrationExceptionCode, | ||||||
| } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; | } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; | ||||||
|  |  | ||||||
|  | // TODO: could we export this to GraphQL ? | ||||||
| export type CompositeFieldMetadataType = | export type CompositeFieldMetadataType = | ||||||
|   | FieldMetadataType.ADDRESS |   | FieldMetadataType.ADDRESS | ||||||
|   | FieldMetadataType.CURRENCY |   | FieldMetadataType.CURRENCY | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user