diff --git a/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.styles.ts b/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.styles.ts deleted file mode 100644 index 0959f0ed7..000000000 --- a/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.styles.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { styled as createStyled, type Theme } from 'baseui'; - -export const styled = { - FiltersContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ - marginTop: $theme.sizing.scale950, - marginBottom: $theme.sizing.scale900, - })), -}; diff --git a/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.tsx b/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.tsx deleted file mode 100644 index 4eb2151bb..000000000 --- a/src/views/domain-workflows/domain-workflows-filters/domain-workflows-filters.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; -import PageFilters from '@/components/page-filters/page-filters'; -import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; - -import domainWorkflowsFiltersConfig from '../config/domain-workflows-filters.config'; - -import { styled } from './domain-workflows-filters.styles'; - -export default function DomainWorkflowsFilters() { - return ( - - - - ); -} diff --git a/src/views/domain-workflows/domain-workflows-header/__tests__/domain-workflows-header.test.tsx b/src/views/domain-workflows/domain-workflows-header/__tests__/domain-workflows-header.test.tsx new file mode 100644 index 000000000..fc587dcde --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-header/__tests__/domain-workflows-header.test.tsx @@ -0,0 +1,97 @@ +import { render, screen, userEvent } from '@/test-utils/rtl'; + +import * as usePageFiltersModule from '@/components/page-filters/hooks/use-page-filters'; +import { type Props as PageFiltersToggleProps } from '@/components/page-filters/page-filters-toggle/page-filters-toggle.types'; + +import { mockDomainWorkflowsQueryParamsValues } from '../../__fixtures__/domain-workflows-query-params'; +import DomainWorkflowsHeader from '../domain-workflows-header'; + +jest.mock( + '@/components/page-filters/page-filters-search/page-filters-search', + () => jest.fn(() =>
Filter search
) +); + +jest.mock( + '@/components/page-filters/page-filters-fields/page-filters-fields', + () => jest.fn(() =>
Filter fields
) +); + +jest.mock( + '@/components/page-filters/page-filters-toggle/page-filters-toggle', + () => + jest.fn((props: PageFiltersToggleProps) => ( + + )) +); + +jest.mock( + '../../domain-workflows-query-input/domain-workflows-query-input', + () => jest.fn(() =>
Query input
) +); + +const mockSetQueryParams = jest.fn(); +const mockResetAllFilters = jest.fn(); +const mockActiveFiltersCount = 2; +jest.mock('@/components/page-filters/hooks/use-page-filters', () => + jest.fn(() => ({ + resetAllFilters: mockResetAllFilters, + activeFiltersCount: mockActiveFiltersCount, + queryParams: mockDomainWorkflowsQueryParamsValues, + setQueryParams: mockSetQueryParams, + })) +); + +describe(DomainWorkflowsHeader.name, () => { + it('renders segmented control', async () => { + render(); + + expect(await screen.findByText('Search')).toBeInTheDocument(); + expect(await screen.findByText('Query')).toBeInTheDocument(); + }); + + it('renders page search and filters button when input type is search', async () => { + render(); + + expect(await screen.findByText('Filter search')).toBeInTheDocument(); + expect(await screen.findByText('Filter toggle')).toBeInTheDocument(); + }); + + it('renders page filters when filter toggle is clicked', async () => { + const user = userEvent.setup(); + render(); + + const filterToggle = await screen.findByText('Filter toggle'); + await user.click(filterToggle); + + expect(await screen.findByText('Filter fields')).toBeInTheDocument(); + }); + + it('renders query input when input type is query', async () => { + jest.spyOn(usePageFiltersModule, 'default').mockReturnValueOnce({ + resetAllFilters: mockResetAllFilters, + activeFiltersCount: mockActiveFiltersCount, + queryParams: { + ...mockDomainWorkflowsQueryParamsValues, + inputType: 'query', + }, + setQueryParams: mockSetQueryParams, + }); + + render(); + + expect(await screen.findByText('Query')).toBeInTheDocument(); + }); + + it('toggles input type when segmented control is used', async () => { + const user = userEvent.setup(); + render(); + + const queryButton = await screen.findByText('Search'); + await user.click(queryButton); + + expect(mockSetQueryParams).toHaveBeenCalledWith( + { inputType: 'search' }, + { pageRerender: true, replace: false } + ); + }); +}); diff --git a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.styles.ts b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.styles.ts new file mode 100644 index 000000000..f651cce16 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.styles.ts @@ -0,0 +1,60 @@ +import { styled as createStyled, type Theme } from 'baseui'; +import { + type SegmentOverrides, + type SegmentedControlOverrides, +} from 'baseui/segmented-control'; +import { type StyleObject } from 'styletron-react'; + +export const styled = { + HeaderContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + marginTop: $theme.sizing.scale950, + marginBottom: $theme.sizing.scale900, + })), + InputContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: $theme.sizing.scale500, + marginBottom: $theme.sizing.scale500, + [$theme.mediaQuery.medium]: { + flexDirection: 'row', + }, + })), +}; + +export const overrides = { + inputToggle: { + Root: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + flex: '1 0 auto', + height: $theme.sizing.scale950, + padding: $theme.sizing.scale0, + borderRadius: $theme.borders.radius300, + width: '100%', + ...$theme.typography.ParagraphSmall, + [$theme.mediaQuery.medium]: { + width: 'auto', + }, + }), + }, + SegmentList: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + height: $theme.sizing.scale950, + ...$theme.typography.ParagraphSmall, + }), + }, + Active: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + height: $theme.sizing.scale900, + top: 0, + }), + }, + } satisfies SegmentedControlOverrides, + inputToggleSegment: { + Segment: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + height: $theme.sizing.scale900, + whiteSpace: 'nowrap', + }), + }, + } satisfies SegmentOverrides, +}; diff --git a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx new file mode 100644 index 000000000..847fe3b29 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx @@ -0,0 +1,85 @@ +'use client'; +import { useState } from 'react'; + +import { Segment, SegmentedControl } from 'baseui/segmented-control'; + +import usePageFilters from '@/components/page-filters/hooks/use-page-filters'; +import PageFiltersFields from '@/components/page-filters/page-filters-fields/page-filters-fields'; +import PageFiltersSearch from '@/components/page-filters/page-filters-search/page-filters-search'; +import PageFiltersToggle from '@/components/page-filters/page-filters-toggle/page-filters-toggle'; +import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; + +import domainWorkflowsFiltersConfig from '../config/domain-workflows-filters.config'; +import DomainWorkflowsQueryInput from '../domain-workflows-query-input/domain-workflows-query-input'; + +import { overrides, styled } from './domain-workflows-header.styles'; + +export default function DomainWorkflowsHeader() { + const [areFiltersShown, setAreFiltersShown] = useState(false); + + const { resetAllFilters, activeFiltersCount, queryParams, setQueryParams } = + usePageFilters({ + pageFiltersConfig: domainWorkflowsFiltersConfig, + pageQueryParamsConfig: domainPageQueryParamsConfig, + }); + + return ( + + + { + setQueryParams( + { + inputType: activeKey === 'query' ? 'query' : 'search', + }, + { replace: false, pageRerender: true } + ); + }} + overrides={overrides.inputToggle} + > + + + + {queryParams.inputType === 'query' ? ( + setQueryParams({ query: v })} + /> + ) : ( + <> + + { + setAreFiltersShown((value) => !value); + }} + activeFiltersCount={activeFiltersCount} + /> + + )} + + {queryParams.inputType === 'search' && areFiltersShown && ( + + )} + + ); +} diff --git a/src/views/domain-workflows/domain-workflows-query-input/__tests__/domain-workflows-query-input.test.tsx b/src/views/domain-workflows/domain-workflows-query-input/__tests__/domain-workflows-query-input.test.tsx new file mode 100644 index 000000000..99bb0a7a7 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-query-input/__tests__/domain-workflows-query-input.test.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { render, screen, userEvent, waitFor } from '@/test-utils/rtl'; + +import DomainWorkflowsQueryInput from '../domain-workflows-query-input'; + +describe(DomainWorkflowsQueryInput.name, () => { + it('renders as expected', async () => { + setup({}); + + expect(await screen.findByRole('textbox')).toBeInTheDocument(); + expect(await screen.findByText('Run Query')).toBeInTheDocument(); + }); + + it('renders as expected when loaded with a start value', async () => { + setup({ startValue: 'test_query' }); + + const textbox = await screen.findByRole('textbox'); + await waitFor(() => expect(textbox).toHaveValue('test_query')); + expect(await screen.findByText('Rerun Query')).toBeInTheDocument(); + }); + + it('calls setValue and changes text when the Run Query button is clicked', async () => { + const { mockSetValue, user } = setup({}); + + const textbox = await screen.findByRole('textbox'); + await user.type(textbox, 'mock_query'); + await user.click(await screen.findByText('Run Query')); + + expect(mockSetValue).toHaveBeenCalledWith('mock_query'); + }); +}); + +function setup({ startValue }: { startValue?: string }) { + const mockSetValue = jest.fn(); + const user = userEvent.setup(); + render( + + ); + + return { mockSetValue, user }; +} diff --git a/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.styles.ts b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.styles.ts new file mode 100644 index 000000000..40aebdcef --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.styles.ts @@ -0,0 +1,28 @@ +import { type Theme } from 'baseui'; +import { type ButtonOverrides } from 'baseui/button'; +import { type InputOverrides } from 'baseui/input'; +import { type StyleObject } from 'styletron-react'; + +export const overrides = { + input: { + Root: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + height: $theme.sizing.scale950, + }), + }, + Input: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.MonoParagraphXSmall, + }), + }, + } satisfies InputOverrides, + runButton: { + Root: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + whiteSpace: 'nowrap', + height: $theme.sizing.scale950, + ...$theme.typography.LabelSmall, + }), + }, + } satisfies ButtonOverrides, +}; diff --git a/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.tsx b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.tsx new file mode 100644 index 000000000..3066aadd5 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from 'react'; + +import { Button } from 'baseui/button'; +import { Input } from 'baseui/input'; +import { MdPlayArrow, MdCode, MdRefresh } from 'react-icons/md'; + +import { overrides } from './domain-workflows-query-input.styles'; +import { type Props } from './domain-workflows-query-input.types'; + +export default function DomainWorkflowsQueryInput({ value, setValue }: Props) { + const [queryText, setQueryText] = useState(''); + + useEffect(() => { + setQueryText(value); + }, [value]); + + const isQueryUnchanged = value && value === queryText; + + return ( + <> + { + setQueryText(event.target.value); + }} + startEnhancer={() => } + overrides={overrides.input} + placeholder="Filter workflows using a custom query" + clearable + clearOnEscape + /> + + + ); +} diff --git a/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.types.ts b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.types.ts new file mode 100644 index 000000000..99094d144 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-query-input/domain-workflows-query-input.types.ts @@ -0,0 +1,4 @@ +export type Props = { + value: string; + setValue: (v: string | undefined) => void; +}; diff --git a/src/views/domain-workflows/domain-workflows.tsx b/src/views/domain-workflows/domain-workflows.tsx index 01c176bd3..dbf5c5e7d 100644 --- a/src/views/domain-workflows/domain-workflows.tsx +++ b/src/views/domain-workflows/domain-workflows.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { type DomainPageTabContentProps } from '@/views/domain-page/domain-page-content/domain-page-content.types'; -import DomainWorkflowsFilters from './domain-workflows-filters/domain-workflows-filters'; +import DomainWorkflowsHeader from './domain-workflows-header/domain-workflows-header'; import DomainWorkflowsTable from './domain-workflows-table/domain-workflows-table'; export default function DomainWorkflows(props: DomainPageTabContentProps) { return ( <> - + );