diff --git a/src/components/page-filters/page-filters.types.ts b/src/components/page-filters/page-filters.types.ts index 322cb168..84d99002 100644 --- a/src/components/page-filters/page-filters.types.ts +++ b/src/components/page-filters/page-filters.types.ts @@ -18,7 +18,12 @@ export type PageFilterConfig< > = { id: string; getValue: (queryParamsValues: PageQueryParamValues

) => V; - formatValue: (value: V) => Partial>; + formatValue: ( + value: V + ) => Pick< + PageQueryParamSetterValues

, + keyof V extends string ? keyof V : never + >; component: React.ComponentType>; }; diff --git a/src/views/domain-page/config/domain-page-tabs.config.ts b/src/views/domain-page/config/domain-page-tabs.config.ts index 0a9d91cb..f97f62fe 100644 --- a/src/views/domain-page/config/domain-page-tabs.config.ts +++ b/src/views/domain-page/config/domain-page-tabs.config.ts @@ -1,4 +1,4 @@ -import { MdListAlt, MdSettings, MdSort } from 'react-icons/md'; +import { MdArchive, MdListAlt, MdSettings, MdSort } from 'react-icons/md'; import type { DomainPageTabs } from '../domain-page-tabs/domain-page-tabs.types'; @@ -21,7 +21,7 @@ const domainPageTabsConfig = [ { key: 'archival', title: 'Archival', - artwork: MdSort, + artwork: MdArchive, }, ] as const satisfies DomainPageTabs; diff --git a/src/views/domain-workflows/__fixtures__/domain-workflows-query-params.ts b/src/views/domain-workflows/__fixtures__/domain-workflows-query-params.ts index 38ecf037..bf4ce307 100644 --- a/src/views/domain-workflows/__fixtures__/domain-workflows-query-params.ts +++ b/src/views/domain-workflows/__fixtures__/domain-workflows-query-params.ts @@ -15,9 +15,22 @@ export const mockDomainWorkflowsQueryParamsValues: PageQueryParamValues< workflowId: '', workflowType: '', statusBasic: undefined, + inputTypeArchival: 'search', + searchArchival: '', + statusArchival: undefined, + timeRangeStartArchival: undefined, + timeRangeEndArchival: undefined, + sortColumnArchival: 'startTime', + sortOrderArchival: 'DESC', + queryArchival: '', }; export const mockDateOverrides = { timeRangeStart: new Date(1684800000000), // 23 May 2023 00:00 timeRangeEnd: new Date(1684886400000), // 24 May 2023 00:00 }; + +export const mockDateOverridesArchival = { + timeRangeStartArchival: new Date(1684800000000), // 23 May 2023 00:00 + timeRangeEndArchival: new Date(1684886400000), // 24 May 2023 00:00 +}; diff --git a/src/views/domain-workflows/config/domain-workflows-archival-filters.config.ts b/src/views/domain-workflows/config/domain-workflows-archival-filters.config.ts index 25b5f3a5..49c53e27 100644 --- a/src/views/domain-workflows/config/domain-workflows-archival-filters.config.ts +++ b/src/views/domain-workflows/config/domain-workflows-archival-filters.config.ts @@ -1,38 +1,38 @@ import { type PageFilterConfig } from '@/components/page-filters/page-filters.types'; import type domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; -import DomainWorkflowsFiltersDates from '../domain-workflows-filters-dates/domain-workflows-filters-dates'; -import { type DomainWorkflowsFiltersDatesValue } from '../domain-workflows-filters-dates/domain-workflows-filters-dates.types'; -import DomainWorkflowsFiltersStatus from '../domain-workflows-filters-status/domain-workflows-filters-status'; -import { type DomainWorkflowsFiltersStatusValue } from '../domain-workflows-filters-status/domain-workflows-filters-status.types'; +import DomainWorkflowsArchivalFiltersDates from '../domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates'; +import { type DomainWorkflowsArchivalFiltersDatesValue } from '../domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.types'; +import DomainWorkflowsArchivalFiltersStatus from '../domain-workflows-archival-filters-status/domain-workflows-archival-filters-status'; +import { type DomainWorkflowsArchivalFiltersStatusValue } from '../domain-workflows-archival-filters-status/domain-workflows-archival-filters-status.types'; const domainWorkflowsArchivalFiltersConfig: [ PageFilterConfig< typeof domainPageQueryParamsConfig, - DomainWorkflowsFiltersStatusValue + DomainWorkflowsArchivalFiltersStatusValue >, PageFilterConfig< typeof domainPageQueryParamsConfig, - DomainWorkflowsFiltersDatesValue + DomainWorkflowsArchivalFiltersDatesValue >, ] = [ { id: 'status', - getValue: (v) => ({ status: v.statusArchival }), - formatValue: (v) => ({ statusArchival: v.status }), - component: DomainWorkflowsFiltersStatus, + getValue: (v) => ({ statusArchival: v.statusArchival }), + formatValue: (v) => ({ statusArchival: v.statusArchival }), + component: DomainWorkflowsArchivalFiltersStatus, }, { id: 'dates', getValue: (v) => ({ - timeRangeStart: v.timeRangeStartArchival, - timeRangeEnd: v.timeRangeEndArchival, + timeRangeStartArchival: v.timeRangeStartArchival, + timeRangeEndArchival: v.timeRangeEndArchival, }), formatValue: (v) => ({ - timeRangeStartArchival: v.timeRangeStart?.toISOString(), - timeRangeEndArchival: v.timeRangeEnd?.toISOString(), + timeRangeStartArchival: v.timeRangeStartArchival?.toISOString(), + timeRangeEndArchival: v.timeRangeEndArchival?.toISOString(), }), - component: DomainWorkflowsFiltersDates, + component: DomainWorkflowsArchivalFiltersDates, }, ] as const; diff --git a/src/views/domain-workflows/domain-workflows-archival-filters-dates/__tests__/domain-workflows-archival-filters-dates.test.tsx b/src/views/domain-workflows/domain-workflows-archival-filters-dates/__tests__/domain-workflows-archival-filters-dates.test.tsx new file mode 100644 index 00000000..5fc9a3d4 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-archival-filters-dates/__tests__/domain-workflows-archival-filters-dates.test.tsx @@ -0,0 +1,154 @@ +import React from 'react'; + +import { render, screen, act, fireEvent } from '@/test-utils/rtl'; + +import { + mockDomainWorkflowsQueryParamsValues, + mockDateOverridesArchival, +} from '../../__fixtures__/domain-workflows-query-params'; +import DomainWorkflowsArchivalFiltersDates from '../domain-workflows-archival-filters-dates'; +import { type DomainWorkflowsArchivalFiltersDatesValue } from '../domain-workflows-archival-filters-dates.types'; + +jest.useFakeTimers().setSystemTime(new Date('2023-05-25')); + +jest.mock( + '../../domain-workflows-filters-dates/domain-workflows-filters-dates.constants', + () => ({ + ...jest.requireActual( + '../../domain-workflows-filters-dates/domain-workflows-filters-dates.constants' + ), + DATE_FORMAT: 'dd MMM yyyy, HH:mm x', + }) +); + +describe('DomainWorkflowsArchivalFiltersDates', () => { + it('displays the date picker component', () => { + setup({}); + expect( + screen.getByPlaceholderText('Select time range') + ).toBeInTheDocument(); + }); + + it('renders without errors when dates are already provided in query params', () => { + setup({ + overrides: mockDateOverridesArchival, + }); + expect( + // TODO - set timezone config for unit tests to UTC + screen.getByDisplayValue( + '23 May 2023, 00:00 +00 – 24 May 2023, 00:00 +00' + ) + ).toBeInTheDocument(); + }); + + it('sets query params when date is set', () => { + const { mockSetValue } = setup({}); + const datePicker = screen.getByPlaceholderText('Select time range'); + act(() => { + fireEvent.change(datePicker, { + target: { value: '13 May 2023, 00:00 +00 – 14 May 2023, 00:00 +00' }, + }); + }); + + expect(mockSetValue).toHaveBeenCalledWith({ + timeRangeStartArchival: new Date('2023-05-13T00:00:00.000Z'), + timeRangeEndArchival: new Date('2023-05-14T00:00:00.000Z'), + }); + }); + + it('resets to previous date when one date is selected and then the modal is closed', () => { + const { mockSetValue } = setup({ + overrides: mockDateOverridesArchival, + }); + const datePicker = screen.getByPlaceholderText('Select time range'); + + act(() => { + fireEvent.focus(datePicker); + }); + + const timeRangeStartLabel = screen.getByLabelText( + "Choose Saturday, May 13th 2023. It's available." + ); + + act(() => { + fireEvent.click(timeRangeStartLabel); + }); + + screen.getByText( + 'Selected date is 13 May 2023, 00:00 +00. Select the second date.' + ); + + act(() => { + fireEvent.keyDown(datePicker, { keyCode: 9 }); + }); + + expect(datePicker).toHaveValue( + '23 May 2023, 00:00 +00 – 24 May 2023, 00:00 +00' + ); + expect(mockSetValue).not.toHaveBeenCalled(); + }); + + it('resets to empty state when one date is selected and then the modal is closed', () => { + const { mockSetValue } = setup({}); + const datePicker = screen.getByPlaceholderText('Select time range'); + + act(() => { + fireEvent.focus(datePicker); + }); + + const timeRangeStartLabel = screen.getByLabelText( + "Choose Saturday, May 13th 2023. It's available." + ); + + act(() => { + fireEvent.click(timeRangeStartLabel); + }); + + screen.getByText( + 'Selected date is 13 May 2023, 00:00 +00. Select the second date.' + ); + + act(() => { + fireEvent.keyDown(datePicker, { keyCode: 9 }); + }); + + expect(datePicker).toHaveValue(''); + expect(mockSetValue).not.toHaveBeenCalled(); + }); + + it('clears the date when the clear button is clicked', () => { + const { mockSetValue } = setup({ + overrides: mockDateOverridesArchival, + }); + const clearButton = screen.getByLabelText('Clear value'); + act(() => { + fireEvent.click(clearButton); + }); + + expect(mockSetValue).toHaveBeenCalledWith({ + timeRangeStartArchival: undefined, + timeRangeEndArchival: undefined, + }); + }); +}); + +function setup({ + overrides, +}: { + overrides?: DomainWorkflowsArchivalFiltersDatesValue; +}) { + const mockSetValue = jest.fn(); + render( + + ); + + return { mockSetValue }; +} diff --git a/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.styles.ts b/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.styles.ts new file mode 100644 index 00000000..47848d09 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.styles.ts @@ -0,0 +1,18 @@ +import { type Theme } from 'baseui'; +import type { FormControlOverrides } from 'baseui/form-control/types'; +import { type StyleObject } from 'styletron-react'; + +export const overrides = { + dateFormControl: { + Label: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelXSmall, + }), + }, + ControlContainer: { + style: (): StyleObject => ({ + margin: '0px', + }), + }, + } satisfies FormControlOverrides, +}; diff --git a/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.tsx b/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.tsx new file mode 100644 index 00000000..f86b292a --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.tsx @@ -0,0 +1,76 @@ +'use client'; +import * as React from 'react'; + +import { DatePicker } from 'baseui/datepicker'; +import { FormControl } from 'baseui/form-control'; +import { SIZE } from 'baseui/input'; + +import { type PageFilterComponentProps } from '@/components/page-filters/page-filters.types'; + +import { DATE_FORMAT } from '../domain-workflows-filters-dates/domain-workflows-filters-dates.constants'; + +import { overrides } from './domain-workflows-archival-filters-dates.styles'; +import { type DomainWorkflowsArchivalFiltersDatesValue } from './domain-workflows-archival-filters-dates.types'; + +export default function DomainWorkflowsArchivalFiltersDates({ + value, + setValue, +}: PageFilterComponentProps) { + const [dates, setDates] = React.useState>([]); + + React.useEffect(() => { + setDates( + Boolean(value.timeRangeStartArchival) && + Boolean(value.timeRangeEndArchival) + ? [value.timeRangeStartArchival, value.timeRangeEndArchival] + : [] + ); + }, [value]); + + return ( + + { + if (!date || !Array.isArray(date)) { + return; + } + setDates(date); + if (date.length === 0) { + setValue({ + timeRangeStartArchival: undefined, + timeRangeEndArchival: undefined, + }); + } else if (date.length === 2) { + const [start, end] = date; + if (!start || !end) { + return; + } + setValue({ + timeRangeStartArchival: start, + timeRangeEndArchival: end, + }); + } + }} + onClose={() => { + if (dates.length !== 2 || dates.some((date) => !date)) { + setDates( + Boolean(value.timeRangeStartArchival) && + Boolean(value.timeRangeEndArchival) + ? [value.timeRangeStartArchival, value.timeRangeEndArchival] + : [] + ); + } + }} + placeholder="Select time range" + formatString={DATE_FORMAT} + size={SIZE.compact} + quickSelect + range + clearable + timeSelectStart + timeSelectEnd + /> + + ); +} diff --git a/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.types.ts b/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.types.ts new file mode 100644 index 00000000..7a5588f8 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-archival-filters-dates/domain-workflows-archival-filters-dates.types.ts @@ -0,0 +1,4 @@ +export type DomainWorkflowsArchivalFiltersDatesValue = { + timeRangeStartArchival: Date | undefined; + timeRangeEndArchival: Date | undefined; +}; diff --git a/src/views/domain-workflows/domain-workflows-archival-filters-status/__tests__/domain-workflows-archival-filters-status.test.tsx b/src/views/domain-workflows/domain-workflows-archival-filters-status/__tests__/domain-workflows-archival-filters-status.test.tsx new file mode 100644 index 00000000..3b310a76 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-archival-filters-status/__tests__/domain-workflows-archival-filters-status.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; + +import { render, screen, fireEvent, act } from '@/test-utils/rtl'; + +import { WORKFLOW_STATUS_NAMES } from '@/views/shared/workflow-status-tag/workflow-status-tag.constants'; + +import { mockDomainWorkflowsQueryParamsValues } from '../../__fixtures__/domain-workflows-query-params'; +import DomainWorkflowsArchivalFiltersStatus from '../domain-workflows-archival-filters-status'; +import { type DomainWorkflowsArchivalFiltersStatusValue } from '../domain-workflows-archival-filters-status.types'; + +describe('DomainWorkflowsArchivalFiltersStatus', () => { + it('renders without errors', () => { + setup({}); + expect(screen.getByRole('combobox')).toBeInTheDocument(); + }); + + it('displays all the options in the select component', () => { + setup({}); + const selectFilter = screen.getByRole('combobox'); + act(() => { + fireEvent.click(selectFilter); + }); + Object.entries(WORKFLOW_STATUS_NAMES).forEach(([_, value]) => + expect(screen.getByText(value)).toBeInTheDocument() + ); + }); + + it('calls the setQueryParams function when an option is selected', () => { + const { mockSetValue } = setup({}); + const selectFilter = screen.getByRole('combobox'); + act(() => { + fireEvent.click(selectFilter); + }); + const runningOption = screen.getByText('Running'); + act(() => { + fireEvent.click(runningOption); + }); + expect(mockSetValue).toHaveBeenCalledWith({ + statusArchival: 'WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID', + }); + }); + + it('calls the setQueryParams function when the filter is cleared', () => { + const { mockSetValue } = setup({ + overrides: { + statusArchival: 'WORKFLOW_EXECUTION_CLOSE_STATUS_FAILED', + }, + }); + const clearButton = screen.getByLabelText('Clear value'); + act(() => { + fireEvent.click(clearButton); + }); + expect(mockSetValue).toHaveBeenCalledWith({ statusArchival: undefined }); + }); +}); + +function setup({ + overrides, +}: { + overrides?: DomainWorkflowsArchivalFiltersStatusValue; +}) { + const mockSetValue = jest.fn(); + render( + + ); + + return { mockSetValue }; +} diff --git a/src/views/domain-workflows/domain-workflows-archival-filters-status/domain-workflows-archival-filters-status.styles.ts b/src/views/domain-workflows/domain-workflows-archival-filters-status/domain-workflows-archival-filters-status.styles.ts new file mode 100644 index 00000000..d9a2ff7d --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-archival-filters-status/domain-workflows-archival-filters-status.styles.ts @@ -0,0 +1,18 @@ +import { type Theme } from 'baseui'; +import type { FormControlOverrides } from 'baseui/form-control/types'; +import { type StyleObject } from 'styletron-react'; + +export const overrides = { + selectFormControl: { + Label: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelXSmall, + }), + }, + ControlContainer: { + style: (): StyleObject => ({ + margin: '0px', + }), + }, + } satisfies FormControlOverrides, +}; diff --git a/src/views/domain-workflows/domain-workflows-archival-filters-status/domain-workflows-archival-filters-status.tsx b/src/views/domain-workflows/domain-workflows-archival-filters-status/domain-workflows-archival-filters-status.tsx new file mode 100644 index 00000000..71c2d7c7 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-archival-filters-status/domain-workflows-archival-filters-status.tsx @@ -0,0 +1,42 @@ +'use client'; +import React from 'react'; + +import { FormControl } from 'baseui/form-control'; +import { Select, SIZE } from 'baseui/select'; + +import { type PageFilterComponentProps } from '@/components/page-filters/page-filters.types'; +import { type WorkflowStatus } from '@/views/shared/workflow-status-tag/workflow-status-tag.types'; + +import { WORKFLOW_STATUS_OPTIONS } from '../domain-workflows-filters-status/domain-workflows-filters-status.constants'; + +import { overrides } from './domain-workflows-archival-filters-status.styles'; +import { type DomainWorkflowsArchivalFiltersStatusValue } from './domain-workflows-archival-filters-status.types'; + +export default function DomainWorkflowsArchivalFiltersStatus({ + value, + setValue, +}: PageFilterComponentProps) { + const statusOptionValue = WORKFLOW_STATUS_OPTIONS.filter( + (option) => option.id === value.statusArchival + ); + + return ( + +