diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 9fcda17fe..f956d7dff 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -36,6 +36,8 @@ "Enter value": "Enter value", "Save info for developers": "Save info for developers", "Show meta": "Show meta", + "Start date": "Start date", + "End date": "End date", "Screen is missing or unavailable for your role": "Screen {{screenName}} is missing or unavailable for your role", "View is missing or unavailable for your role": "View {{viewName}} is missing or unavailable for your role" } diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index 14ca1bf2a..ad809791a 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -38,6 +38,8 @@ "Refresh meta": "Обновить мета данные", "Save info for developers": "Сохранить информацию для разработчиков", "Show meta": "Показать мета данные", + "Start date": "Начальная дата", + "End date": "Конечная дата", "Screen is missing or unavailable for your role": "Экран \"{{screenName}}\" отсутствует или недоступен для вашей роли", "View is missing or unavailable for your role": "Панель \"{{viewName}}\" отсутствует или недоступна для вашей роли" } diff --git a/src/components/ColumnTitle/ColumnFilter.tsx b/src/components/ColumnTitle/ColumnFilter.tsx index e46bd8e30..b878b0fd2 100644 --- a/src/components/ColumnTitle/ColumnFilter.tsx +++ b/src/components/ColumnTitle/ColumnFilter.tsx @@ -58,6 +58,7 @@ export function ColumnFilter({ widgetName, widgetMeta, rowMeta, components }: Co const bcName = widget?.bcName const effectiveFieldMeta = (widget?.fields?.find((item: WidgetListField) => item.key === widgetMeta.filterBy) ?? widgetMeta) as WidgetListField + const widgetOptions = widget?.options const filter = useSelector((store: Store) => store.screen.filters[bcName]?.find(item => item.fieldName === effectiveFieldMeta.key)) const [value, setValue] = React.useState(filter?.value) const [visible, setVisible] = React.useState(false) @@ -109,8 +110,21 @@ export function ColumnFilter({ widgetName, widgetMeta, rowMeta, components }: Co }, []) const content = components?.popup ?? ( - - + + ) diff --git a/src/components/FilterPopup/FilterPopup.tsx b/src/components/FilterPopup/FilterPopup.tsx index 9b361146e..a9ce2c6d6 100644 --- a/src/components/FilterPopup/FilterPopup.tsx +++ b/src/components/FilterPopup/FilterPopup.tsx @@ -20,9 +20,9 @@ */ import React, { FormEvent } from 'react' -import { Form, Button } from 'antd' +import { Button, Form } from 'antd' import styles from './FilterPopup.less' -import { BcFilter } from '../../interfaces/filters' +import { BcFilter, FilterType } from '../../interfaces/filters' import { getFilterType } from '../../utils/filters' import { useDispatch, useSelector } from 'react-redux' import { $do } from '../../actions/actions' @@ -30,12 +30,14 @@ import { Store } from '../../interfaces/store' import { WidgetField } from '../../interfaces/widget' import { DataValue } from '../../interfaces/data' import { useTranslation } from 'react-i18next' +import { FieldType } from '@tesler-ui/schema' export interface FilterPopupProps { widgetName: string fieldKey: string value: DataValue | DataValue[] children: React.ReactNode + fieldType?: FieldType onApply?: () => void onCancel?: () => void } @@ -64,7 +66,11 @@ export const FilterPopup: React.FC = props => { const handleApply = (e: FormEvent) => { e.preventDefault() const newFilter: BcFilter = { - type: getFilterType(widgetMeta.type), + type: + widget.options?.filterDateByRange && + [FieldType.date, FieldType.dateTime, FieldType.dateTimeWithSeconds].includes(props.fieldType) // todo the list must be extendable by customer + ? FilterType.range + : getFilterType(widgetMeta.type), value: props.value, fieldName: props.fieldKey, viewName, diff --git a/src/components/ui/FilterField/FilterField.tsx b/src/components/ui/FilterField/FilterField.tsx index 5d851ab55..e80e78c95 100644 --- a/src/components/ui/FilterField/FilterField.tsx +++ b/src/components/ui/FilterField/FilterField.tsx @@ -26,15 +26,17 @@ import { FieldType } from '../../../interfaces/view' import { Checkbox, Input, Icon, DatePicker } from 'antd' import { CheckboxChangeEvent } from 'antd/lib/checkbox' import moment, { Moment } from 'moment' -import { WidgetListField } from '../../../interfaces/widget' +import { WidgetListField, WidgetMeta } from '../../../interfaces/widget' import { RowMetaField } from '../../../interfaces/rowMeta' import { getFormat } from '../DatePickerField/DatePickerField' +import RangePicker from './components/RangePicker' export interface ColumnFilterControlProps { widgetFieldMeta: WidgetListField rowFieldMeta: RowMetaField value: DataValue | DataValue[] onChange: (value: DataValue | DataValue[]) => void + widgetOptions?: WidgetMeta['options'] } /** @@ -66,6 +68,9 @@ export const ColumnFilterControl: React.FC = props => } case FieldType.dateTime: case FieldType.date: { + if (props.widgetOptions?.filterDateByRange) { + return props.onChange(v)} format={getFormat()} /> + } return ( { + onChange: (v: DataValue[]) => void + value: DataValue[] +} + +function RangePicker({ value, onChange, ...rest }: RangePickerProps) { + const startDate = Array.isArray(value) && value?.[0] ? moment(value[0] as string, moment.ISO_8601) : null + const endDate = Array.isArray(value) && value?.[1] ? moment(value[1] as string, moment.ISO_8601) : null + const [endOpen, setEndOpen] = useState(false) + + const disabledStartDate = (startValue: Moment) => { + if (!startValue || !endDate) { + return false + } + return startValue.valueOf() > endDate.valueOf() + } + + const disabledEndDate = (endValue: Moment) => { + if (!endValue || !startDate) { + return false + } + return endValue.valueOf() <= startDate.valueOf() + } + + const handleStartOpenChange = (open: boolean) => { + if (!open) { + setEndOpen(true) + } + } + + const handleEndOpenChange = (open: boolean) => { + setEndOpen(open) + } + + const { t } = useTranslation() + return ( +
+ { + onChange([date?.toISOString(), endDate?.toISOString()]) + }} + value={startDate} + onOpenChange={handleStartOpenChange} + /> + { + onChange([startDate?.toISOString(), date?.toISOString()]) + }} + value={endDate} + open={endOpen} + onOpenChange={handleEndOpenChange} + /> +
+ ) +} + +export default React.memo(RangePicker) diff --git a/src/interfaces/filters.ts b/src/interfaces/filters.ts index 2dbc27fb7..11e38b112 100644 --- a/src/interfaces/filters.ts +++ b/src/interfaces/filters.ts @@ -1,6 +1,10 @@ import { DataValue } from './data' export enum FilterType { + /** + * Transforms into combination of 'greaterOrEqualThan' and 'lessOrEqualThan' (See src/utils/filters.ts) + */ + range = 'range', equals = 'equals', greaterThan = 'greaterThan', lessThan = 'lessThan', diff --git a/src/interfaces/widget.ts b/src/interfaces/widget.ts index 0628a7ef0..62e5ecf60 100644 --- a/src/interfaces/widget.ts +++ b/src/interfaces/widget.ts @@ -85,7 +85,7 @@ export interface WidgetMeta { limit?: number gridWidth: number // 1-24 fields: unknown[] - options?: WidgetOptions + options?: WidgetOptions & { filterDateByRange?: boolean } // TODO extract `filterDateByRange` to tesler-schema showCondition?: WidgetShowCondition description?: string // description for documentation } diff --git a/src/utils/__tests__/filters.test.ts b/src/utils/__tests__/filters.test.ts index 5ad2e7363..45aeb8d2a 100644 --- a/src/utils/__tests__/filters.test.ts +++ b/src/utils/__tests__/filters.test.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { getFilters, getSorters, parseSorters, getFilterType, parseFilters } from '../filters' +import { getFilters, getFilterType, getSorters, parseFilters, parseSorters } from '../filters' import { FieldType } from '../../interfaces/view' import { FilterType } from '../../interfaces/filters' @@ -70,6 +70,18 @@ describe('getFilters', () => { expect(getFilters(null)).toBe(null) expect(getFilters([])).toBe(null) }) + it("should convert \"range\" into combination of 'greaterOrEqualThan' and 'lessOrEqualThan'", () => { + expect(getFilters([{ type: FilterType.range, fieldName: 'test-field', value: [1, 2] }])).toMatchObject({ + 'test-field.greaterOrEqualThan': '1', + 'test-field.lessOrEqualThan': '2' + }) + expect(getFilters([{ type: FilterType.range, fieldName: 'test-field', value: [1, null] }])).toMatchObject({ + 'test-field.greaterOrEqualThan': '1' + }) + expect(getFilters([{ type: FilterType.range, fieldName: 'test-field', value: [null, 2] }])).toMatchObject({ + 'test-field.lessOrEqualThan': '2' + }) + }) }) describe('getSorters', () => { diff --git a/src/utils/filters.ts b/src/utils/filters.ts index 8a2c2bb3b..dd394a876 100644 --- a/src/utils/filters.ts +++ b/src/utils/filters.ts @@ -25,12 +25,22 @@ export function getFilters(filters: BcFilter[]) { } const result: Record = {} filters.forEach(item => { - let value = String(item.value) - if (Array.isArray(item.value)) { - const values = (item.value as DataValue[]).map(val => `"${val}"`) - value = `[${values}]` + if (item.type === FilterType.range) { + const values = item.value as DataValue[] + if (values[0]) { + result[`${item.fieldName}.${FilterType.greaterOrEqualThan}`] = String(values[0]) + } + if (values[1]) { + result[`${item.fieldName}.${FilterType.lessOrEqualThan}`] = String(values[1]) + } + } else { + let value = String(item.value) + if (Array.isArray(item.value)) { + const values = (item.value as DataValue[]).map(val => `"${val}"`) + value = `[${values}]` + } + result[`${item.fieldName}.${item.type}`] = value } - result[`${item.fieldName}.${item.type}`] = value }) return result }