First working version of new dropdown UI (#360)

* First working version of new dropdown UI

* Removed consolelog

* Fixed test storybook

* Cleaned optional args
This commit is contained in:
Lucas Bordeau 2023-06-23 12:39:16 +02:00 committed by GitHub
parent 703f31632d
commit ceaf482f62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 111 additions and 40 deletions

View File

@ -2,6 +2,6 @@ import { DocumentNode } from 'graphql';
export type SearchConfigType = {
query: DocumentNode;
template: (searchInput: string) => any;
template: (searchInput: string, currentSelectedId?: string) => any;
resultMapper: (data: any) => any;
};

View File

@ -79,7 +79,13 @@ export type SearchResultsType<T> = {
loading: boolean;
};
export const useSearch = <T>(): [
type SearchArgs = {
currentSelectedId?: string | null;
};
export const useSearch = <T>(
searchArgs?: SearchArgs,
): [
SearchResultsType<T>,
React.Dispatch<React.SetStateAction<string>>,
React.Dispatch<React.SetStateAction<SearchConfigType | null>>,
@ -99,9 +105,12 @@ export const useSearch = <T>(): [
return (
searchConfig &&
searchConfig.template &&
searchConfig.template(searchInput)
searchConfig.template(
searchInput,
searchArgs?.currentSelectedId ?? undefined,
)
);
}, [searchConfig, searchInput]);
}, [searchConfig, searchInput, searchArgs]);
const searchQueryResults = useQuery(searchConfig?.query || EMPTY_QUERY, {
variables: {

View File

@ -57,6 +57,7 @@ export function DropdownMenuSelectableItem({
onClick={onClick}
selected={selected}
hovered={hovered}
data-testid="dropdown-menu-item"
>
<StyledLeftContainer>{children}</StyledLeftContainer>
<StyledRightIcon>

View File

@ -11,6 +11,9 @@ import { SearchResultsType, useSearch } from '@/search/services/search';
import { humanReadableDate } from '@/utils/utils';
import DatePicker from '../../form/DatePicker';
import { DropdownMenuItemContainer } from '../../menu/DropdownMenuItemContainer';
import { DropdownMenuSelectableItem } from '../../menu/DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '../../menu/DropdownMenuSeparator';
import DropdownButton from './DropdownButton';
@ -29,6 +32,8 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
}: OwnProps<TData>) => {
const [isUnfolded, setIsUnfolded] = useState(false);
const [selectedEntityId, setSelectedEntityId] = useState<string | null>(null);
const [isOperandSelectionUnfolded, setIsOperandSelectionUnfolded] =
useState(false);
@ -41,7 +46,7 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
>(undefined);
const [filterSearchResults, setSearchInput, setFilterSearch] =
useSearch<TData>();
useSearch<TData>({ currentSelectedId: selectedEntityId });
const resetState = useCallback(() => {
setIsOperandSelectionUnfolded(false);
@ -92,29 +97,50 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
);
}
return filterSearchResults.results.map((result, index) => {
return (
<DropdownButton.StyledDropdownItem
key={`fields-value-${index}`}
onClick={() => {
onFilterSelect({
key: selectedFilter.key,
label: selectedFilter.label,
value: result.value,
displayValue: result.render(result.value),
icon: selectedFilter.icon,
operand: selectedFilterOperand,
});
setIsUnfolded(false);
setSelectedFilter(undefined);
}}
>
<DropdownButton.StyledDropdownItemClipped>
{result.render(result.value)}
</DropdownButton.StyledDropdownItemClipped>
</DropdownButton.StyledDropdownItem>
);
});
function resultIsEntity(result: any): result is { id: string } {
return Object.keys(result ?? {}).includes('id');
}
return (
<>
<DropdownMenuSeparator />
<DropdownMenuItemContainer>
{filterSearchResults.results.map((result, index) => {
return (
<DropdownMenuSelectableItem
key={`fields-value-${index}`}
selected={
resultIsEntity(result.value) &&
result.value.id === selectedEntityId
}
onClick={() => {
console.log({ result });
if (resultIsEntity(result.value)) {
setSelectedEntityId(result.value.id);
}
onFilterSelect({
key: selectedFilter.key,
label: selectedFilter.label,
value: result.value,
displayValue: result.render(result.value),
icon: selectedFilter.icon,
operand: selectedFilterOperand,
});
setIsUnfolded(false);
setSelectedFilter(undefined);
}}
>
<DropdownButton.StyledDropdownItemClipped>
{result.render(result.value)}
</DropdownButton.StyledDropdownItemClipped>
</DropdownMenuSelectableItem>
);
})}
</DropdownMenuItemContainer>
</>
);
};
function renderValueSelection(
@ -131,7 +157,7 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>
<DropdownButton.StyledSearchField key={'search-filter'}>
<DropdownButton.StyledSearchField autoFocus key={'search-filter'}>
{['text', 'relation'].includes(selectedFilter.type) && (
<input
type="text"

View File

@ -1,6 +1,7 @@
import { expect } from '@storybook/jest';
import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import assert from 'assert';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
@ -62,9 +63,16 @@ export const FilterByAccountOwner: Story = {
delay: 200,
});
const charlesChip = canvas.getByText('Charles Test', {
selector: 'li > span',
});
const charlesChip = canvas
.getAllByTestId('dropdown-menu-item')
.find((item) => {
return item.textContent === 'Charles Test';
});
expect(charlesChip).toBeInTheDocument();
assert(charlesChip);
await userEvent.click(charlesChip);
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();

View File

@ -164,11 +164,18 @@ export const accountOwnerFilter = {
type: 'relation',
searchConfig: {
query: SEARCH_USER_QUERY,
template: (searchString: string) => ({
displayName: {
contains: `%${searchString}%`,
mode: QueryMode.Insensitive,
},
template: (searchString: string, currentSelectedId?: string) => ({
OR: [
{
displayName: {
contains: `%${searchString}%`,
mode: QueryMode.Insensitive,
},
},
{
id: currentSelectedId ? { equals: currentSelectedId } : undefined,
},
],
}),
resultMapper: (data: any) => ({
value: data,

View File

@ -1,6 +1,7 @@
import { expect } from '@storybook/jest';
import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import assert from 'assert';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
@ -59,7 +60,16 @@ export const CompanyName: Story = {
delay: 200,
});
const qontoChip = canvas.getByText('Qonto', { selector: 'li > span' });
const qontoChip = canvas
.getAllByTestId('dropdown-menu-item')
.find((item) => {
return item.textContent === 'Qonto';
});
expect(qontoChip).toBeInTheDocument();
assert(qontoChip);
await userEvent.click(qontoChip);
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();

View File

@ -100,8 +100,18 @@ export const companyFilter = {
type: 'relation',
searchConfig: {
query: SEARCH_COMPANY_QUERY,
template: (searchString: string) => ({
name: { contains: `%${searchString}%`, mode: QueryMode.Insensitive },
template: (searchString: string, currentSelectedId?: string) => ({
OR: [
{
name: {
contains: `%${searchString}%`,
mode: QueryMode.Insensitive,
},
},
{
id: currentSelectedId ? { equals: currentSelectedId } : undefined,
},
],
}),
resultMapper: (data) => ({
value: data,