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:
@ -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;
|
|
||||||
}[];
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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,
|
||||||
@ -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}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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,
|
||||||
@ -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 />
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { createStateScopeMap } from '@/ui/utilities/recoil-scope/utils/createStateScopeMap';
|
||||||
|
|
||||||
|
export const objectFilterDropdownSelectedOptionValuesScopedState =
|
||||||
|
createStateScopeMap<string[]>({
|
||||||
|
key: 'objectFilterDropdownSelectedOptionValuesScopedState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
@ -7,4 +7,5 @@ export type FilterType =
|
|||||||
| 'CURRENCY'
|
| 'CURRENCY'
|
||||||
| 'FULL_NAME'
|
| 'FULL_NAME'
|
||||||
| 'LINK'
|
| 'LINK'
|
||||||
| 'RELATION';
|
| 'RELATION'
|
||||||
|
| 'SELECT';
|
||||||
|
|||||||
@ -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 [];
|
||||||
|
|||||||
@ -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))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 <></>;
|
||||||
|
|||||||
@ -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}]',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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(', ');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user