Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit f7c253f

Browse files
authored
colored tags search filter list (#1058)
1 parent 36c5293 commit f7c253f

File tree

7 files changed

+352
-31
lines changed

7 files changed

+352
-31
lines changed

‎frontend/apps/ui/src/features/search/__tests__/microcomp/scanSearchText.test.ts‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import {describe, it, expect} from "vitest"
21
import {scanSearchText} from "@/features/search/microcomp/scanner"
2+
import {describe, expect, it} from "vitest"
3+
import {FILTERS} from "../../microcomp/const"
34

45
describe("scanSearchText", () => {
56
//----------------------------------------------
@@ -66,17 +67,22 @@ describe("scanSearchText", () => {
6667
})
6768

6869
//----------------------------------------------
69-
it("should not suggest tag completion if there is no comma at the end - with spaces", () => {
70-
const input = "some text tag:invoice " // no comma, however there are some spaces
70+
it("should return complete token if there one space at the end", () => {
71+
const input = "some text tag:invoice " // one space at the end
7172
const result = scanSearchText(input)
7273

7374
expect(result.hasSuggestions).toBe(true)
75+
expect(result.tokenIsComplete).toBe(true)
76+
expect(result.token).toEqual({
77+
type: "tag",
78+
operator: "all",
79+
values: ["invoice"]
80+
})
81+
7482
expect(result.suggestions).toEqual([
75-
{type: "operator", items: []},
7683
{
77-
type: "tag",
78-
exclude: [],
79-
filter: "invoice"
84+
type: "filter",
85+
items: FILTERS.sort()
8086
}
8187
])
8288
})

‎frontend/apps/ui/src/features/search/components/SearchTokens/TagToken/TagToken.container.tsx‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {useAppDispatch, useAppSelector} from "@/app/hooks"
22
import {TagOperator, TagToken} from "@/features/search/microcomp/types"
33
import {updateToken} from "@/features/search/storage/search"
44
import {TagTokenPresentation} from "./TagToken.presentation"
5+
import {useTagTokenLogic} from "./useTagToken"
56

67
interface TagTokenContainerProps {
78
index: number
@@ -11,6 +12,7 @@ export function TagTokenContainer({index}: TagTokenContainerProps) {
1112
const dispatch = useAppDispatch()
1213
const token = useAppSelector(state => state.search.tokens[index]) as TagToken
1314

15+
// Redux handlers
1416
const handleOperatorChange = (operator: TagOperator) => {
1517
dispatch(updateToken({index, updates: {operator}}))
1618
}
@@ -19,11 +21,28 @@ export function TagTokenContainer({index}: TagTokenContainerProps) {
1921
dispatch(updateToken({index, updates: {values}}))
2022
}
2123

24+
// Business logic hook
25+
const tagLogic = useTagTokenLogic({
26+
selectedTagNames: token.values || [],
27+
onValuesChange: handleValuesChange
28+
})
29+
2230
return (
2331
<TagTokenPresentation
2432
item={token}
2533
onOperatorChange={handleOperatorChange}
26-
onValuesChange={handleValuesChange}
34+
selectedTags={tagLogic.selectedTags}
35+
availableTags={tagLogic.availableTags}
36+
search={tagLogic.search}
37+
isLoading={tagLogic.isLoading}
38+
combobox={tagLogic.combobox}
39+
onValueSelect={tagLogic.handleValueSelect}
40+
onValueRemove={tagLogic.handleValueRemove}
41+
onSearchChange={tagLogic.handleSearchChange}
42+
onBackspace={tagLogic.handleBackspace}
43+
onToggleDropdown={tagLogic.toggleDropdown}
44+
onOpenDropdown={tagLogic.openDropdown}
45+
onCloseDropdown={tagLogic.closeDropdown}
2746
/>
2847
)
2948
}

‎frontend/apps/ui/src/features/search/components/SearchTokens/TagToken/TagToken.presentation.tsx‎

Lines changed: 187 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,76 @@
11
import {TagOperator, TagToken} from "@/features/search/microcomp/types"
2-
import {Box, Group, MultiSelect, Select, Text} from "@mantine/core"
2+
import {ColoredTag} from "@/types"
3+
import {
4+
Box,
5+
Combobox,
6+
Group,
7+
Pill,
8+
PillsInput,
9+
Select,
10+
Text,
11+
useCombobox
12+
} from "@mantine/core"
313
import styles from "./TagToken.module.css"
414

515
interface TagTokenPresentationProps {
616
item: TagToken
17+
selectedTags: ColoredTag[]
18+
availableTags: ColoredTag[]
719
onOperatorChange?: (operator: TagOperator) => void
8-
onValuesChange?: (values: string[]) => void
20+
search?: string
21+
isLoading?: boolean
22+
combobox?: ReturnType<typeof useCombobox>
23+
onValueSelect?: (tagName: string) => void
24+
onValueRemove?: (tagName: string) => void
25+
onSearchChange?: (value: string) => void
26+
onBackspace?: () => void
27+
onToggleDropdown?: () => void
28+
onOpenDropdown?: () => void
29+
onCloseDropdown?: () => void
930
}
1031

1132
export function TagTokenPresentation({
1233
item,
34+
selectedTags,
35+
availableTags,
1336
onOperatorChange,
14-
onValuesChange
37+
search = "",
38+
isLoading = false,
39+
combobox,
40+
onValueSelect,
41+
onValueRemove,
42+
onSearchChange,
43+
onBackspace,
44+
onToggleDropdown,
45+
onOpenDropdown,
46+
onCloseDropdown
1547
}: TagTokenPresentationProps) {
48+
// Fallback combobox for Storybook/testing
49+
const fallbackCombobox = useCombobox({
50+
onDropdownClose: () => fallbackCombobox.resetSelectedOption(),
51+
onDropdownOpen: () => fallbackCombobox.updateSelectedOptionIndex("active")
52+
})
53+
const comboboxStore = combobox || fallbackCombobox
54+
1655
return (
1756
<Box className={styles.tokenContainer} onClick={e => e.stopPropagation()}>
1857
<Group gap={0}>
1958
<Text c={"blue"}>tag:</Text>
2059
<TokenTagOperator item={item} onOperatorChange={onOperatorChange} />
21-
<TokenTagValues item={item} onValuesChange={onValuesChange} />
60+
<TokenTagValues
61+
selectedTags={selectedTags}
62+
availableTags={availableTags}
63+
search={search}
64+
isLoading={isLoading}
65+
combobox={comboboxStore}
66+
onValueSelect={onValueSelect}
67+
onValueRemove={onValueRemove}
68+
onSearchChange={onSearchChange}
69+
onBackspace={onBackspace}
70+
onToggleDropdown={onToggleDropdown}
71+
onOpenDropdown={onOpenDropdown}
72+
onCloseDropdown={onCloseDropdown}
73+
/>
2274
</Group>
2375
</Box>
2476
)
@@ -52,23 +104,140 @@ function TokenTagOperator({item, onOperatorChange}: TokenTagOperatorProps) {
52104
}
53105

54106
interface TokenTagValuesProps {
55-
item: TagToken
56-
onValuesChange?: (values: string[]) => void
107+
selectedTags: ColoredTag[]
108+
availableTags: ColoredTag[]
109+
search?: string
110+
isLoading?: boolean
111+
combobox?: ReturnType<typeof useCombobox>
112+
onValueSelect?: (tagName: string) => void
113+
onValueRemove?: (tagName: string) => void
114+
onSearchChange?: (value: string) => void
115+
onBackspace?: () => void
116+
onToggleDropdown?: () => void
117+
onOpenDropdown?: () => void
118+
onCloseDropdown?: () => void
57119
}
58120

59-
function TokenTagValues({item, onValuesChange}: TokenTagValuesProps) {
60-
const handleChange = (values: string[]) => {
61-
if (onValuesChange) {
62-
onValuesChange(values)
63-
}
64-
}
121+
function TokenTagValues({
122+
selectedTags,
123+
availableTags,
124+
search = "",
125+
isLoading = false,
126+
combobox,
127+
onValueSelect,
128+
onValueRemove,
129+
onSearchChange,
130+
onBackspace,
131+
onToggleDropdown,
132+
onOpenDropdown,
133+
onCloseDropdown
134+
}: TokenTagValuesProps) {
135+
// Fallback combobox for Storybook/testing
136+
const fallbackCombobox = useCombobox({
137+
onDropdownClose: () => fallbackCombobox.resetSelectedOption(),
138+
onDropdownOpen: () => fallbackCombobox.updateSelectedOptionIndex("active")
139+
})
140+
const comboboxStore = combobox || fallbackCombobox
141+
142+
// Render selected tag pills with colors
143+
const values = selectedTags.map(tag => (
144+
<Pill
145+
key={tag.name}
146+
withRemoveButton
147+
onRemove={() => onValueRemove?.(tag.name)}
148+
style={{
149+
backgroundColor: tag.bg_color,
150+
color: tag.fg_color
151+
}}
152+
>
153+
{tag.name}
154+
</Pill>
155+
))
65156

66157
return (
67-
<MultiSelect
68-
data={item.values || []}
69-
value={item.values || []}
70-
onChange={handleChange}
71-
onClick={e => e.stopPropagation()}
72-
/>
158+
<Combobox
159+
store={comboboxStore}
160+
onOptionSubmit={val => onValueSelect?.(val)}
161+
withinPortal={true}
162+
>
163+
<Combobox.DropdownTarget>
164+
<PillsInput
165+
pointer
166+
onClick={e => {
167+
e.stopPropagation()
168+
onToggleDropdown?.()
169+
}}
170+
size="sm"
171+
>
172+
<Pill.Group>
173+
{values}
174+
<Combobox.EventsTarget>
175+
<PillsInput.Field
176+
onFocus={() => onOpenDropdown?.()}
177+
onBlur={() => onCloseDropdown?.()}
178+
value={search}
179+
placeholder={
180+
selectedTags.length === 0 ? "Select tags" : undefined
181+
}
182+
onChange={event => onSearchChange?.(event.currentTarget.value)}
183+
onClick={e => e.stopPropagation()}
184+
style={{width: "80px", minWidth: "80px"}}
185+
/>
186+
</Combobox.EventsTarget>
187+
</Pill.Group>
188+
</PillsInput>
189+
</Combobox.DropdownTarget>
190+
191+
<Combobox.Dropdown
192+
onClick={e => e.stopPropagation()}
193+
style={{
194+
zIndex: 1000,
195+
position: "absolute"
196+
}}
197+
>
198+
<Combobox.Options>
199+
<TagOptionsList
200+
isLoading={isLoading}
201+
search={search}
202+
availableTags={availableTags}
203+
/>
204+
</Combobox.Options>
205+
</Combobox.Dropdown>
206+
</Combobox>
73207
)
74208
}
209+
210+
interface TagListArgs {
211+
isLoading: boolean
212+
search: string
213+
availableTags: ColoredTag[]
214+
}
215+
216+
function TagOptionsList({isLoading, search, availableTags}: TagListArgs) {
217+
if (isLoading) {
218+
return <Combobox.Empty>Loading tags...</Combobox.Empty>
219+
}
220+
221+
if (availableTags.length === 0) {
222+
return (
223+
<Combobox.Empty>
224+
{search ? "No tags found" : "All tags selected"}
225+
</Combobox.Empty>
226+
)
227+
}
228+
229+
return availableTags.map(tag => (
230+
<Combobox.Option value={tag.name} key={tag.name}>
231+
<Group gap="sm">
232+
<Pill
233+
style={{
234+
backgroundColor: tag.bg_color,
235+
color: tag.fg_color
236+
}}
237+
>
238+
{tag.name}
239+
</Pill>
240+
</Group>
241+
</Combobox.Option>
242+
))
243+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /