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