From 6d5577176dcf353be4c204b8ad1a4474393e25ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=AA=20Boaventura?= Date: Fri, 19 Apr 2024 10:48:02 -0300 Subject: [PATCH 01/10] create tag multi select --- src/components/ComboBox.tsx | 4 +- src/components/TagMultiSelect.tsx | 181 ++++++++++++++++++++++++++++++ src/index.ts | 1 + src/stories/ComboBox.stories.tsx | 55 +++++++++ 4 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/components/TagMultiSelect.tsx diff --git a/src/components/ComboBox.tsx b/src/components/ComboBox.tsx index ce832598..2e555b47 100644 --- a/src/components/ComboBox.tsx +++ b/src/components/ComboBox.tsx @@ -67,6 +67,7 @@ type ComboBoxProps = Exclude & { onDeleteChip?: (key: string) => void inputContent?: ComponentProps['inputContent'] onDeleteInputContent?: ComponentProps['onDeleteInputContent'] + containerProps?: HTMLAttributes } & Pick & Omit< ComboBoxStateOptions, @@ -251,6 +252,7 @@ function ComboBox({ chips, inputContent, onDeleteChip: onDeleteChipProp, + containerProps, ...props }: ComboBoxProps) { const nextFocusedKeyRef = useRef(null) @@ -514,7 +516,7 @@ function ComboBox({ ) return ( - + ) => void +}) { + const theme = useTheme() + const [selectedTagKeys, setSelectedTagKeys] = useState(new Set()) + const selectedTagArr = useMemo(() => [...selectedTagKeys], [selectedTagKeys]) + const [inputValue, setInputValue] = useState('') + const [isOpen, setIsOpen] = useState(false) + const [searchLogic, setSearchLogic] = useState(matchOptions[0].value) + + const fuse = useMemo( + () => + new Fuse(tags, { + includeScore: true, + shouldSort: true, + threshold: 0.3, + keys: ['name', 'value'], + }), + [tags] + ) + + const searchResults = useMemo(() => { + let ret: Fuse.FuseResult[] + + if (inputValue) { + ret = fuse.search(inputValue) + } else { + ret = tags.map((tag, i) => ({ item: tag, score: 1, refIndex: i })) + } + + return ret.filter((tag) => !selectedTagKeys.has(tagToKey(tag.item))) + }, [fuse, inputValue, selectedTagKeys, tags]) + + const onSelectionChange: ComponentProps< + typeof ComboBox + >['onSelectionChange'] = (key) => { + if (key) { + setSelectedTagKeys(new Set([...selectedTagArr, key])) + setInputValue('') + } + } + + useEffect(() => { + onChange?.(selectedTagKeys) + }, [selectedTagKeys, onChange]) + + const onInputChange: ComponentProps['onInputChange'] = ( + value + ) => { + setInputValue(value) + } + + return ( + + + ({ + key, + children: key, + }))} + onDeleteChip={(chipKey) => { + const newKeys = new Set(selectedTagKeys) + + newKeys.delete(chipKey) + setSelectedTagKeys(newKeys) + }} + inputProps={{ + placeholder: 'Search Tags...', + style: { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + backgroundColor: theme.colors['fill-one'], + }, + }} + onOpenChange={(isOpen, _trigger) => { + setIsOpen(isOpen) + }} + maxHeight={232} + allowsEmptyCollection + loading={loading} + containerProps={{ style: { flexGrow: 1 } }} + > + {searchResults + .map(({ item: tag, score: _score, refIndex: _refIndex }) => { + const tagStr = tagToKey(tag) + + if (selectedTagKeys.has(tagStr)) { + return null + } + + return ( + + {tagStr} + + } + textValue={tagStr} + /> + ) + }) + .filter(isNonNullable)} + + + ) +} diff --git a/src/index.ts b/src/index.ts index 137ce760..e1731c2a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -103,6 +103,7 @@ export type { export { default as LoadingSpinner } from './components/LoadingSpinner' export { default as LoopingLogo } from './components/LoopingLogo' export { ComboBox } from './components/ComboBox' +export { TagMultiSelect } from './components/TagMultiSelect' export { Toast, GraphQLToast } from './components/Toast' export { default as WrapWithIf } from './components/WrapWithIf' export type { diff --git a/src/stories/ComboBox.stories.tsx b/src/stories/ComboBox.stories.tsx index f7121fae..86a88bc5 100644 --- a/src/stories/ComboBox.stories.tsx +++ b/src/stories/ComboBox.stories.tsx @@ -3,6 +3,8 @@ import { type ComponentProps, type Key, useMemo, useState } from 'react' import styled from 'styled-components' import Fuse from 'fuse.js' +import { isEqual, uniqWith } from 'lodash-es' + import { AppIcon, BrowseAppsIcon, @@ -12,9 +14,12 @@ import { ListBoxFooterPlus, ListBoxItem, ListBoxItemChipList, + TagMultiSelect, WrapWithIf, } from '..' +import { type MultiSelectTag } from '../components/TagMultiSelect' + import { ClusterTagsTemplate } from './ClusterTagsTemplate' export default { @@ -486,3 +491,53 @@ ClusterTags.args = { loading: false, withTitleContent: false, } + +const TAGS = [ + { name: 'local', value: 'true' }, + { name: 'local', value: 'false' }, + { name: 'stage', value: 'dev' }, + { name: 'stage', value: 'prod' }, + { name: 'stage', value: 'canary' }, + { name: 'route', value: 'some-very-very-long-tag-value' }, + { name: 'route', value: 'short-name' }, + { name: 'local2', value: 'true' }, + { name: 'local2', value: 'false' }, + { name: 'stage2', value: 'dev' }, + { name: 'stage2', value: 'prod' }, + { name: 'stage2', value: 'canary' }, + { name: 'route2', value: 'some-very-very-long-tag-value' }, + { name: 'route2', value: 'short-name' }, +] +const tags = uniqWith(TAGS, isEqual) + +function TagMultiSelectTemplate({ + loading, + tags, + width, + onChange, +}: { + loading: boolean + tags: MultiSelectTag[] + width: number + onChange?: (keys: Set) => void +}) { + return ( +
+ +
+ ) +} + +export const TagMultiSelectStory = TagMultiSelectTemplate.bind({}) +TagMultiSelectStory.args = { + loading: false, + tags, + width: 100, + onChange: (keys: Set) => { + console.log('Selected keys:', keys) + }, +} From 25d4dcb91c765fb58a53967285dac6397b028173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=AA=20Boaventura?= Date: Mon, 22 Apr 2024 12:46:53 -0300 Subject: [PATCH 02/10] add on filter change prop --- src/components/TagMultiSelect.tsx | 14 ++++++++++---- src/stories/ComboBox.stories.tsx | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/TagMultiSelect.tsx b/src/components/TagMultiSelect.tsx index 706ff672..6d0345a6 100644 --- a/src/components/TagMultiSelect.tsx +++ b/src/components/TagMultiSelect.tsx @@ -30,11 +30,13 @@ const matchOptions = [ export function TagMultiSelect({ tags, loading, - onChange, + onSelectedTagsChange, + onFilterChange, }: { tags: MultiSelectTag[] loading: boolean - onChange?: (keys: Set) => void + onSelectedTagsChange?: (keys: Set) => void + onFilterChange?: (value: string) => void }) { const theme = useTheme() const [selectedTagKeys, setSelectedTagKeys] = useState(new Set()) @@ -76,8 +78,12 @@ export function TagMultiSelect({ } useEffect(() => { - onChange?.(selectedTagKeys) - }, [selectedTagKeys, onChange]) + onSelectedTagsChange?.(selectedTagKeys) + }, [selectedTagKeys, onSelectedTagsChange]) + + useEffect(() => { + onFilterChange?.(inputValue) + }, [inputValue, onFilterChange]) const onInputChange: ComponentProps['onInputChange'] = ( value diff --git a/src/stories/ComboBox.stories.tsx b/src/stories/ComboBox.stories.tsx index 86a88bc5..deb9e243 100644 --- a/src/stories/ComboBox.stories.tsx +++ b/src/stories/ComboBox.stories.tsx @@ -526,7 +526,7 @@ function TagMultiSelectTemplate({ ) From 4267971384dc91faf9605245a963c6a66fcf1b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=AA=20Boaventura?= Date: Mon, 22 Apr 2024 12:54:38 -0300 Subject: [PATCH 03/10] fix story name --- src/stories/ComboBox.stories.tsx | 30 +++----------------------- src/stories/TagMultiselectTemplate.tsx | 28 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 src/stories/TagMultiselectTemplate.tsx diff --git a/src/stories/ComboBox.stories.tsx b/src/stories/ComboBox.stories.tsx index deb9e243..5ac145cc 100644 --- a/src/stories/ComboBox.stories.tsx +++ b/src/stories/ComboBox.stories.tsx @@ -14,13 +14,11 @@ import { ListBoxFooterPlus, ListBoxItem, ListBoxItemChipList, - TagMultiSelect, WrapWithIf, } from '..' -import { type MultiSelectTag } from '../components/TagMultiSelect' - import { ClusterTagsTemplate } from './ClusterTagsTemplate' +import TagMultiSelectTemplate from './TagMultiselectTemplate' export default { title: 'Combo Box', @@ -510,30 +508,8 @@ const TAGS = [ ] const tags = uniqWith(TAGS, isEqual) -function TagMultiSelectTemplate({ - loading, - tags, - width, - onChange, -}: { - loading: boolean - tags: MultiSelectTag[] - width: number - onChange?: (keys: Set) => void -}) { - return ( -
- -
- ) -} - -export const TagMultiSelectStory = TagMultiSelectTemplate.bind({}) -TagMultiSelectStory.args = { +export const TagMultiSelect = TagMultiSelectTemplate.bind({}) +TagMultiSelect.args = { loading: false, tags, width: 100, diff --git a/src/stories/TagMultiselectTemplate.tsx b/src/stories/TagMultiselectTemplate.tsx new file mode 100644 index 00000000..24c1d4ca --- /dev/null +++ b/src/stories/TagMultiselectTemplate.tsx @@ -0,0 +1,28 @@ +import { type Key } from 'react' + +import { + type MultiSelectTag, + TagMultiSelect, +} from '../components/TagMultiSelect' + +export default function TagMultiSelectTemplate({ + loading, + tags, + width, + onChange, +}: { + loading: boolean + tags: MultiSelectTag[] + width: number + onChange?: (keys: Set) => void +}) { + return ( +
+ +
+ ) +} From 8aa566319296bc730904a10604574647fd40481b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=AA=20Boaventura?= Date: Mon, 22 Apr 2024 12:57:19 -0300 Subject: [PATCH 04/10] fix story args --- src/stories/ComboBox.stories.tsx | 5 ++++- src/stories/TagMultiselectTemplate.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/stories/ComboBox.stories.tsx b/src/stories/ComboBox.stories.tsx index 5ac145cc..2069971b 100644 --- a/src/stories/ComboBox.stories.tsx +++ b/src/stories/ComboBox.stories.tsx @@ -513,7 +513,10 @@ TagMultiSelect.args = { loading: false, tags, width: 100, - onChange: (keys: Set) => { + onSelectedTagsChange: (keys: Set) => { console.log('Selected keys:', keys) }, + onFilterChange: (filter: string) => { + console.log('Filter:', filter) + }, } diff --git a/src/stories/TagMultiselectTemplate.tsx b/src/stories/TagMultiselectTemplate.tsx index 24c1d4ca..ec0b679f 100644 --- a/src/stories/TagMultiselectTemplate.tsx +++ b/src/stories/TagMultiselectTemplate.tsx @@ -9,19 +9,22 @@ export default function TagMultiSelectTemplate({ loading, tags, width, - onChange, + onSelectedTagsChange, + onFilterChange, }: { loading: boolean tags: MultiSelectTag[] width: number - onChange?: (keys: Set) => void + onSelectedTagsChange?: (keys: Set) => void + onFilterChange?: (value: string) => void }) { return (
) From 756c8e6182c3ebaa77d845d22274df96144dd864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=AA=20Boaventura?= Date: Mon, 22 Apr 2024 13:18:56 -0300 Subject: [PATCH 05/10] filter results on parent --- src/components/TagMultiSelect.tsx | 37 +++----------------------- src/stories/ComboBox.stories.tsx | 2 +- src/stories/TagMultiselectTemplate.tsx | 11 +++----- 3 files changed, 9 insertions(+), 41 deletions(-) diff --git a/src/components/TagMultiSelect.tsx b/src/components/TagMultiSelect.tsx index 6d0345a6..627ce4f1 100644 --- a/src/components/TagMultiSelect.tsx +++ b/src/components/TagMultiSelect.tsx @@ -6,7 +6,6 @@ import { useMemo, useState, } from 'react' -import Fuse from 'fuse.js' import { useTheme } from 'styled-components' @@ -19,21 +18,18 @@ export type MultiSelectTag = { value: string } -function tagToKey(tag: MultiSelectTag) { - return `${tag.name}:${tag.value}` -} const matchOptions = [ { label: 'All', value: 'AND' }, { label: 'Any', value: 'OR' }, ] export function TagMultiSelect({ - tags, + options, loading, onSelectedTagsChange, onFilterChange, }: { - tags: MultiSelectTag[] + options: string[] loading: boolean onSelectedTagsChange?: (keys: Set) => void onFilterChange?: (value: string) => void @@ -45,29 +41,6 @@ export function TagMultiSelect({ const [isOpen, setIsOpen] = useState(false) const [searchLogic, setSearchLogic] = useState(matchOptions[0].value) - const fuse = useMemo( - () => - new Fuse(tags, { - includeScore: true, - shouldSort: true, - threshold: 0.3, - keys: ['name', 'value'], - }), - [tags] - ) - - const searchResults = useMemo(() => { - let ret: Fuse.FuseResult[] - - if (inputValue) { - ret = fuse.search(inputValue) - } else { - ret = tags.map((tag, i) => ({ item: tag, score: 1, refIndex: i })) - } - - return ret.filter((tag) => !selectedTagKeys.has(tagToKey(tag.item))) - }, [fuse, inputValue, selectedTagKeys, tags]) - const onSelectionChange: ComponentProps< typeof ComboBox >['onSelectionChange'] = (key) => { @@ -156,10 +129,8 @@ export function TagMultiSelect({ loading={loading} containerProps={{ style: { flexGrow: 1 } }} > - {searchResults - .map(({ item: tag, score: _score, refIndex: _refIndex }) => { - const tagStr = tagToKey(tag) - + {options + .map((tagStr) => { if (selectedTagKeys.has(tagStr)) { return null } diff --git a/src/stories/ComboBox.stories.tsx b/src/stories/ComboBox.stories.tsx index 2069971b..c288ec02 100644 --- a/src/stories/ComboBox.stories.tsx +++ b/src/stories/ComboBox.stories.tsx @@ -511,7 +511,7 @@ const tags = uniqWith(TAGS, isEqual) export const TagMultiSelect = TagMultiSelectTemplate.bind({}) TagMultiSelect.args = { loading: false, - tags, + options: tags.map((tag) => `${tag.name}:${tag.value}`), width: 100, onSelectedTagsChange: (keys: Set) => { console.log('Selected keys:', keys) diff --git a/src/stories/TagMultiselectTemplate.tsx b/src/stories/TagMultiselectTemplate.tsx index ec0b679f..2ca7220e 100644 --- a/src/stories/TagMultiselectTemplate.tsx +++ b/src/stories/TagMultiselectTemplate.tsx @@ -1,19 +1,16 @@ import { type Key } from 'react' -import { - type MultiSelectTag, - TagMultiSelect, -} from '../components/TagMultiSelect' +import { TagMultiSelect } from '../components/TagMultiSelect' export default function TagMultiSelectTemplate({ loading, - tags, + options, width, onSelectedTagsChange, onFilterChange, }: { loading: boolean - tags: MultiSelectTag[] + options: string[] width: number onSelectedTagsChange?: (keys: Set) => void onFilterChange?: (value: string) => void @@ -22,7 +19,7 @@ export default function TagMultiSelectTemplate({
From 3367108d68a2b5c5d27e19830f0ba95def4dc85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=AA=20Boaventura?= Date: Mon, 22 Apr 2024 16:00:00 -0300 Subject: [PATCH 06/10] add match type callback --- src/components/TagMultiSelect.tsx | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/TagMultiSelect.tsx b/src/components/TagMultiSelect.tsx index 627ce4f1..2ca7d930 100644 --- a/src/components/TagMultiSelect.tsx +++ b/src/components/TagMultiSelect.tsx @@ -21,19 +21,23 @@ export type MultiSelectTag = { const matchOptions = [ { label: 'All', value: 'AND' }, { label: 'Any', value: 'OR' }, -] +] as const -export function TagMultiSelect({ - options, - loading, - onSelectedTagsChange, - onFilterChange, -}: { +type TagMultiSelectProps = { options: string[] loading: boolean onSelectedTagsChange?: (keys: Set) => void onFilterChange?: (value: string) => void -}) { + onChangeMatchType?: (value: 'AND' | 'OR') => void +} + +function TagMultiSelect({ + options, + loading, + onSelectedTagsChange, + onFilterChange, + onChangeMatchType, +}: TagMultiSelectProps) { const theme = useTheme() const [selectedTagKeys, setSelectedTagKeys] = useState(new Set()) const selectedTagArr = useMemo(() => [...selectedTagKeys], [selectedTagKeys]) @@ -72,8 +76,9 @@ export function TagMultiSelect({