mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 04:37:56 +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 { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect'; | ||||
| 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 styled from '@emotion/styled'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| @@ -98,9 +99,10 @@ export const MultipleFiltersDropdownContent = ({ | ||||
|                 'ACTOR', | ||||
|                 'ARRAY', | ||||
|                 'PHONES', | ||||
|               ].includes(filterDefinitionUsedInDropdown.type) && ( | ||||
|                 <ObjectFilterDropdownTextSearchInput /> | ||||
|               )} | ||||
|               ].includes(filterDefinitionUsedInDropdown.type) && | ||||
|                 !isActorSourceCompositeFilter( | ||||
|                   filterDefinitionUsedInDropdown, | ||||
|                 ) && <ObjectFilterDropdownTextSearchInput />} | ||||
|               {['NUMBER', 'CURRENCY'].includes( | ||||
|                 filterDefinitionUsedInDropdown.type, | ||||
|               ) && <ObjectFilterDropdownNumberInput />} | ||||
| @@ -116,7 +118,7 @@ export const MultipleFiltersDropdownContent = ({ | ||||
|                   <ObjectFilterDropdownRecordSelect /> | ||||
|                 </> | ||||
|               )} | ||||
|               {filterDefinitionUsedInDropdown.type === 'SOURCE' && ( | ||||
|               {isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && ( | ||||
|                 <> | ||||
|                   <DropdownMenuSeparator /> | ||||
|                   <ObjectFilterDropdownSourceSelect /> | ||||
|   | ||||
| @@ -1,16 +1,27 @@ | ||||
| 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 { ObjectFilterSelectSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterSelectSubMenu'; | ||||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||||
|  | ||||
| import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu'; | ||||
| 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 { 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 { 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 { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; | ||||
| import { useRecoilState } from 'recoil'; | ||||
| import { isDefined } from 'twenty-ui'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { isDefined, useIcons } from 'twenty-ui'; | ||||
| import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; | ||||
|  | ||||
| export const StyledInput = styled.input` | ||||
|   background: transparent; | ||||
| @@ -39,19 +50,33 @@ export const StyledInput = styled.input` | ||||
| `; | ||||
|  | ||||
| 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( | ||||
|     availableFilterDefinitionsComponentState, | ||||
|   ); | ||||
|  | ||||
|   const [currentSubMenu, setCurrentSubMenu] = | ||||
|     useRecoilState(currentSubMenuState); | ||||
|  | ||||
|   const sortedAvailableFilterDefinitions = [...availableFilterDefinitions] | ||||
|     .sort((a, b) => a.label.localeCompare(b.label)) | ||||
|     .filter((item) => | ||||
|       item.label.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()), | ||||
|       item.label | ||||
|         .toLocaleLowerCase() | ||||
|         .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), | ||||
|     ); | ||||
|  | ||||
|   const selectableListItemIds = sortedAvailableFilterDefinitions.map( | ||||
| @@ -76,21 +101,96 @@ export const ObjectFilterDropdownFilterSelect = () => { | ||||
|     selectFilter({ filterDefinition: selectedFilterDefinition }); | ||||
|   }; | ||||
|  | ||||
|   useEffect(() => { | ||||
|     return () => { | ||||
|       setCurrentSubMenu(null); | ||||
|     }; | ||||
|   }, [setCurrentSubMenu]); | ||||
|   const setHotkeyScope = useSetHotkeyScope(); | ||||
|   const { getIcon } = useIcons(); | ||||
|  | ||||
|   return !currentSubMenu ? ( | ||||
|     <ObjectFilterSelectMenu | ||||
|       searchText={searchText} | ||||
|       setSearchText={setSearchText} | ||||
|       sortedAvailableFilterDefinitions={sortedAvailableFilterDefinitions} | ||||
|       selectableListItemIds={selectableListItemIds} | ||||
|       handleEnter={handleEnter} | ||||
|     /> | ||||
|   ) : ( | ||||
|     <ObjectFilterSelectSubMenu /> | ||||
|   const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => { | ||||
|     setFilterDefinitionUsedInDropdown(availableFilterDefinition); | ||||
|  | ||||
|     if ( | ||||
|       availableFilterDefinition.type === 'RELATION' || | ||||
|       availableFilterDefinition.type === 'SELECT' | ||||
|     ) { | ||||
|       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> | ||||
|         </> | ||||
|       ) : ( | ||||
|         <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 { 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 { hasSubMenuFilter } from '@/object-record/object-filter-dropdown/utils/hasSubMenuFilter'; | ||||
| import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; | ||||
| import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; | ||||
| import { useRecoilValue, useSetRecoilState } from 'recoil'; | ||||
| import { useRecoilValue } from 'recoil'; | ||||
| import { useIcons } from 'twenty-ui'; | ||||
|  | ||||
| export type ObjectFilterDropdownFilterSelectMenuItemProps = { | ||||
| @@ -28,24 +24,12 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ | ||||
|     isSelectedItemIdSelector(filterDefinition.fieldMetadataId), | ||||
|   ); | ||||
|  | ||||
|   const hasSubMenu = hasSubMenuFilter(filterDefinition.type); | ||||
|  | ||||
|   const { getIcon } = useIcons(); | ||||
|  | ||||
|   const setCurrentSubMenu = useSetRecoilState(currentSubMenuState); | ||||
|   const setCurrentParentFilterDefinition = useSetRecoilState( | ||||
|     currentParentFilterDefinitionState, | ||||
|   ); | ||||
|  | ||||
|   const handleClick = () => { | ||||
|     resetSelectedItem(); | ||||
|  | ||||
|     if (hasSubMenu) { | ||||
|       setCurrentSubMenu(filterDefinition.type); | ||||
|       setCurrentParentFilterDefinition(filterDefinition); | ||||
|     } else { | ||||
|       selectFilter({ filterDefinition }); | ||||
|     } | ||||
|     selectFilter({ filterDefinition }); | ||||
|   }; | ||||
|  | ||||
|   return ( | ||||
| @@ -55,7 +39,6 @@ export const ObjectFilterDropdownFilterSelectMenuItem = ({ | ||||
|       onClick={handleClick} | ||||
|       LeftIcon={getIcon(filterDefinition.iconName)} | ||||
|       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 { getOperandLabel } from '../utils/getOperandLabel'; | ||||
| import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; | ||||
| import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; | ||||
|  | ||||
| export const ObjectFilterDropdownOperandSelect = () => { | ||||
|   const { | ||||
| @@ -31,9 +31,9 @@ export const ObjectFilterDropdownOperandSelect = () => { | ||||
|  | ||||
|   const selectedFilter = useRecoilValue(selectedFilterState); | ||||
|  | ||||
|   const operandsForFilterType = getOperandsForFilterType( | ||||
|     filterDefinitionUsedInDropdown?.type, | ||||
|   ); | ||||
|   const operandsForFilterType = isDefined(filterDefinitionUsedInDropdown) | ||||
|     ? getOperandsForFilterDefinition(filterDefinitionUsedInDropdown) | ||||
|     : []; | ||||
|  | ||||
|   const handleOperandChange = (newOperand: ViewFilterOperand) => { | ||||
|     const isValuelessOperand = [ | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil'; | ||||
| import { v4 } from 'uuid'; | ||||
|  | ||||
| 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 { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; | ||||
| import { SelectableItem } from '@/object-record/select/types/SelectableItem'; | ||||
| @@ -55,7 +55,7 @@ export const ObjectFilterDropdownSourceSelect = ({ | ||||
|  | ||||
|   const selectedFilter = useRecoilValue(selectedFilterState); | ||||
|  | ||||
|   const sourceTypes = getSourceEnumOptions( | ||||
|   const sourceTypes = getActorSourceMultiSelectOptions( | ||||
|     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 { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; | ||||
| import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; | ||||
| import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; | ||||
| import { GenericEntityFilterChip } from './GenericEntityFilterChip'; | ||||
| import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; | ||||
| import { ObjectFilterDropdownSearchInput } from './ObjectFilterDropdownSearchInput'; | ||||
| @@ -36,14 +36,16 @@ export const SingleEntityObjectFilterDropdownButton = ({ | ||||
|   ); | ||||
|   const selectedFilter = useRecoilValue(selectedFilterState); | ||||
|  | ||||
|   const availableFilter = availableFilterDefinitions[0]; | ||||
|   const availableFilterDefinition = availableFilterDefinitions[0]; | ||||
|  | ||||
|   React.useEffect(() => { | ||||
|     setFilterDefinitionUsedInDropdown(availableFilter); | ||||
|     const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0]; | ||||
|     setFilterDefinitionUsedInDropdown(availableFilterDefinition); | ||||
|     const defaultOperand = getOperandsForFilterDefinition( | ||||
|       availableFilterDefinition, | ||||
|     )[0]; | ||||
|     setSelectedOperandInDropdown(defaultOperand); | ||||
|   }, [ | ||||
|     availableFilter, | ||||
|     availableFilterDefinition, | ||||
|     setFilterDefinitionUsedInDropdown, | ||||
|     setSelectedOperandInDropdown, | ||||
|   ]); | ||||
| @@ -62,7 +64,7 @@ export const SingleEntityObjectFilterDropdownButton = ({ | ||||
|               filter={selectedFilter} | ||||
|               Icon={ | ||||
|                 selectedFilter.operand === ViewFilterOperand.IsNotNull | ||||
|                   ? availableFilter.SelectAllIcon | ||||
|                   ? availableFilterDefinition.SelectAllIcon | ||||
|                   : undefined | ||||
|               } | ||||
|             /> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; | ||||
| import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; | ||||
| 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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; | ||||
| import { v4 } from 'uuid'; | ||||
| @@ -31,12 +31,12 @@ export const useSelectFilter = () => { | ||||
|     } | ||||
|  | ||||
|     setSelectedOperandInDropdown( | ||||
|       getOperandsForFilterType(filterDefinition.type)?.[0], | ||||
|       getOperandsForFilterDefinition(filterDefinition)[0], | ||||
|     ); | ||||
|  | ||||
|     const { value, displayValue } = getInitialFilterValue( | ||||
|       filterDefinition.type, | ||||
|       getOperandsForFilterType(filterDefinition.type)?.[0], | ||||
|       getOperandsForFilterDefinition(filterDefinition)[0], | ||||
|     ); | ||||
|  | ||||
|     if (value !== '') { | ||||
| @@ -44,7 +44,7 @@ export const useSelectFilter = () => { | ||||
|         id: v4(), | ||||
|         fieldMetadataId: filterDefinition.fieldMetadataId, | ||||
|         displayValue, | ||||
|         operand: getOperandsForFilterType(filterDefinition.type)?.[0], | ||||
|         operand: getOperandsForFilterDefinition(filterDefinition)[0], | ||||
|         value, | ||||
|         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 { FilterType } from './FilterType'; | ||||
| import { FilterableFieldType } from './FilterableFieldType'; | ||||
|  | ||||
| export type FilterDefinition = { | ||||
|   fieldMetadataId: string; | ||||
|   label: string; | ||||
|   iconName: string; | ||||
|   type: FilterType; | ||||
|   type: FilterableFieldType; | ||||
|   relationObjectMetadataNamePlural?: string; | ||||
|   relationObjectMetadataNameSingular?: string; | ||||
|   selectAllLabel?: string; | ||||
|   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' | ||||
|   | 'PHONE' | ||||
|   | 'PHONES' | ||||
| @@ -18,4 +22,4 @@ export type FilterType = | ||||
|   | 'MULTI_SELECT' | ||||
|   | 'ACTOR' | ||||
|   | 'ARRAY' | ||||
|   | 'SOURCE'; | ||||
| >; | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; | ||||
| 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', () => { | ||||
|   const emptyOperands = [ | ||||
| @@ -51,7 +52,9 @@ describe('getOperandsForFilterType', () => { | ||||
|  | ||||
|   testCases.forEach(([filterType, expectedOperands]) => { | ||||
|     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); | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
| @@ -9,54 +9,54 @@ import { | ||||
|   IconUserCircle, | ||||
| } from 'twenty-ui'; | ||||
| 
 | ||||
| export const getSourceEnumOptions = ( | ||||
|   selectedItemIds: string[], | ||||
| export const getActorSourceMultiSelectOptions = ( | ||||
|   selectedSourceNames: string[], | ||||
| ): SelectableItem[] => { | ||||
|   return [ | ||||
|     { | ||||
|       id: 'MANUAL', | ||||
|       name: 'User', | ||||
|       isSelected: selectedItemIds.includes('MANUAL'), | ||||
|       isSelected: selectedSourceNames.includes('MANUAL'), | ||||
|       AvatarIcon: IconUserCircle, | ||||
|       isIconInverted: true, | ||||
|     }, | ||||
|     { | ||||
|       id: 'IMPORT', | ||||
|       name: 'Import', | ||||
|       isSelected: selectedItemIds.includes('IMPORT'), | ||||
|       isSelected: selectedSourceNames.includes('IMPORT'), | ||||
|       AvatarIcon: IconCsv, | ||||
|       isIconInverted: true, | ||||
|     }, | ||||
|     { | ||||
|       id: 'API', | ||||
|       name: 'Api', | ||||
|       isSelected: selectedItemIds.includes('API'), | ||||
|       isSelected: selectedSourceNames.includes('API'), | ||||
|       AvatarIcon: IconApi, | ||||
|       isIconInverted: true, | ||||
|     }, | ||||
|     { | ||||
|       id: 'EMAIL', | ||||
|       name: 'Email', | ||||
|       isSelected: selectedItemIds.includes('EMAIL'), | ||||
|       isSelected: selectedSourceNames.includes('EMAIL'), | ||||
|       AvatarIcon: IconGmail, | ||||
|     }, | ||||
|     { | ||||
|       id: 'CALENDAR', | ||||
|       name: 'Calendar', | ||||
|       isSelected: selectedItemIds.includes('CALENDAR'), | ||||
|       isSelected: selectedSourceNames.includes('CALENDAR'), | ||||
|       AvatarIcon: IconGoogleCalendar, | ||||
|     }, | ||||
|     { | ||||
|       id: 'WORKFLOW', | ||||
|       name: 'Workflow', | ||||
|       isSelected: selectedItemIds.includes('WORKFLOW'), | ||||
|       isSelected: selectedSourceNames.includes('WORKFLOW'), | ||||
|       AvatarIcon: IconSettingsAutomation, | ||||
|       isIconInverted: true, | ||||
|     }, | ||||
|     { | ||||
|       id: 'SYSTEM', | ||||
|       name: 'System', | ||||
|       isSelected: selectedItemIds.includes('SYSTEM'), | ||||
|       isSelected: selectedSourceNames.includes('SYSTEM'), | ||||
|       AvatarIcon: IconRobot, | ||||
|       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 { 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 { z } from 'zod'; | ||||
|  | ||||
| export const getInitialFilterValue = ( | ||||
|   newType: FilterType, | ||||
|   newType: FilterableFieldType, | ||||
|   newOperand: ViewFilterOperand, | ||||
|   oldValue?: string, | ||||
|   oldDisplayValue?: string, | ||||
| @@ -35,6 +35,7 @@ export const getInitialFilterValue = ( | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     value: oldValue ?? '', | ||||
|     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 { FilterType } from '../types/FilterType'; | ||||
|  | ||||
| export const getOperandsForFilterType = ( | ||||
|   filterType: FilterType | null | undefined, | ||||
| export const getOperandsForFilterDefinition = ( | ||||
|   filterDefinition: FilterDefinition, | ||||
| ): ViewFilterOperand[] => { | ||||
|   const emptyOperands = [ | ||||
|     ViewFilterOperand.IsEmpty, | ||||
| @@ -12,7 +12,7 @@ export const getOperandsForFilterType = ( | ||||
|  | ||||
|   const relationOperands = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; | ||||
|  | ||||
|   switch (filterType) { | ||||
|   switch (filterDefinition.type) { | ||||
|     case 'TEXT': | ||||
|     case 'EMAIL': | ||||
|     case 'EMAILS': | ||||
| @@ -21,7 +21,6 @@ export const getOperandsForFilterType = ( | ||||
|     case 'PHONE': | ||||
|     case 'LINK': | ||||
|     case 'LINKS': | ||||
|     case 'ACTOR': | ||||
|     case 'ARRAY': | ||||
|     case 'PHONES': | ||||
|       return [ | ||||
| @@ -57,10 +56,23 @@ export const getOperandsForFilterType = ( | ||||
|       ]; | ||||
|     case 'RELATION': | ||||
|       return [...relationOperands, ...emptyOperands]; | ||||
|     case 'SOURCE': | ||||
|       return [...relationOperands]; | ||||
|     case 'SELECT': | ||||
|       return [...relationOperands]; | ||||
|     case 'ACTOR': { | ||||
|       if (isActorSourceCompositeFilter(filterDefinition)) { | ||||
|         return [ | ||||
|           ViewFilterOperand.Is, | ||||
|           ViewFilterOperand.IsNot, | ||||
|           ...emptyOperands, | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       return [ | ||||
|         ViewFilterOperand.Contains, | ||||
|         ViewFilterOperand.DoesNotContain, | ||||
|         ...emptyOperands, | ||||
|       ]; | ||||
|     } | ||||
|     default: | ||||
|       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) { | ||||
|     case 'ACTOR': | ||||
|       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; | ||||
|  | ||||
| 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 FieldDateValue = string | null; | ||||
| export type FieldNumberValue = number | null; | ||||
| @@ -225,6 +225,8 @@ export type FieldRelationValue< | ||||
| export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[]; | ||||
| export type FieldJsonValue = Record<string, Json> | Json[] | null; | ||||
|  | ||||
| export type FieldRichTextValue = Record<string, Json> | Json[] | null; | ||||
|  | ||||
| export type FieldActorValue = { | ||||
|   source: 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, | ||||
|   RelationFilter, | ||||
|   StringFilter, | ||||
|   URLFilter, | ||||
|   UUIDFilter, | ||||
| } from '@/object-record/graphql/types/RecordGqlOperationFilter'; | ||||
| import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; | ||||
| import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; | ||||
| import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; | ||||
| import { Field } from '~/generated/graphql'; | ||||
| @@ -24,244 +24,15 @@ import { | ||||
|   convertLessThanRatingToArrayOfRatingValues, | ||||
|   convertRatingToRatingValue, | ||||
| } 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 { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; | ||||
| 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 = ( | ||||
|   rawUIFilters: Filter[], | ||||
|   fields: Pick<Field, 'id' | 'name'>[], | ||||
| @@ -273,7 +44,11 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|       (field) => field.id === rawUIFilter.fieldMetadataId, | ||||
|     ); | ||||
|  | ||||
|     const isValuelessOperand = [ | ||||
|     const compositeFieldName = rawUIFilter.definition.compositeFieldName; | ||||
|  | ||||
|     const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); | ||||
|  | ||||
|     const isEmptyOperand = [ | ||||
|       ViewFilterOperand.IsEmpty, | ||||
|       ViewFilterOperand.IsNotEmpty, | ||||
|       ViewFilterOperand.IsInPast, | ||||
| @@ -285,7 +60,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     if (!isValuelessOperand) { | ||||
|     if (!isEmptyOperand) { | ||||
|       if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') { | ||||
|         continue; | ||||
|       } | ||||
| @@ -316,7 +91,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
| @@ -355,7 +130,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           } | ||||
| @@ -372,8 +147,9 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               operand: ViewFilterOperand.IsRelative, | ||||
|             }); | ||||
|  | ||||
|             if (!defaultDateRange) | ||||
|             if (!defaultDateRange) { | ||||
|               throw new Error('Failed to resolve default date range'); | ||||
|             } | ||||
|  | ||||
|             const { start, end } = dateRange ?? defaultDateRange; | ||||
|  | ||||
| @@ -484,7 +260,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
| @@ -515,7 +291,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
| @@ -525,7 +301,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|         } | ||||
|         break; | ||||
|       case 'RELATION': { | ||||
|         if (!isValuelessOperand) { | ||||
|         if (!isEmptyOperand) { | ||||
|           try { | ||||
|             JSON.parse(rawUIFilter.value); | ||||
|           } catch (e) { | ||||
| @@ -570,7 +346,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|                 rawUIFilter.operand, | ||||
|                 correspondingField, | ||||
|                 objectRecordFilters, | ||||
|                 rawUIFilter.definition.type, | ||||
|                 rawUIFilter.definition, | ||||
|               ); | ||||
|               break; | ||||
|             default: | ||||
| @@ -603,7 +379,44 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               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; | ||||
|           default: | ||||
| @@ -618,20 +431,43 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|           correspondingField.name, | ||||
|           ['primaryLinkLabel', 'primaryLinkUrl'], | ||||
|         ); | ||||
|  | ||||
|         switch (rawUIFilter.operand) { | ||||
|           case ViewFilterOperand.Contains: | ||||
|             objectRecordFilters.push({ | ||||
|               or: linksFilters, | ||||
|             }); | ||||
|             if (!isCompositeFieldFiter) { | ||||
|               objectRecordFilters.push({ | ||||
|                 or: linksFilters, | ||||
|               }); | ||||
|             } else { | ||||
|               objectRecordFilters.push({ | ||||
|                 [correspondingField.name]: { | ||||
|                   [compositeFieldName]: { | ||||
|                     ilike: `%${rawUIFilter.value}%`, | ||||
|                   }, | ||||
|                 }, | ||||
|               }); | ||||
|             } | ||||
|             break; | ||||
|           case ViewFilterOperand.DoesNotContain: | ||||
|             objectRecordFilters.push({ | ||||
|               and: linksFilters.map((filter) => { | ||||
|                 return { | ||||
|                   not: filter, | ||||
|                 }; | ||||
|               }), | ||||
|             }); | ||||
|             if (!isCompositeFieldFiter) { | ||||
|               objectRecordFilters.push({ | ||||
|                 and: linksFilters.map((filter) => { | ||||
|                   return { | ||||
|                     not: filter, | ||||
|                   }; | ||||
|                 }), | ||||
|               }); | ||||
|             } else { | ||||
|               objectRecordFilters.push({ | ||||
|                 not: { | ||||
|                   [correspondingField.name]: { | ||||
|                     [compositeFieldName]: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }); | ||||
|             } | ||||
|             break; | ||||
|           case ViewFilterOperand.IsEmpty: | ||||
|           case ViewFilterOperand.IsNotEmpty: | ||||
| @@ -639,7 +475,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
| @@ -657,18 +493,40 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|         ); | ||||
|         switch (rawUIFilter.operand) { | ||||
|           case ViewFilterOperand.Contains: | ||||
|             objectRecordFilters.push({ | ||||
|               or: fullNameFilters, | ||||
|             }); | ||||
|             if (!isCompositeFieldFiter) { | ||||
|               objectRecordFilters.push({ | ||||
|                 or: fullNameFilters, | ||||
|               }); | ||||
|             } else { | ||||
|               objectRecordFilters.push({ | ||||
|                 [correspondingField.name]: { | ||||
|                   [compositeFieldName]: { | ||||
|                     ilike: `%${rawUIFilter.value}%`, | ||||
|                   }, | ||||
|                 }, | ||||
|               }); | ||||
|             } | ||||
|             break; | ||||
|           case ViewFilterOperand.DoesNotContain: | ||||
|             objectRecordFilters.push({ | ||||
|               and: fullNameFilters.map((filter) => { | ||||
|                 return { | ||||
|                   not: filter, | ||||
|                 }; | ||||
|               }), | ||||
|             }); | ||||
|             if (!isCompositeFieldFiter) { | ||||
|               objectRecordFilters.push({ | ||||
|                 and: fullNameFilters.map((filter) => { | ||||
|                   return { | ||||
|                     not: filter, | ||||
|                   }; | ||||
|                 }), | ||||
|               }); | ||||
|             } else { | ||||
|               objectRecordFilters.push({ | ||||
|                 not: { | ||||
|                   [correspondingField.name]: { | ||||
|                     [compositeFieldName]: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }); | ||||
|             } | ||||
|             break; | ||||
|           case ViewFilterOperand.IsEmpty: | ||||
|           case ViewFilterOperand.IsNotEmpty: | ||||
| @@ -676,7 +534,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
| @@ -689,85 +547,107 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|       case 'ADDRESS': | ||||
|         switch (rawUIFilter.operand) { | ||||
|           case ViewFilterOperand.Contains: | ||||
|             objectRecordFilters.push({ | ||||
|               or: [ | ||||
|                 { | ||||
|                   [correspondingField.name]: { | ||||
|                     addressStreet1: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   } as AddressFilter, | ||||
|                 }, | ||||
|                 { | ||||
|                   [correspondingField.name]: { | ||||
|                     addressStreet2: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   } as AddressFilter, | ||||
|                 }, | ||||
|                 { | ||||
|                   [correspondingField.name]: { | ||||
|                     addressCity: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   } as AddressFilter, | ||||
|                 }, | ||||
|                 { | ||||
|                   [correspondingField.name]: { | ||||
|                     addressState: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   } as AddressFilter, | ||||
|                 }, | ||||
|                 { | ||||
|                   [correspondingField.name]: { | ||||
|                     addressCountry: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   } as AddressFilter, | ||||
|                 }, | ||||
|                 { | ||||
|                   [correspondingField.name]: { | ||||
|                     addressPostcode: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     }, | ||||
|                   } as AddressFilter, | ||||
|                 }, | ||||
|               ], | ||||
|             }); | ||||
|             break; | ||||
|           case ViewFilterOperand.DoesNotContain: | ||||
|             objectRecordFilters.push({ | ||||
|               and: [ | ||||
|                 { | ||||
|                   not: { | ||||
|             if (!isCompositeFieldFiter) { | ||||
|               objectRecordFilters.push({ | ||||
|                 or: [ | ||||
|                   { | ||||
|                     [correspondingField.name]: { | ||||
|                       addressStreet1: { | ||||
|                         ilike: `%${rawUIFilter.value}%`, | ||||
|                       }, | ||||
|                     } as AddressFilter, | ||||
|                   }, | ||||
|                 }, | ||||
|                 { | ||||
|                   not: { | ||||
|                   { | ||||
|                     [correspondingField.name]: { | ||||
|                       addressStreet2: { | ||||
|                         ilike: `%${rawUIFilter.value}%`, | ||||
|                       }, | ||||
|                     } as AddressFilter, | ||||
|                   }, | ||||
|                 }, | ||||
|                 { | ||||
|                   not: { | ||||
|                   { | ||||
|                     [correspondingField.name]: { | ||||
|                       addressCity: { | ||||
|                         ilike: `%${rawUIFilter.value}%`, | ||||
|                       }, | ||||
|                     } as AddressFilter, | ||||
|                   }, | ||||
|                   { | ||||
|                     [correspondingField.name]: { | ||||
|                       addressState: { | ||||
|                         ilike: `%${rawUIFilter.value}%`, | ||||
|                       }, | ||||
|                     } as AddressFilter, | ||||
|                   }, | ||||
|                   { | ||||
|                     [correspondingField.name]: { | ||||
|                       addressCountry: { | ||||
|                         ilike: `%${rawUIFilter.value}%`, | ||||
|                       }, | ||||
|                     } as AddressFilter, | ||||
|                   }, | ||||
|                   { | ||||
|                     [correspondingField.name]: { | ||||
|                       addressPostcode: { | ||||
|                         ilike: `%${rawUIFilter.value}%`, | ||||
|                       }, | ||||
|                     } as AddressFilter, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             } else { | ||||
|               objectRecordFilters.push({ | ||||
|                 [correspondingField.name]: { | ||||
|                   [compositeFieldName]: { | ||||
|                     ilike: `%${rawUIFilter.value}%`, | ||||
|                   } as AddressFilter, | ||||
|                 }, | ||||
|               ], | ||||
|             }); | ||||
|               }); | ||||
|             } | ||||
|             break; | ||||
|           case ViewFilterOperand.DoesNotContain: | ||||
|             if (!isCompositeFieldFiter) { | ||||
|               objectRecordFilters.push({ | ||||
|                 and: [ | ||||
|                   { | ||||
|                     not: { | ||||
|                       [correspondingField.name]: { | ||||
|                         addressStreet1: { | ||||
|                           ilike: `%${rawUIFilter.value}%`, | ||||
|                         }, | ||||
|                       } as AddressFilter, | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     not: { | ||||
|                       [correspondingField.name]: { | ||||
|                         addressStreet2: { | ||||
|                           ilike: `%${rawUIFilter.value}%`, | ||||
|                         }, | ||||
|                       } as AddressFilter, | ||||
|                     }, | ||||
|                   }, | ||||
|                   { | ||||
|                     not: { | ||||
|                       [correspondingField.name]: { | ||||
|                         addressCity: { | ||||
|                           ilike: `%${rawUIFilter.value}%`, | ||||
|                         }, | ||||
|                       } as AddressFilter, | ||||
|                     }, | ||||
|                   }, | ||||
|                 ], | ||||
|               }); | ||||
|             } else { | ||||
|               objectRecordFilters.push({ | ||||
|                 not: { | ||||
|                   [correspondingField.name]: { | ||||
|                     [compositeFieldName]: { | ||||
|                       ilike: `%${rawUIFilter.value}%`, | ||||
|                     } as AddressFilter, | ||||
|                   }, | ||||
|                 }, | ||||
|               }); | ||||
|             } | ||||
|             break; | ||||
|           case ViewFilterOperand.IsEmpty: | ||||
|           case ViewFilterOperand.IsNotEmpty: | ||||
| @@ -775,7 +655,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
| @@ -785,12 +665,12 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|         } | ||||
|         break; | ||||
|       case 'SELECT': { | ||||
|         if (isValuelessOperand) { | ||||
|         if (isEmptyOperand) { | ||||
|           applyEmptyFilters( | ||||
|             rawUIFilter.operand, | ||||
|             correspondingField, | ||||
|             objectRecordFilters, | ||||
|             rawUIFilter.definition.type, | ||||
|             rawUIFilter.definition, | ||||
|           ); | ||||
|           break; | ||||
|         } | ||||
| @@ -836,41 +716,33 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|         break; | ||||
|       } | ||||
|       case 'ACTOR': | ||||
|         if (rawUIFilter.definition.subFieldType !== undefined) { | ||||
|         if (isActorSourceCompositeFilter(rawUIFilter.definition)) { | ||||
|           const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[]; | ||||
|           switch (rawUIFilter.definition.subFieldType) { | ||||
|             case 'SOURCE': | ||||
|               switch (rawUIFilter.operand) { | ||||
|                 case ViewFilterOperand.Is: | ||||
|                   objectRecordFilters.push({ | ||||
|  | ||||
|           switch (rawUIFilter.operand) { | ||||
|             case ViewFilterOperand.Is: | ||||
|               objectRecordFilters.push({ | ||||
|                 [correspondingField.name]: { | ||||
|                   source: { | ||||
|                     in: parsedRecordIds, | ||||
|                   } as RelationFilter, | ||||
|                 }, | ||||
|               }); | ||||
|  | ||||
|               break; | ||||
|             case ViewFilterOperand.IsNot: | ||||
|               if (parsedRecordIds.length > 0) { | ||||
|                 objectRecordFilters.push({ | ||||
|                   not: { | ||||
|                     [correspondingField.name]: { | ||||
|                       source: { | ||||
|                         in: parsedRecordIds, | ||||
|                       } as RelationFilter, | ||||
|                     }, | ||||
|                   }); | ||||
|  | ||||
|                   break; | ||||
|                 case ViewFilterOperand.IsNot: | ||||
|                   if (parsedRecordIds.length > 0) { | ||||
|                     objectRecordFilters.push({ | ||||
|                       not: { | ||||
|                         [correspondingField.name]: { | ||||
|                           [rawUIFilter.definition.subFieldType.toLowerCase()]: { | ||||
|                             in: parsedRecordIds, | ||||
|                           } as RelationFilter, | ||||
|                         }, | ||||
|                       }, | ||||
|                     }); | ||||
|                   } | ||||
|  | ||||
|                   break; | ||||
|  | ||||
|                 default: | ||||
|                   throw new Error( | ||||
|                     `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.subFieldType} filter`, | ||||
|                   ); | ||||
|                   }, | ||||
|                 }); | ||||
|               } | ||||
|               break; | ||||
|           } | ||||
|         } else { | ||||
|           switch (rawUIFilter.operand) { | ||||
| @@ -908,15 +780,14 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|                 rawUIFilter.operand, | ||||
|                 correspondingField, | ||||
|                 objectRecordFilters, | ||||
|                 rawUIFilter.definition.type, | ||||
|                 rawUIFilter.definition, | ||||
|               ); | ||||
|               break; | ||||
|             default: | ||||
|               throw new Error( | ||||
|                 `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, | ||||
|                 `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.label} filter`, | ||||
|               ); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|         break; | ||||
|       case 'EMAILS': | ||||
| @@ -955,7 +826,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
| @@ -991,7 +862,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( | ||||
|               rawUIFilter.operand, | ||||
|               correspondingField, | ||||
|               objectRecordFilters, | ||||
|               rawUIFilter.definition.type, | ||||
|               rawUIFilter.definition, | ||||
|             ); | ||||
|             break; | ||||
|           default: | ||||
|   | ||||
| @@ -5,7 +5,8 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u | ||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||
| import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; | ||||
| 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 { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; | ||||
| import { isDefined } from '~/utils/isDefined'; | ||||
| @@ -42,7 +43,15 @@ export const useHandleToggleColumnFilter = ({ | ||||
|         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]; | ||||
|  | ||||
| @@ -51,12 +60,7 @@ export const useHandleToggleColumnFilter = ({ | ||||
|         fieldMetadataId, | ||||
|         operand: defaultOperand, | ||||
|         displayValue: '', | ||||
|         definition: { | ||||
|           label: correspondingColumnDefinition.label, | ||||
|           iconName: correspondingColumnDefinition.iconName, | ||||
|           fieldMetadataId, | ||||
|           type: filterType, | ||||
|         }, | ||||
|         definition: filterDefinition, | ||||
|         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 { | ||||
|   IconComponent, | ||||
|   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; | ||||
| }; | ||||
| import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; | ||||
| import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; | ||||
|  | ||||
| export const SETTINGS_FIELD_TYPE_CONFIGS = { | ||||
|   [FieldMetadataType.Uuid]: { | ||||
|     label: 'Unique ID', | ||||
|     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 | ||||
| >; | ||||
|   ...SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS, | ||||
|   ...SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS, | ||||
| }; | ||||
|   | ||||
| @@ -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 { 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_CONFIGS, | ||||
|   SettingsFieldTypeConfig, | ||||
| } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | ||||
| import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | ||||
| import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; | ||||
|  | ||||
| import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues'; | ||||
| import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; | ||||
| 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 { useTheme } from '@emotion/react'; | ||||
| import styled from '@emotion/styled'; | ||||
| @@ -23,8 +22,8 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
| export const settingsDataModelFieldTypeFormSchema = z.object({ | ||||
|   type: z.enum( | ||||
|     Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [ | ||||
|       SettingsSupportedFieldType, | ||||
|       ...SettingsSupportedFieldType[], | ||||
|       SettingsFieldType, | ||||
|       ...SettingsFieldType[], | ||||
|     ], | ||||
|   ), | ||||
| }); | ||||
| @@ -35,7 +34,7 @@ export type SettingsDataModelFieldTypeFormValues = z.infer< | ||||
|  | ||||
| type SettingsDataModelFieldTypeSelectProps = { | ||||
|   className?: string; | ||||
|   excludedFieldTypes?: SettingsSupportedFieldType[]; | ||||
|   excludedFieldTypes?: SettingsFieldType[]; | ||||
|   fieldMetadataItem?: Pick< | ||||
|     FieldMetadataItem, | ||||
|     'defaultValue' | 'options' | 'type' | ||||
| @@ -78,11 +77,11 @@ export const SettingsDataModelFieldTypeSelect = ({ | ||||
|   const theme = useTheme(); | ||||
|   const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>(); | ||||
|   const [searchQuery, setSearchQuery] = useState(''); | ||||
|   const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig>( | ||||
|   const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig<any>>( | ||||
|     SETTINGS_FIELD_TYPE_CONFIGS, | ||||
|   ).filter( | ||||
|     ([key, config]) => | ||||
|       !excludedFieldTypes.includes(key as SettingsSupportedFieldType) && | ||||
|       !excludedFieldTypes.includes(key as SettingsFieldType) && | ||||
|       config.label.toLowerCase().includes(searchQuery.toLowerCase()), | ||||
|   ); | ||||
|  | ||||
| @@ -95,7 +94,7 @@ export const SettingsDataModelFieldTypeSelect = ({ | ||||
|   const { resetDefaultValueField: resetSelectDefaultValueField } = | ||||
|     useSelectSettingsFormInitialValues({ fieldMetadataItem }); | ||||
|  | ||||
|   const resetDefaultValueField = (nextValue: SettingsSupportedFieldType) => { | ||||
|   const resetDefaultValueField = (nextValue: SettingsFieldType) => { | ||||
|     switch (nextValue) { | ||||
|       case FieldMetadataType.Boolean: | ||||
|         resetBooleanDefaultValueField(); | ||||
| @@ -118,7 +117,7 @@ export const SettingsDataModelFieldTypeSelect = ({ | ||||
|       control={control} | ||||
|       defaultValue={ | ||||
|         fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs | ||||
|           ? (fieldMetadataItem.type as SettingsSupportedFieldType) | ||||
|           ? (fieldMetadataItem.type as SettingsFieldType) | ||||
|           : FieldMetadataType.Text | ||||
|       } | ||||
|       render={({ field: { onChange } }) => ( | ||||
| @@ -147,10 +146,8 @@ export const SettingsDataModelFieldTypeSelect = ({ | ||||
|                       <SettingsCard | ||||
|                         key={key} | ||||
|                         onClick={() => { | ||||
|                           onChange(key as SettingsSupportedFieldType); | ||||
|                           resetDefaultValueField( | ||||
|                             key as SettingsSupportedFieldType, | ||||
|                           ); | ||||
|                           onChange(key as SettingsFieldType); | ||||
|                           resetDefaultValueField(key as SettingsFieldType); | ||||
|                           onFieldTypeSelect(); | ||||
|                         }} | ||||
|                         Icon={ | ||||
|   | ||||
| @@ -16,9 +16,11 @@ export const getCurrencyFieldPreviewValue = ({ | ||||
| }): FieldCurrencyValue | null => { | ||||
|   if (fieldMetadataItem.type !== FieldMetadataType.Currency) return null; | ||||
|  | ||||
|   const placeholderDefaultValue = getSettingsFieldTypeConfig( | ||||
|   const currencyFieldTypeConfig = getSettingsFieldTypeConfig( | ||||
|     FieldMetadataType.Currency, | ||||
|   ).exampleValue; | ||||
|   ); | ||||
|  | ||||
|   const placeholderDefaultValue = currencyFieldTypeConfig.exampleValue; | ||||
|  | ||||
|   return currencyFieldDefaultValueSchema | ||||
|     .transform((value) => ({ | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; | ||||
| import { Link } from 'react-router-dom'; | ||||
| 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 { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
|  | ||||
| @@ -11,11 +11,11 @@ type SettingsObjectFieldDataTypeProps = { | ||||
|   to?: string; | ||||
|   Icon?: IconComponent; | ||||
|   label?: string; | ||||
|   value: SettingsSupportedFieldType; | ||||
|   value: SettingsFieldType; | ||||
| }; | ||||
|  | ||||
| const StyledDataType = styled.div<{ | ||||
|   value: SettingsSupportedFieldType; | ||||
|   value: SettingsFieldType; | ||||
|   to?: string; | ||||
| }>` | ||||
|   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 { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | ||||
| import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
| import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||
|  | ||||
| export const getSettingsFieldTypeConfig = <T extends FieldMetadataType>( | ||||
|   fieldType: T, | ||||
| ) => | ||||
|   (isFieldTypeSupportedInSettings(fieldType) | ||||
|     ? SETTINGS_FIELD_TYPE_CONFIGS[fieldType] | ||||
|     : undefined) as T extends SettingsSupportedFieldType | ||||
|     ? (typeof SETTINGS_FIELD_TYPE_CONFIGS)[T] | ||||
|     : undefined; | ||||
| export const getSettingsFieldTypeConfig = (fieldType: SettingsFieldType) => { | ||||
|   return SETTINGS_FIELD_TYPE_CONFIGS[fieldType as SettingsFieldType]; | ||||
| }; | ||||
|   | ||||
| @@ -1,8 +1,7 @@ | ||||
| import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; | ||||
| import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
| import { FieldType } from '@/settings/data-model/types/FieldType'; | ||||
| import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; | ||||
|  | ||||
| export const isFieldTypeSupportedInSettings = ( | ||||
|   fieldType: FieldMetadataType, | ||||
| ): fieldType is SettingsSupportedFieldType => | ||||
|   fieldType in SETTINGS_FIELD_TYPE_CONFIGS; | ||||
|   fieldType: FieldType, | ||||
| ): fieldType is SettingsFieldType => fieldType in SETTINGS_FIELD_TYPE_CONFIGS; | ||||
|   | ||||
| @@ -106,15 +106,18 @@ export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         const newValue = [ | ||||
|           ...unsavedToUpsertViewFilters, | ||||
|           { | ||||
|             ...upsertedFilter, | ||||
|             id: upsertedFilter.id, | ||||
|             __typename: 'ViewFilter', | ||||
|           } satisfies ViewFilter, | ||||
|         ] satisfies ViewFilter[]; | ||||
|  | ||||
|         set( | ||||
|           unsavedToUpsertViewFiltersCallbackState({ viewId: currentViewId }), | ||||
|           [ | ||||
|             ...unsavedToUpsertViewFilters, | ||||
|             { | ||||
|               ...upsertedFilter, | ||||
|               __typename: 'ViewFilter', | ||||
|             } satisfies ViewFilter, | ||||
|           ], | ||||
|           newValue, | ||||
|         ); | ||||
|       }, | ||||
|     [ | ||||
|   | ||||
| @@ -16,6 +16,7 @@ const baseDefinition = { | ||||
|   fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482', | ||||
|   label: 'label', | ||||
|   iconName: 'iconName', | ||||
|   fieldName: 'fieldName', | ||||
| }; | ||||
|  | ||||
| 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 { isDefined } from '~/utils/isDefined'; | ||||
|  | ||||
| import { getFilterDefinitionForViewFilter } from '@/views/utils/getFilterDefinitionForViewFilter'; | ||||
| import { ViewFilter } from '../types/ViewFilter'; | ||||
|  | ||||
| export const mapViewFiltersToFilters = ( | ||||
| @@ -24,10 +23,7 @@ export const mapViewFiltersToFilters = ( | ||||
|         value: viewFilter.value, | ||||
|         displayValue: viewFilter.displayValue, | ||||
|         operand: viewFilter.operand, | ||||
|         definition: getFilterDefinitionForViewFilter( | ||||
|           viewFilter, | ||||
|           availableFilterDefinition, | ||||
|         ), | ||||
|         definition: viewFilter.definition ?? availableFilterDefinition, | ||||
|       }; | ||||
|     }) | ||||
|     .filter(isDefined); | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| 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 { resolveNumberViewFilterValue } from '@/views/utils/view-filter-value/resolveNumberViewFilterValue'; | ||||
| import { | ||||
| @@ -8,7 +8,7 @@ import { | ||||
| } from './resolveDateViewFilterValue'; | ||||
|  | ||||
| type ResolvedFilterValue< | ||||
|   T extends FilterType, | ||||
|   T extends FilterableFieldType, | ||||
|   O extends ViewFilterOperand, | ||||
| > = T extends 'DATE' | 'DATE_TIME' | ||||
|   ? ResolvedDateViewFilterValue<O> | ||||
| @@ -16,16 +16,16 @@ type ResolvedFilterValue< | ||||
|     ? ReturnType<typeof resolveNumberViewFilterValue> | ||||
|     : string; | ||||
|  | ||||
| type PartialFilter<T extends FilterType, O extends ViewFilterOperand> = Pick< | ||||
|   Filter, | ||||
|   'value' | ||||
| > & { | ||||
| type PartialFilter< | ||||
|   T extends FilterableFieldType, | ||||
|   O extends ViewFilterOperand, | ||||
| > = Pick<Filter, 'value'> & { | ||||
|   definition: { type: T }; | ||||
|   operand: O; | ||||
| }; | ||||
|  | ||||
| export const resolveFilterValue = < | ||||
|   T extends FilterType, | ||||
|   T extends FilterableFieldType, | ||||
|   O extends ViewFilterOperand, | ||||
| >( | ||||
|   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 { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; | ||||
| 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 { AppPath } from '@/types/AppPath'; | ||||
| import { SettingsPath } from '@/types/SettingsPath'; | ||||
| @@ -42,9 +42,11 @@ import { Section } from '@/ui/layout/section/components/Section'; | ||||
| import { FieldMetadataType } from '~/generated-metadata/graphql'; | ||||
| import { isDefined } from '~/utils/isDefined'; | ||||
|  | ||||
| //TODO: fix this type | ||||
| type SettingsDataModelFieldEditFormValues = z.infer< | ||||
|   ReturnType<typeof settingsFieldFormSchema> | ||||
| >; | ||||
| > & | ||||
|   any; | ||||
|  | ||||
| const canPersistFieldMetadataItemUpdate = ( | ||||
|   fieldMetadataItem: FieldMetadataItem, | ||||
| @@ -94,7 +96,7 @@ export const SettingsObjectFieldEdit = () => { | ||||
|     resolver: zodResolver(settingsFieldFormSchema()), | ||||
|     values: { | ||||
|       icon: fieldMetadataItem?.icon ?? 'Icon', | ||||
|       type: fieldMetadataItem?.type as SettingsSupportedFieldType, | ||||
|       type: fieldMetadataItem?.type as SettingsFieldType, | ||||
|       label: fieldMetadataItem?.label ?? '', | ||||
|       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 { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect'; | ||||
| 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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; | ||||
| 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 { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; | ||||
|  | ||||
| // TODO: fix this type | ||||
| type SettingsDataModelNewFieldFormValues = z.infer< | ||||
|   ReturnType<typeof settingsFieldFormSchema> | ||||
| >; | ||||
| > & | ||||
|   any; | ||||
|  | ||||
| const StyledH1Title = styled(H1Title)` | ||||
|   margin-bottom: 0; | ||||
| @@ -46,7 +48,7 @@ export const SettingsObjectNewFieldStep2 = () => { | ||||
|   const navigate = useNavigate(); | ||||
|   const { objectSlug = '' } = useParams(); | ||||
|   const [searchParams] = useSearchParams(); | ||||
|   const fieldType = searchParams.get('fieldType') as SettingsSupportedFieldType; | ||||
|   const fieldType = searchParams.get('fieldType') as SettingsFieldType; | ||||
|   const { enqueueSnackBar } = useSnackBar(); | ||||
|  | ||||
|   const [isConfigureStep, setIsConfigureStep] = useState(false); | ||||
| @@ -159,7 +161,7 @@ export const SettingsObjectNewFieldStep2 = () => { | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const excludedFieldTypes: SettingsSupportedFieldType[] = ( | ||||
|   const excludedFieldTypes: SettingsFieldType[] = ( | ||||
|     [ | ||||
|       FieldMetadataType.Link, | ||||
|       FieldMetadataType.Numeric, | ||||
| @@ -226,7 +228,7 @@ export const SettingsObjectNewFieldStep2 = () => { | ||||
|               <SettingsDataModelFieldTypeSelect | ||||
|                 excludedFieldTypes={excludedFieldTypes} | ||||
|                 fieldMetadataItem={{ | ||||
|                   type: fieldType, | ||||
|                   type: fieldType as FieldMetadataType, | ||||
|                 }} | ||||
|                 onFieldTypeSelect={() => setIsConfigureStep(true)} | ||||
|               /> | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; | ||||
| import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; | ||||
| 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 { 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 { getSettingsObjectFieldType } from '~/pages/settings/data-model/utils/getSettingsObjectFieldType'; | ||||
|  | ||||
| @@ -29,13 +32,17 @@ export const useMapFieldMetadataItemToSettingsObjectDetailTableItem = ( | ||||
|       objectMetadataItem, | ||||
|     ); | ||||
|  | ||||
|     const fieldMetadataType = fieldMetadataItem.type as FieldType; | ||||
|  | ||||
|     return { | ||||
|       fieldMetadataItem, | ||||
|       fieldType: fieldType ?? '', | ||||
|       dataType: | ||||
|         relationObjectMetadataItem?.labelPlural ?? | ||||
|         getSettingsFieldTypeConfig(fieldMetadataItem.type)?.label ?? | ||||
|         '', | ||||
|         (relationObjectMetadataItem?.labelPlural ?? | ||||
|         isFieldTypeSupportedInSettings(fieldMetadataType)) | ||||
|           ? getSettingsFieldTypeConfig(fieldMetadataType as SettingsFieldType) | ||||
|               ?.label | ||||
|           : '', | ||||
|       label: fieldMetadataItem.label, | ||||
|       identifierType: identifierType, | ||||
|       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, | ||||
| } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; | ||||
|  | ||||
| // TODO: could we export this to GraphQL ? | ||||
| export type CompositeFieldMetadataType = | ||||
|   | FieldMetadataType.ADDRESS | ||||
|   | FieldMetadataType.CURRENCY | ||||
|   | ||||
		Reference in New Issue
	
	Block a user