Select Field Input Menu scrollable and add Select Field in Filter and Sort (#3656)

* - fix Select Option Menu scrollable and added search

- add select field in filter and sort operation

* Fix lint

* Fix post merge

* Fix select filter

* Fix

* Remove duplicated search input

* fix turn object into query

* Rename search inputs

* Remove debounced for options

* Simplify option filter

* Rename option to MenuItemSelectTag

* Fix test

* Infer type from field metadata item

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Arshil Vahora
2024-03-05 22:11:41 +05:30
committed by GitHub
parent 0b889ef089
commit 6bb7042a68
22 changed files with 367 additions and 27 deletions

View File

@ -1,6 +1,14 @@
import { ThemeColor } from '@/ui/theme/constants/MainColorNames'; import { ThemeColor } from '@/ui/theme/constants/MainColorNames';
import { Field, Relation } from '~/generated-metadata/graphql'; import { Field, Relation } from '~/generated-metadata/graphql';
export type FieldMetadataItemOption = {
color: ThemeColor;
id: string;
label: string;
position: number;
value: string;
};
export type FieldMetadataItem = Omit< export type FieldMetadataItem = Omit<
Field, Field,
| '__typename' | '__typename'
@ -27,11 +35,5 @@ export type FieldMetadataItem = Omit<
}) })
| null; | null;
defaultValue?: any; defaultValue?: any;
options?: { options?: FieldMetadataItemOption[];
color: ThemeColor;
id: string;
label: string;
position: number;
value: string;
}[];
}; };

View File

@ -18,6 +18,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
FieldMetadataType.Link, FieldMetadataType.Link,
FieldMetadataType.FullName, FieldMetadataType.FullName,
FieldMetadataType.Relation, FieldMetadataType.Relation,
FieldMetadataType.Select,
FieldMetadataType.Currency, FieldMetadataType.Currency,
].includes(field.type) ].includes(field.type)
) { ) {
@ -67,5 +68,7 @@ export const formatFieldMetadataItemAsFilterDefinition = ({
? 'TEXT' ? 'TEXT'
: field.type === FieldMetadataType.Relation : field.type === FieldMetadataType.Relation
? 'RELATION' ? 'RELATION'
: 'TEXT', : field.type === FieldMetadataType.Select
? 'SELECT'
: 'TEXT',
}); });

View File

@ -15,6 +15,7 @@ export const formatFieldMetadataItemsAsSortDefinitions = ({
FieldMetadataType.Number, FieldMetadataType.Number,
FieldMetadataType.Text, FieldMetadataType.Text,
FieldMetadataType.Boolean, FieldMetadataType.Boolean,
FieldMetadataType.Select,
].includes(field.type) ].includes(field.type)
) { ) {
return acc; return acc;

View File

@ -1,13 +1,14 @@
import { ObjectFilterDropdownRecordSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchInput'; import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownDateSearchInput } from './ObjectFilterDropdownDateSearchInput'; import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
import { ObjectFilterDropdownNumberSearchInput } from './ObjectFilterDropdownNumberSearchInput'; import { ObjectFilterDropdownNumberInput } from './ObjectFilterDropdownNumberInput';
import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton'; import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton';
import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect'; import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect';
import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect';
import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput'; import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput';
@ -40,17 +41,24 @@ export const MultipleFiltersDropdownContent = ({
) && <ObjectFilterDropdownTextSearchInput />} ) && <ObjectFilterDropdownTextSearchInput />}
{['NUMBER', 'CURRENCY'].includes( {['NUMBER', 'CURRENCY'].includes(
filterDefinitionUsedInDropdown.type, filterDefinitionUsedInDropdown.type,
) && <ObjectFilterDropdownNumberSearchInput />} ) && <ObjectFilterDropdownNumberInput />}
{filterDefinitionUsedInDropdown.type === 'DATE_TIME' && ( {filterDefinitionUsedInDropdown.type === 'DATE_TIME' && (
<ObjectFilterDropdownDateSearchInput /> <ObjectFilterDropdownDateInput />
)} )}
{filterDefinitionUsedInDropdown.type === 'RELATION' && ( {filterDefinitionUsedInDropdown.type === 'RELATION' && (
<> <>
<ObjectFilterDropdownRecordSearchInput /> <ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<ObjectFilterDropdownRecordSelect /> <ObjectFilterDropdownRecordSelect />
</> </>
)} )}
{filterDefinitionUsedInDropdown.type === 'SELECT' && (
<>
<ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator />
<ObjectFilterDropdownOptionSelect />
</>
)}
</> </>
) )
)} )}

