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
2024-10-08 11:25:42 +02:00
committed by GitHub
parent af4f3cebb0
commit 4156d7821c
57 changed files with 1424 additions and 972 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = [

View File

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

View File

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

View File

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

View File

@@ -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
}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 [];
}

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType';
export const hasSubMenuFilter = (type: FilterType) => ['ACTOR'].includes(type);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
export type FieldType = `${FieldMetadataType}`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
export type SettingsSupportedFieldType = Exclude<
FieldMetadataType,
FieldMetadataType.Position | FieldMetadataType.TsVector
>;

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ const baseDefinition = {
fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482',
label: 'label',
iconName: 'iconName',
fieldName: 'fieldName',
};
describe('mapViewSortsToSorts', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
export type ExcludeLiteral<T, U extends T> = T extends U ? never : T;

View File

@@ -0,0 +1 @@
export type PickLiteral<T, U extends T> = U;

View File

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