mirror of
https://github.com/lingble/twenty.git
synced 2025-11-02 21:57:56 +00:00
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:
@@ -2,6 +2,6 @@ import { DocumentNode } from 'graphql';
|
|||||||
|
|
||||||
export type SearchConfigType = {
|
export type SearchConfigType = {
|
||||||
query: DocumentNode;
|
query: DocumentNode;
|
||||||
template: (searchInput: string) => any;
|
template: (searchInput: string, currentSelectedId?: string) => any;
|
||||||
resultMapper: (data: any) => any;
|
resultMapper: (data: any) => any;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -79,7 +79,13 @@ export type SearchResultsType<T> = {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSearch = <T>(): [
|
type SearchArgs = {
|
||||||
|
currentSelectedId?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSearch = <T>(
|
||||||
|
searchArgs?: SearchArgs,
|
||||||
|
): [
|
||||||
SearchResultsType<T>,
|
SearchResultsType<T>,
|
||||||
React.Dispatch<React.SetStateAction<string>>,
|
React.Dispatch<React.SetStateAction<string>>,
|
||||||
React.Dispatch<React.SetStateAction<SearchConfigType | null>>,
|
React.Dispatch<React.SetStateAction<SearchConfigType | null>>,
|
||||||
@@ -99,9 +105,12 @@ export const useSearch = <T>(): [
|
|||||||
return (
|
return (
|
||||||
searchConfig &&
|
searchConfig &&
|
||||||
searchConfig.template &&
|
searchConfig.template &&
|
||||||
searchConfig.template(searchInput)
|
searchConfig.template(
|
||||||
|
searchInput,
|
||||||
|
searchArgs?.currentSelectedId ?? undefined,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}, [searchConfig, searchInput]);
|
}, [searchConfig, searchInput, searchArgs]);
|
||||||
|
|
||||||
const searchQueryResults = useQuery(searchConfig?.query || EMPTY_QUERY, {
|
const searchQueryResults = useQuery(searchConfig?.query || EMPTY_QUERY, {
|
||||||
variables: {
|
variables: {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export function DropdownMenuSelectableItem({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
hovered={hovered}
|
hovered={hovered}
|
||||||
|
data-testid="dropdown-menu-item"
|
||||||
>
|
>
|
||||||
<StyledLeftContainer>{children}</StyledLeftContainer>
|
<StyledLeftContainer>{children}</StyledLeftContainer>
|
||||||
<StyledRightIcon>
|
<StyledRightIcon>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { SearchResultsType, useSearch } from '@/search/services/search';
|
|||||||
import { humanReadableDate } from '@/utils/utils';
|
import { humanReadableDate } from '@/utils/utils';
|
||||||
|
|
||||||
import DatePicker from '../../form/DatePicker';
|
import DatePicker from '../../form/DatePicker';
|
||||||
|
import { DropdownMenuItemContainer } from '../../menu/DropdownMenuItemContainer';
|
||||||
|
import { DropdownMenuSelectableItem } from '../../menu/DropdownMenuSelectableItem';
|
||||||
|
import { DropdownMenuSeparator } from '../../menu/DropdownMenuSeparator';
|
||||||
|
|
||||||
import DropdownButton from './DropdownButton';
|
import DropdownButton from './DropdownButton';
|
||||||
|
|
||||||
@@ -29,6 +32,8 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
|
|||||||
}: OwnProps<TData>) => {
|
}: OwnProps<TData>) => {
|
||||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||||
|
|
||||||
|
const [selectedEntityId, setSelectedEntityId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [isOperandSelectionUnfolded, setIsOperandSelectionUnfolded] =
|
const [isOperandSelectionUnfolded, setIsOperandSelectionUnfolded] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@@ -41,7 +46,7 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
const [filterSearchResults, setSearchInput, setFilterSearch] =
|
const [filterSearchResults, setSearchInput, setFilterSearch] =
|
||||||
useSearch<TData>();
|
useSearch<TData>({ currentSelectedId: selectedEntityId });
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
const resetState = useCallback(() => {
|
||||||
setIsOperandSelectionUnfolded(false);
|
setIsOperandSelectionUnfolded(false);
|
||||||
@@ -92,11 +97,29 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterSearchResults.results.map((result, index) => {
|
function resultIsEntity(result: any): result is { id: string } {
|
||||||
|
return Object.keys(result ?? {}).includes('id');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownButton.StyledDropdownItem
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItemContainer>
|
||||||
|
{filterSearchResults.results.map((result, index) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuSelectableItem
|
||||||
key={`fields-value-${index}`}
|
key={`fields-value-${index}`}
|
||||||
|
selected={
|
||||||
|
resultIsEntity(result.value) &&
|
||||||
|
result.value.id === selectedEntityId
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
console.log({ result });
|
||||||
|
|
||||||
|
if (resultIsEntity(result.value)) {
|
||||||
|
setSelectedEntityId(result.value.id);
|
||||||
|
}
|
||||||
|
|
||||||
onFilterSelect({
|
onFilterSelect({
|
||||||
key: selectedFilter.key,
|
key: selectedFilter.key,
|
||||||
label: selectedFilter.label,
|
label: selectedFilter.label,
|
||||||
@@ -112,9 +135,12 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
|
|||||||
<DropdownButton.StyledDropdownItemClipped>
|
<DropdownButton.StyledDropdownItemClipped>
|
||||||
{result.render(result.value)}
|
{result.render(result.value)}
|
||||||
</DropdownButton.StyledDropdownItemClipped>
|
</DropdownButton.StyledDropdownItemClipped>
|
||||||
</DropdownButton.StyledDropdownItem>
|
</DropdownMenuSelectableItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuItemContainer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderValueSelection(
|
function renderValueSelection(
|
||||||
@@ -131,7 +157,7 @@ export const FilterDropdownButton = <TData extends FilterableFieldsType>({
|
|||||||
|
|
||||||
<DropdownButton.StyledDropdownTopOptionAngleDown />
|
<DropdownButton.StyledDropdownTopOptionAngleDown />
|
||||||
</DropdownButton.StyledDropdownTopOption>
|
</DropdownButton.StyledDropdownTopOption>
|
||||||
<DropdownButton.StyledSearchField key={'search-filter'}>
|
<DropdownButton.StyledSearchField autoFocus key={'search-filter'}>
|
||||||
{['text', 'relation'].includes(selectedFilter.type) && (
|
{['text', 'relation'].includes(selectedFilter.type) && (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import type { Meta } from '@storybook/react';
|
import type { Meta } from '@storybook/react';
|
||||||
import { userEvent, within } from '@storybook/testing-library';
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
@@ -62,9 +63,16 @@ export const FilterByAccountOwner: Story = {
|
|||||||
delay: 200,
|
delay: 200,
|
||||||
});
|
});
|
||||||
|
|
||||||
const charlesChip = canvas.getByText('Charles Test', {
|
const charlesChip = canvas
|
||||||
selector: 'li > span',
|
.getAllByTestId('dropdown-menu-item')
|
||||||
|
.find((item) => {
|
||||||
|
return item.textContent === 'Charles Test';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(charlesChip).toBeInTheDocument();
|
||||||
|
|
||||||
|
assert(charlesChip);
|
||||||
|
|
||||||
await userEvent.click(charlesChip);
|
await userEvent.click(charlesChip);
|
||||||
|
|
||||||
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
|
expect(await canvas.findByText('Airbnb')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -164,11 +164,18 @@ export const accountOwnerFilter = {
|
|||||||
type: 'relation',
|
type: 'relation',
|
||||||
searchConfig: {
|
searchConfig: {
|
||||||
query: SEARCH_USER_QUERY,
|
query: SEARCH_USER_QUERY,
|
||||||
template: (searchString: string) => ({
|
template: (searchString: string, currentSelectedId?: string) => ({
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
displayName: {
|
displayName: {
|
||||||
contains: `%${searchString}%`,
|
contains: `%${searchString}%`,
|
||||||
mode: QueryMode.Insensitive,
|
mode: QueryMode.Insensitive,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: currentSelectedId ? { equals: currentSelectedId } : undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
resultMapper: (data: any) => ({
|
resultMapper: (data: any) => ({
|
||||||
value: data,
|
value: data,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import type { Meta } from '@storybook/react';
|
import type { Meta } from '@storybook/react';
|
||||||
import { userEvent, within } from '@storybook/testing-library';
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
|
||||||
@@ -59,7 +60,16 @@ export const CompanyName: Story = {
|
|||||||
delay: 200,
|
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);
|
await userEvent.click(qontoChip);
|
||||||
|
|
||||||
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
|
expect(await canvas.findByText('Alexandre Prot')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -100,8 +100,18 @@ export const companyFilter = {
|
|||||||
type: 'relation',
|
type: 'relation',
|
||||||
searchConfig: {
|
searchConfig: {
|
||||||
query: SEARCH_COMPANY_QUERY,
|
query: SEARCH_COMPANY_QUERY,
|
||||||
template: (searchString: string) => ({
|
template: (searchString: string, currentSelectedId?: string) => ({
|
||||||
name: { contains: `%${searchString}%`, mode: QueryMode.Insensitive },
|
OR: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
contains: `%${searchString}%`,
|
||||||
|
mode: QueryMode.Insensitive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: currentSelectedId ? { equals: currentSelectedId } : undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
resultMapper: (data) => ({
|
resultMapper: (data) => ({
|
||||||
value: data,
|
value: data,
|
||||||
|
|||||||
Reference in New Issue
Block a user