View File

@ -2,7 +2,7 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { isNonNullable } from '~/utils/isNonNullable'; import { isNonNullable } from '~/utils/isNonNullable';
export const ObjectFilterDropdownDateSearchInput = () => { export const ObjectFilterDropdownDateInput = () => {
const { const {
filterDefinitionUsedInDropdown, filterDefinitionUsedInDropdown,
selectedOperandInDropdown, selectedOperandInDropdown,

View File

@ -1,9 +1,9 @@
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput';
export const ObjectFilterDropdownNumberSearchInput = () => { export const ObjectFilterDropdownNumberInput = () => {
const { const {
selectedOperandInDropdown, selectedOperandInDropdown,
filterDefinitionUsedInDropdown, filterDefinitionUsedInDropdown,
@ -13,7 +13,7 @@ export const ObjectFilterDropdownNumberSearchInput = () => {
return ( return (
filterDefinitionUsedInDropdown && filterDefinitionUsedInDropdown &&
selectedOperandInDropdown && ( selectedOperandInDropdown && (
<DropdownMenuSearchInput <DropdownMenuInput
autoFocus autoFocus
type="number" type="number"
placeholder={filterDefinitionUsedInDropdown.label} placeholder={filterDefinitionUsedInDropdown.label}

View File

@ -0,0 +1,111 @@
import { useEffect, useState } from 'react';
import { MenuItem, MenuItemMultiSelect } from 'tsup.ui.index';
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { useOptionsForSelect } from '@/object-record/object-filter-dropdown/hooks/useOptionsForSelect';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
export const EMPTY_FILTER_VALUE = '';
export const MAX_OPTIONS_TO_DISPLAY = 3;
type SelectOptionForFilter = FieldMetadataItemOption & {
isSelected: boolean;
};
export const ObjectFilterDropdownOptionSelect = () => {
const {
filterDefinitionUsedInDropdown,
objectFilterDropdownSearchInput,
selectedOperandInDropdown,
objectFilterDropdownSelectedOptionValues,
selectFilter,
} = useFilterDropdown();
const fieldMetaDataId = filterDefinitionUsedInDropdown?.fieldMetadataId ?? '';
const { selectOptions } = useOptionsForSelect(fieldMetaDataId);
const [selectableOptions, setSelectableOptions] = useState<
SelectOptionForFilter[]
>([]);
useEffect(() => {
if (selectOptions) {
const options = selectOptions.map((option) => {
const isSelected =
objectFilterDropdownSelectedOptionValues?.includes(option.value) ??
false;
return {
...option,
isSelected,
};
});
setSelectableOptions(options);
}
}, [objectFilterDropdownSelectedOptionValues, selectOptions]);
const handleMultipleOptionSelectChange = (
optionChanged: SelectOptionForFilter,
isSelected: boolean,
) => {
if (!selectOptions) {
return;
}
const newSelectableOptions = selectableOptions.map((option) =>
option.id === optionChanged.id ? { ...option, isSelected } : option,
);
setSelectableOptions(newSelectableOptions);
const selectedOptions = newSelectableOptions.filter(
(option) => option.isSelected,
);
const filterDisplayValue =
selectedOptions.length > MAX_OPTIONS_TO_DISPLAY
? `${selectedOptions.length} options`
: selectedOptions.map((option) => option.label).join(', ');
if (filterDefinitionUsedInDropdown && selectedOperandInDropdown) {
const newFilterValue =
selectedOptions.length > 0
? JSON.stringify(selectedOptions.map((option) => option.value))
: EMPTY_FILTER_VALUE;
selectFilter({
definition: filterDefinitionUsedInDropdown,
operand: selectedOperandInDropdown,
displayValue: filterDisplayValue,
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
value: newFilterValue,
});
}
};
const optionsInDropdown = selectableOptions?.filter((option) =>
option.label.toLowerCase().includes(objectFilterDropdownSearchInput),
);
const showNoResult = optionsInDropdown?.length === 0;
return (
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
className=""
/>
))}
{showNoResult && <MenuItem text="No result" />}
</DropdownMenuItemsContainer>
);
};

View File

@ -3,7 +3,7 @@ import { ChangeEvent } from 'react';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
export const ObjectFilterDropdownRecordSearchInput = () => { export const ObjectFilterDropdownSearchInput = () => {
const { const {
filterDefinitionUsedInDropdown, filterDefinitionUsedInDropdown,
selectedOperandInDropdown, selectedOperandInDropdown,

View File

@ -13,8 +13,8 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { GenericEntityFilterChip } from './GenericEntityFilterChip'; import { GenericEntityFilterChip } from './GenericEntityFilterChip';
import { ObjectFilterDropdownRecordSearchInput } from './ObjectFilterDropdownEntitySearchInput';
import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownSearchInput } from './ObjectFilterDropdownSearchInput';
export const SingleEntityObjectFilterDropdownButton = ({ export const SingleEntityObjectFilterDropdownButton = ({
hotkeyScope, hotkeyScope,
@ -66,7 +66,7 @@ export const SingleEntityObjectFilterDropdownButton = ({
} }
dropdownComponents={ dropdownComponents={
<> <>
<ObjectFilterDropdownRecordSearchInput /> <ObjectFilterDropdownSearchInput />
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<ObjectFilterDropdownRecordRemoveFilterMenuItem /> <ObjectFilterDropdownRecordRemoveFilterMenuItem />
<ObjectFilterDropdownRecordSelect /> <ObjectFilterDropdownRecordSelect />

View File

@ -27,6 +27,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
setObjectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId,
objectFilterDropdownSelectedRecordIds, objectFilterDropdownSelectedRecordIds,
setObjectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds,
objectFilterDropdownSelectedOptionValues,
setObjectFilterDropdownSelectedOptionValues,
isObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownOperandSelectUnfolded,
setIsObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded,
isObjectFilterDropdownUnfolded, isObjectFilterDropdownUnfolded,
@ -87,6 +89,8 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => {
setObjectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId,
objectFilterDropdownSelectedRecordIds, objectFilterDropdownSelectedRecordIds,
setObjectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds,
objectFilterDropdownSelectedOptionValues,
setObjectFilterDropdownSelectedOptionValues,
isObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownOperandSelectUnfolded,
setIsObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded,
isObjectFilterDropdownUnfolded, isObjectFilterDropdownUnfolded,

View File

@ -8,6 +8,7 @@ import { isObjectFilterDropdownOperandSelectUnfoldedScopedState } from '../state
import { isObjectFilterDropdownUnfoldedScopedState } from '../states/isObjectFilterDropdownUnfoldedScopedState'; import { isObjectFilterDropdownUnfoldedScopedState } from '../states/isObjectFilterDropdownUnfoldedScopedState';
import { objectFilterDropdownSearchInputScopedState } from '../states/objectFilterDropdownSearchInputScopedState'; import { objectFilterDropdownSearchInputScopedState } from '../states/objectFilterDropdownSearchInputScopedState';
import { objectFilterDropdownSelectedEntityIdScopedState } from '../states/objectFilterDropdownSelectedEntityIdScopedState'; import { objectFilterDropdownSelectedEntityIdScopedState } from '../states/objectFilterDropdownSelectedEntityIdScopedState';
import { objectFilterDropdownSelectedOptionValuesScopedState } from '../states/objectFilterDropdownSelectedOptionValuesScopedState';
import { selectedFilterScopedState } from '../states/selectedFilterScopedState'; import { selectedFilterScopedState } from '../states/selectedFilterScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
@ -37,6 +38,14 @@ export const useFilterDropdownStates = (scopeId: string) => {
scopeId, scopeId,
); );
const [
objectFilterDropdownSelectedOptionValues,
setObjectFilterDropdownSelectedOptionValues,
] = useRecoilScopedStateV2(
objectFilterDropdownSelectedOptionValuesScopedState,
scopeId,
);
const [ const [
isObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownOperandSelectUnfolded,
setIsObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded,
@ -71,6 +80,8 @@ export const useFilterDropdownStates = (scopeId: string) => {
objectFilterDropdownSelectedEntityId, objectFilterDropdownSelectedEntityId,
setObjectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId,
objectFilterDropdownSelectedRecordIds, objectFilterDropdownSelectedRecordIds,
objectFilterDropdownSelectedOptionValues,
setObjectFilterDropdownSelectedOptionValues,
setObjectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds,
isObjectFilterDropdownOperandSelectUnfolded, isObjectFilterDropdownOperandSelectUnfolded,
setIsObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded,

View File

@ -0,0 +1,26 @@
import { useParams } from 'react-router-dom';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
export const DEFAULT_SEARCH_REQUEST_LIMIT = 60;
export const useOptionsForSelect = (fieldMetadataId: string) => {
const objectNamePlural = useParams().objectNamePlural ?? '';
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
const fieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === fieldMetadataId,
);
const selectOptions = fieldMetadataItem?.options;
return {
selectOptions,
};
};

View File

@ -0,0 +1,7 @@
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
export const objectFilterDropdownSelectedOptionValuesScopedState =
createStateScopeMap<string[]>({
key: 'objectFilterDropdownSelectedOptionValuesScopedState',
defaultValue: [],
});

View File

@ -7,4 +7,5 @@ export type FilterType =
| 'CURRENCY' | 'CURRENCY'
| 'FULL_NAME' | 'FULL_NAME'
| 'LINK' | 'LINK'
| 'RELATION'; | 'RELATION'
| 'SELECT';

View File

@ -16,6 +16,7 @@ export const getOperandsForFilterType = (
case 'DATE_TIME': case 'DATE_TIME':
return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]; return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan];
case 'RELATION': case 'RELATION':
case 'SELECT':
return [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; return [ViewFilterOperand.Is, ViewFilterOperand.IsNot];
default: default:
return []; return [];

View File

@ -1,10 +1,13 @@
import { useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { MenuItem } from 'tsup.ui.index';
import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField'; import { useSelectField } from '@/object-record/record-field/meta-types/hooks/useSelectField';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { MenuItemSelectTag } from '@/ui/navigation/menu-item/components/MenuItemSelectTag';
const StyledRelationPickerContainer = styled.div` const StyledRelationPickerContainer = styled.div`
left: -1px; left: -1px;
@ -17,16 +20,36 @@ export type SelectFieldInputProps = {
}; };
export const SelectFieldInput = ({ onSubmit }: SelectFieldInputProps) => { export const SelectFieldInput = ({ onSubmit }: SelectFieldInputProps) => {
const { persistField, fieldDefinition } = useSelectField(); const { persistField, fieldDefinition, fieldValue } = useSelectField();
const [searchFilter, setSearchFilter] = useState('');
const selectedOption = fieldDefinition.metadata.options.find(
(option) => option.value === fieldValue,
);
const optionsToSelect =
fieldDefinition.metadata.options.filter((option) => {
return option.value !== fieldValue && option.label.includes(searchFilter);
}) || [];
const optionsInDropDown = selectedOption
? [selectedOption, ...optionsToSelect]
: optionsToSelect;
return ( return (
<StyledRelationPickerContainer> <StyledRelationPickerContainer>
<DropdownMenu data-select-disable> <DropdownMenu data-select-disable>
<DropdownMenuItemsContainer> <DropdownMenuSearchInput
{fieldDefinition.metadata.options.map((option) => { value={searchFilter}
onChange={(event) => setSearchFilter(event.currentTarget.value)}
autoFocus
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropDown.map((option) => {
return ( return (
<MenuItem <MenuItemSelectTag
selected={option.value === fieldValue}
text={option.label} text={option.label}
color={option.color}
onClick={() => onSubmit?.(() => persistField(option.value))} onClick={() => onSubmit?.(() => persistField(option.value))}
/> />
); );

View File

@ -141,6 +141,7 @@ export const isRecordMatchingFilter = ({
switch (objectMetadataField.type) { switch (objectMetadataField.type) {
case FieldMetadataType.Email: case FieldMetadataType.Email:
case FieldMetadataType.Phone: case FieldMetadataType.Phone:
case FieldMetadataType.Select:
case FieldMetadataType.Text: { case FieldMetadataType.Text: {
return isMatchingStringFilter({ return isMatchingStringFilter({
stringFilter: filterValue as StringFilter, stringFilter: filterValue as StringFilter,

View File

@ -1,3 +1,5 @@
import { isNonEmptyString } from '@sniptt/guards';
import { import {
CurrencyFilter, CurrencyFilter,
DateFilter, DateFilter,
@ -254,6 +256,48 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
); );
} }
break; break;
case 'SELECT': {
const stringifiedSelectValues = rawUIFilter.value;
let parsedOptionValues: string[] = [];
if (!isNonEmptyString(stringifiedSelectValues)) {
break;
}
try {
parsedOptionValues = JSON.parse(stringifiedSelectValues);
} catch (e) {
throw new Error(
`Cannot parse filter value for SELECT filter : "${stringifiedSelectValues}"`,
);
}
if (parsedOptionValues.length > 0) {
switch (rawUIFilter.operand) {
case ViewFilterOperand.Is:
objectRecordFilters.push({
[correspondingField.name]: {
in: parsedOptionValues,
} as UUIDFilter,
});
break;
case ViewFilterOperand.IsNot:
objectRecordFilters.push({
not: {
[correspondingField.name]: {
in: parsedOptionValues,
} as UUIDFilter,
},
});
break;
default:
throw new Error(
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
);
}
}
break;
}
default: default:
throw new Error('Unknown filter type'); throw new Error('Unknown filter type');
} }

View File

@ -0,0 +1,40 @@
import { useTheme } from '@emotion/react';
import { Tag } from 'tsup.ui.index';
import { IconCheck } from '@/ui/display/icon';
import { ThemeColor } from '@/ui/theme/constants/MainColorNames';
import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase';
import { StyledMenuItemSelect } from './MenuItemSelect';
type MenuItemSelectTagProps = {
selected: boolean;
className?: string;
onClick?: () => void;
color: ThemeColor;
text: string;
};
export const MenuItemSelectTag = ({
color,
selected,
className,
onClick,
text,
}: MenuItemSelectTagProps) => {
const theme = useTheme();
return (
<StyledMenuItemSelect
onClick={onClick}
className={className}
selected={selected}
>
<StyledMenuItemLeftContent>
<Tag color={color} text={text} />
</StyledMenuItemLeftContent>
{selected && <IconCheck size={theme.icon.size.sm} />}
</StyledMenuItemSelect>
);
};

View File

@ -26,6 +26,7 @@ export const ViewBarFilterEffect = ({
setOnFilterSelect, setOnFilterSelect,
filterDefinitionUsedInDropdown, filterDefinitionUsedInDropdown,
setObjectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds,
setObjectFilterDropdownSelectedOptionValues,
isObjectFilterDropdownUnfolded, isObjectFilterDropdownUnfolded,
} = useFilterDropdown({ filterDropdownId }); } = useFilterDropdown({ filterDropdownId });
@ -61,12 +62,29 @@ export const ViewBarFilterEffect = ({
: []; : [];
setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecordIds); setObjectFilterDropdownSelectedRecordIds(viewFilterSelectedRecordIds);
} else if (filterDefinitionUsedInDropdown?.type === 'SELECT') {
const viewFilterUsedInDropdown = currentViewFilters.find(
(filter) =>
filter.fieldMetadataId ===
filterDefinitionUsedInDropdown.fieldMetadataId,
);
const viewFilterSelectedOptionValues = isNonEmptyString(
viewFilterUsedInDropdown?.value,
)
? JSON.parse(viewFilterUsedInDropdown.value)
: [];
setObjectFilterDropdownSelectedOptionValues(
viewFilterSelectedOptionValues,
);
} }
}, [ }, [
filterDefinitionUsedInDropdown, filterDefinitionUsedInDropdown,
currentViewFilters, currentViewFilters,
setObjectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds,
isObjectFilterDropdownUnfolded, isObjectFilterDropdownUnfolded,
setObjectFilterDropdownSelectedOptionValues,
]); ]);
return <></>; return <></>;

View File

@ -82,5 +82,41 @@ describe('ArgsStringFactory', () => {
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}]', 'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}]',
); );
}); });
it('when orderBy is present with position criteria, should return position at the end of the list', () => {
const args = {
orderBy: {
position: 'AscNullsFirst',
id: 'AscNullsFirst',
name: 'AscNullsFirst',
},
};
argsAliasCreate.mockReturnValue(args);
const result = service.create(args, []);
expect(result).toEqual(
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]',
);
});
it('when orderBy is present with position in the middle, should return position at the end of the list', () => {
const args = {
orderBy: {
id: 'AscNullsFirst',
position: 'AscNullsFirst',
name: 'AscNullsFirst',
},
};
argsAliasCreate.mockReturnValue(args);
const result = service.create(args, []);
expect(result).toEqual(
'orderBy: [{id: AscNullsFirst}, {name: AscNullsFirst}, {position: AscNullsFirst}]',
);
});
}); });
}); });

View File

@ -62,6 +62,9 @@ export class ArgsStringFactory {
// PgGraphql is expecting the orderBy argument to be an array of objects // PgGraphql is expecting the orderBy argument to be an array of objects
if (key === 'orderBy') { if (key === 'orderBy') {
const orderByString = Object.keys(obj) const orderByString = Object.keys(obj)
.sort((_, b) => {
return b === 'position' ? -1 : 0;
})
.map((orderByKey) => `{${orderByKey}: ${obj[orderByKey]}}`) .map((orderByKey) => `{${orderByKey}: ${obj[orderByKey]}}`)
.join(', '); .join(', ');