Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Issue #2369 Regarding Fuzzy Search #2437

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion ui/src/dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -880,4 +880,88 @@ describe('Dropdown.tsx', () => {
})
})
})
})

describe('Dialog dropdown exact', () => {
let dialogProps: Dropdown

const createChoices = (size: number) => Array.from(Array(size).keys()).map(key => ({ name: String(key), label: `Choice ${key}` }))
const overOneHundredChoices = createChoices(101)
const choices = createChoices(10)

beforeEach(() => {
dialogProps = {
...defaultProps,
popup: 'always',
exactSearch: true,
choices
};
});
it('Sets correct args after exact filter', () => {
const { getByText, getByTestId, getAllByRole } = render(<XDropdown model={{ ...dialogProps, values: ['1'] }} />)

fireEvent.click(getByTestId(name))
fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } })
fireEvent.click(getAllByRole('checkbox')[0])
fireEvent.click(getByText('Select'))

expect(wave.args[name]).toMatchObject(['1', '9'])
});
it('Filters exact correctly', () => {
const { getByTestId, getAllByRole } = render(<XDropdown model={{ ...dialogProps, values: [] }} />)

fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } })
expect(getAllByRole('listitem')).toHaveLength(1)
});

it('Filters correctly - reset filter', () => {
const { getByTestId, getAllByRole } = render(<XDropdown model={{ ...dialogProps, values: [] }} />)

fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } })
expect(getAllByRole('listitem')).toHaveLength(1)

fireEvent.change(getByTestId(`${name}-search`), { target: { value: '' } })
expect(getAllByRole('listitem')).toHaveLength(10)
});

it('Resets filtered items on cancel', () => {
const { getByTestId, getAllByRole, getByText } = render(<XDropdown model={{ ...dialogProps, values: [] }} />)

fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } })
expect(getAllByRole('listitem')).toHaveLength(1)
fireEvent.click(getByText('Cancel'))
fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
});

it('Resets filtered items on submit', () => {
const { getByTestId, getAllByRole, getByText } = render(<XDropdown model={{ ...dialogProps, values: [] }} />)

fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } })
expect(getAllByRole('listitem')).toHaveLength(1)
fireEvent.click(getByText('Select'))
fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
});

it('Resets filtered items on single valued submit', () => {
const { getByTestId, getAllByRole } = render(<XDropdown model={dialogProps} />)

fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
fireEvent.change(getByTestId(`${name}-search`), { target: { value: 'Choice 9' } })
expect(getAllByRole('listitem')).toHaveLength(1)
fireEvent.click(getAllByRole('checkbox')[0])
fireEvent.click(getByTestId(name))
expect(getAllByRole('listitem')).toHaveLength(10)
});

})
})
19 changes: 15 additions & 4 deletions ui/src/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { B, Id, S, U } from './core'
import React from 'react'
import { stylesheet } from 'typestyle'
import { Choice } from './choice_group'
import { fuzzysearch } from './parts/utils'
import { fuzzysearch, exactsearch } from './parts/utils'
import { clas, cssVar, pc, px } from './theme'
import { wave } from './ui'

Expand Down Expand Up @@ -61,6 +61,8 @@ export interface Dropdown {
tooltip?: S
/** Whether to present the choices using a pop-up dialog. By default pops up a dialog only for more than 100 choices. Defaults to 'auto'. */
popup?: 'auto' | 'always' | 'never'
/**Whether the search will be exact or fuzzy */
exactSearch?: B
Comment on lines +64 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**Whether the search will be exact or fuzzy */
exactSearch?: B
/** Whether the dropdown search will be exact or fuzzy. Defaults to True. */
fuzzy_search?: B

}

type DropdownItem = {
Expand Down Expand Up @@ -170,10 +172,19 @@ const
getPageSpecification = () => ({ itemCount: PAGE_SIZE, height: ROW_HEIGHT * PAGE_SIZE } as Fluent.IPageSpecification),
choicesToItems = (choices: Choice[] = [], v?: S | S[]) => choices.map(({ name, label, disabled = false }, idx) =>
({ name, text: label || name, idx, checked: Array.isArray(v) ? v.includes(name) : v === name, show: true, disabled })),
useItems = (choices?: Choice[], v?: S | S[]) => {
useItems = (choices?: Choice[], v?: S | S[], exactSearch?: boolean) => {
const [items, setItems] = React.useState<DropdownItem[]>(choicesToItems(choices, v))
const onSearchChange = (_e?: React.ChangeEvent<HTMLInputElement>, newVal = '') => setItems(items => items.map(i => ({ ...i, show: fuzzysearch(i.text, newVal) })))

const onSearchChange = (_e?: React.ChangeEvent<HTMLInputElement>, newVal = '') => {
setItems((items) =>
items.map((i) => ({
...i,
show: exactSearch
? exactsearch(i.text, newVal) // Assuming exactsearch is a function for exact matching
: fuzzysearch(i.text, newVal), // Assuming fuzzysearch is a function for fuzzy matching
Comment on lines +183 to +184
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for extra comments.

Suggested change
? exactsearch(i.text, newVal) // Assuming exactsearch is a function for exact matching
: fuzzysearch(i.text, newVal), // Assuming fuzzysearch is a function for fuzzy matching
? exactsearch(i.text, newVal)
: fuzzysearch(i.text, newVal),

}))
);
};
return [items, setItems, onSearchChange] as const
},
onRenderCell = (onChecked: any) => (item?: DropdownItem) => item
Expand Down Expand Up @@ -323,4 +334,4 @@ export const XDropdown = ({ model: m }: { model: Dropdown }) =>
? <BaseDropdown model={m} />
: (m.choices?.length || 0) > 100
? <DialogDropdown model={m} />
: <BaseDropdown model={m} />
: <BaseDropdown model={m} />
10 changes: 9 additions & 1 deletion ui/src/parts/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ export function fuzzysearch(haystack: S, needle: S) {
}
return true
}
// parts / utils.ts
export const exactsearch = (searchTerm: string, itemText: string): boolean => {

// Convert both strings to lowercase for case-insensitive comparison
return itemText.toLowerCase() === searchTerm.toLowerCase(); // Exact match
};
Comment on lines +27 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for a separate function since it's so simple, can be inlined.




// https://github.com/h2oai/wave/issues/1395.
export const fixMenuOverflowStyles: Partial<IContextualMenuStyles> = {
Expand All @@ -32,4 +40,4 @@ export const fixMenuOverflowStyles: Partial<IContextualMenuStyles> = {
'.ms-ContextualMenu-link': { lineHeight: 'unset' },
'.ms-ContextualMenu-submenuIcon': { lineHeight: 'unset', display: 'flex', alignItems: 'center' },
}
}
}