Skip to content

RI-7190 Field Box component #4716

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

Open
wants to merge 4 commits into
base: feature/RI-6855/vector-search
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react'
import { cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils'
import { CreateIndexStepWrapper } from './CreateIndexStepWrapper'

const renderComponent = () => render(<CreateIndexStepWrapper />)

describe('CreateIndexStepWrapper', () => {
beforeEach(() => {
cleanup()
})

it('should render', () => {
const { container } = renderComponent()

expect(container).toBeTruthy()

// Check if the tabs are rendered
const buildNewIndexTabTrigger = screen.getByText('Build new index')
const usePresetIndexTabTrigger = screen.getByText('Use preset index')

expect(buildNewIndexTabTrigger).toBeInTheDocument()
expect(usePresetIndexTabTrigger).toBeInTheDocument()

// Check if the "Use preset index" tab content is selected by default
const usePresetIIndexTabContent = screen.queryByTestId(
'vector-inde-tabs--use-preset-index-content',
)
expect(usePresetIIndexTabContent).toBeInTheDocument()
})

it('should switch to "Use preset index" tab when clicked', () => {
renderComponent()

const buildNewIndexTabTrigger = screen.getByText('Use preset index')
fireEvent.click(buildNewIndexTabTrigger)

// Check if the "Use preset index" tab is rendered
const buildNewIndexTabContent = screen.queryByTestId(
'vector-inde-tabs--use-preset-index-content',
)
expect(buildNewIndexTabContent).toBeInTheDocument()
})

it('shouldn\'t switch to "Build new index" tab when clicked, since it is disabled', () => {
renderComponent()

const buildNewIndexTabTriggerLabel = screen.getByText('Build new index')
const buildNewIndexTabTriggerButton =
buildNewIndexTabTriggerLabel.closest('[type="button"]')

expect(buildNewIndexTabTriggerButton).toHaveAttribute('disabled')
expect(buildNewIndexTabTriggerButton).toHaveAttribute('data-disabled')

// And when clicked, it should not change the active tab
fireEvent.click(buildNewIndexTabTriggerLabel)

// Check if the "Use preset index" tab is still active
const usePresetIndexTabContent = screen.queryByTestId(
'vector-inde-tabs--use-preset-index-content',
)
expect(usePresetIndexTabContent).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react'
import { TabsProps } from '@redis-ui/components'
import Tabs, { TabInfo } from 'uiSrc/components/base/layout/tabs'
import { BuildNewIndexTabTrigger } from './build-new-index-tab/BuildNewIndexTabTrigger'

export enum VectorIndexTab {
BuildNewIndex = 'build-new-index',
UsePresetIndex = 'use-preset-index',
}

const VECTOR_INDEX_TABS: TabInfo<string>[] = [
{
value: VectorIndexTab.BuildNewIndex,
label: <BuildNewIndexTabTrigger />,
content: null,
disabled: true,
},
{
value: VectorIndexTab.UsePresetIndex,
label: 'Use preset index',
content: (
<div data-testid="vector-inde-tabs--use-preset-index-content">
TODO: Add content later
</div>
),
},
]

export const CreateIndexStepWrapper = (props: Partial<TabsProps>) => {
const { tabs, defaultValue, ...rest } = props

return (
<Tabs
tabs={tabs ?? VECTOR_INDEX_TABS}
defaultValue={defaultValue ?? VectorIndexTab.UsePresetIndex}
variant="sub"
{...rest}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import styled from 'styled-components'

export const StyledBuildNewIndexTabTrigger = styled.div`
display: flex;
align-items: center;
gap: ${({ theme }) => theme.core.space.space050};
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import { Badge } from '@redis-ui/components'
import { StyledBuildNewIndexTabTrigger } from './BuildNewIndexTabTrigger.styles'

export const BuildNewIndexTabTrigger = () => (
<StyledBuildNewIndexTabTrigger>
Build new index <Badge label="Coming soon" />
</StyledBuildNewIndexTabTrigger>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react'
import { BoxSelectionGroup } from '@redis-ui/components'

import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'
import { MOCK_VECTOR_SEARCH_BOX } from 'uiSrc/constants/mocks/mock-vector-index-search'
import { cleanup, fireEvent, render, screen } from 'uiSrc/utils/test-utils'

import { FieldBox, FieldBoxProps } from './FieldBox'
import { VectorSearchBox } from './types'

const renderFieldBoxComponent = (props?: FieldBoxProps) => {
const defaultProps: FieldBoxProps = {
box: MOCK_VECTOR_SEARCH_BOX,
}

return render(
<BoxSelectionGroup.Compose>
<FieldBox {...defaultProps} {...props} />
</BoxSelectionGroup.Compose>,
)
}

describe('CreateIndexStepWrapper', () => {
beforeEach(() => {
cleanup()
})

it('should render', () => {
const props: FieldBoxProps = {
box: {
...MOCK_VECTOR_SEARCH_BOX,
value: 'id',
label: 'id',
text: 'Unique product identifier',
tag: FieldTypes.TAG,
disabled: false,
},
}

const { container } = renderFieldBoxComponent(props)

expect(container).toBeTruthy()

// Check if the box is rendered with the correct visual elements
const label = screen.getByText(props.box.label!)
const description = screen.getByText(props.box.text!)
const tag = screen.getByText(props.box.tag.toUpperCase()!)
const checkbox = screen.getByRole('checkbox')

expect(label).toBeInTheDocument()
expect(description).toBeInTheDocument()
expect(tag).toBeInTheDocument()
expect(checkbox).toBeInTheDocument()
})

it('should select the box when clicked', () => {
renderFieldBoxComponent()

const checkbox = screen.getByRole('checkbox')
expect(checkbox).not.toBeChecked()

const box = screen.getByTestId(`field-box-${MOCK_VECTOR_SEARCH_BOX.value}`)
fireEvent.click(box)

expect(checkbox).toBeChecked()
})

it('should not select the box when clicked if disabled', () => {
const disabledBox: VectorSearchBox = {
...MOCK_VECTOR_SEARCH_BOX,
disabled: true,
}

renderFieldBoxComponent({ box: disabledBox })

const checkbox = screen.getByRole('checkbox')
expect(checkbox).not.toBeChecked()

const box = screen.getByTestId(`field-box-${disabledBox.value}`)
fireEvent.click(box)

expect(checkbox).not.toBeChecked()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BoxSelectionGroup } from '@redis-ui/components'
import styled from 'styled-components'

export const StyledFieldBox = styled(BoxSelectionGroup.Item.Compose)`
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
padding: ${({ theme }) => theme.core.space.space100};
gap: ${({ theme }) => theme.components.boxSelectionGroup.defaultItem.gap};
`

export const BoxHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`

export const BoxHeaderActions = styled.div`
display: flex;
align-items: center;
gap: ${({ theme }) => theme.components.boxSelectionGroup.defaultItem.gap};
`

export const BoxContent = styled.div``
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'
import {
BoxSelectionGroup,
BoxSelectionGroupItemComposeProps,
Checkbox,
} from '@redis-ui/components'

import { EditIcon } from 'uiSrc/components/base/icons'
import { IconButton } from 'uiSrc/components/base/forms/buttons/IconButton'
import { Text } from 'uiSrc/components/base/text'

import {
BoxContent,
BoxHeader,
BoxHeaderActions,
StyledFieldBox,
} from './FieldBox.styles'
import { FieldTag } from './FieldTag'
import { VectorSearchBox } from './types'

export interface FieldBoxProps extends BoxSelectionGroupItemComposeProps {
box: VectorSearchBox
}

export const FieldBox = ({ box, ...rest }: FieldBoxProps) => {
const { label, text, tag, disabled } = box

return (
<StyledFieldBox box={box} data-testid={`field-box-${box.value}`} {...rest}>
<BoxHeader>
<BoxSelectionGroup.Item.StateIndicator>
{(props) => <Checkbox {...props} />}
</BoxSelectionGroup.Item.StateIndicator>

<BoxHeaderActions>
<FieldTag tag={tag} />
<IconButton icon={EditIcon} size="XL" disabled={disabled} />
</BoxHeaderActions>
</BoxHeader>
<BoxContent>
<Text size="L" variant="semiBold">
{label}
</Text>

{text && (
<Text size="L" color="secondary" ellipsis tooltipOnEllipsis>
{text}
</Text>
)}
</BoxContent>
</StyledFieldBox>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react'
import { Badge } from '@redis-ui/components'
import {
FIELD_TYPE_OPTIONS,
FieldTypes,
} from 'uiSrc/pages/browser/components/create-redisearch-index/constants'

// TODO: Add colors mapping for tags when @redis-ui/components v38.6.0 is released
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Badge component in @redis-ui/components v38.6.0 supports custom color out of the box (docs), so we can colorize the different types of fields, once we update the dependency version.

Note: Currently @redis-ui/components and @redis-ui/styles. Unfortunately, the latter comes with some breaking changes in its latest version, so the upgrade process might need additional effort, and I won't do it as part of this task.

export const FieldTag = ({ tag }: { tag: FieldTypes }) => {
const tagLabel = FIELD_TYPE_OPTIONS.find(
(option) => option.value === tag,
)?.text

return tagLabel ? <Badge label={tagLabel} /> : null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BoxSelectionGroupBox } from '@redis-ui/components'
import { FieldTypes } from 'uiSrc/pages/browser/components/create-redisearch-index/constants'

export interface VectorSearchBox extends BoxSelectionGroupBox {
text: string
tag: FieldTypes
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'

import { fireEvent, render, screen } from 'uiSrc/utils/test-utils'
import { MOCK_VECTOR_SEARCH_BOX } from 'uiSrc/constants/mocks/mock-vector-index-search'
import { FieldBoxesGroup, FieldBoxesGroupProps } from './FieldBoxesGroup'

const renderFieldBoxesGroupComponent = (
props?: Partial<FieldBoxesGroupProps>,
) => {
const defaultProps: FieldBoxesGroupProps = {
boxes: [MOCK_VECTOR_SEARCH_BOX],
value: [MOCK_VECTOR_SEARCH_BOX.value],
onChange: jest.fn(),
}

return render(<FieldBoxesGroup {...defaultProps} {...props} />)
}

describe('FieldBoxesGroup', () => {
it('should render', () => {
const { container } = renderFieldBoxesGroupComponent()

expect(container).toBeTruthy()

const fieldBoxesGroup = screen.getByTestId('field-boxes-group')
expect(fieldBoxesGroup).toBeInTheDocument()

const fieldBoxes = screen.getAllByTestId(/field-box-/)
expect(fieldBoxes).toHaveLength(1)
})

it('should call onChange when clicking on a box to select it', () => {
const onChangeMock = jest.fn()
const value: string[] = []

renderFieldBoxesGroupComponent({ value, onChange: onChangeMock })

const box = screen.getByTestId(`field-box-${MOCK_VECTOR_SEARCH_BOX.value}`)

fireEvent.click(box)
expect(onChangeMock).toHaveBeenCalledWith([MOCK_VECTOR_SEARCH_BOX.value])
})

it('should call onChange when clicking on a box to deselect it', () => {
const onChangeMock = jest.fn()
const value: string[] = [MOCK_VECTOR_SEARCH_BOX.value]

renderFieldBoxesGroupComponent({ value, onChange: onChangeMock })

const box = screen.getByTestId(`field-box-${MOCK_VECTOR_SEARCH_BOX.value}`)

fireEvent.click(box)
expect(onChangeMock).toHaveBeenCalledWith([])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styled from 'styled-components'
import { MultiBoxSelectionGroup } from '@redis-ui/components'

export const StyledFieldBoxesGroup = styled(MultiBoxSelectionGroup.Compose)`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: ${({ theme }) => theme.core.space.space150};
align-items: flex-start;
align-self: stretch;
`
Loading
Loading