diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c73dc3..3df87c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.4] - 2026-03-22 + +### Changed + +- `FilterList` to allow limiting the used operators + +### Fixed +- `FlexibleDateTimeInput` not having a correct default time and throwing warnings on mode change + + ## [0.9.3] - 2026-03-22 ### Added diff --git a/package.json b/package.json index 4c92fa0..765a4e0 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "access": "public" }, "license": "MPL-2.0", - "version": "0.9.3", + "version": "0.9.4", "files": [ "dist" ], diff --git a/src/components/user-interaction/data/FilterList.tsx b/src/components/user-interaction/data/FilterList.tsx index baf3a25..ea892fa 100644 --- a/src/components/user-interaction/data/FilterList.tsx +++ b/src/components/user-interaction/data/FilterList.tsx @@ -13,6 +13,7 @@ import { FilterPopUp } from './FilterPopUp' import { Combobox } from '@/src/components/user-interaction/Combobox/Combobox' import { ComboboxOption } from '@/src/components/user-interaction/Combobox/ComboboxOption' import { PopUpContext } from '../../layout/popup/PopUpContext' +import type { FilterOperator } from './FilterOperator' import { FilterOperatorUtils } from './FilterOperator' import type { ColumnFilter } from '@tanstack/react-table' @@ -24,11 +25,12 @@ export interface FilterListPopUpBuilderProps { value: FilterValue, onValueChange: (value: FilterValue) => void, onRemove: () => void, + operatorOverrides?: FilterOperator[], dataType: DataType, tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, name: string, isOpen: boolean, - close: () => void, + onClose: () => void, } export interface FilterListItem { @@ -36,6 +38,7 @@ export interface FilterListItem { label: string, dataType: DataType, tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, + operatorOverrides?: FilterOperator[], popUpBuilder?: (props: FilterListPopUpBuilderProps) => ReactNode, activeLabelBuilder?: (value: FilterValue) => ReactNode, } @@ -156,11 +159,12 @@ export const FilterList = ({ value, onValueChange, availableItems }: FilterListP onValueChange(value.filter(prevItem => prevItem.id !== columnFilter.id)) setEditState(undefined) }, + operatorOverrides: item.operatorOverrides, dataType: item.dataType, tags: item.tags, name: item.label, isOpen, - close: () => setIsOpen(false), + onClose: () => setIsOpen(false), }) ) : ( { setEditState({ ...columnFilter, value }) }} diff --git a/src/components/user-interaction/data/FilterPopUp.tsx b/src/components/user-interaction/data/FilterPopUp.tsx index b94a0a8..aaf03d0 100644 --- a/src/components/user-interaction/data/FilterPopUp.tsx +++ b/src/components/user-interaction/data/FilterPopUp.tsx @@ -1,17 +1,16 @@ import { useHightideTranslation } from '@/src/i18n/useHightideTranslation' import { Visibility } from '@/src/components/layout/Visibility' import { IconButton } from '@/src/components/user-interaction/IconButton' -import { TrashIcon, XIcon } from 'lucide-react' +import { Check, TrashIcon } from 'lucide-react' import { PopUp, type PopUpProps } from '@/src/components/layout/popup/PopUp' import type { FilterValue } from './filter-function' import type { FilterOperator } from './FilterOperator' import { FilterOperatorUtils } from './FilterOperator' import type { ReactNode } from 'react' -import { forwardRef, useId, useMemo, useState } from 'react' +import { forwardRef, useEffect, useId, useMemo, useState } from 'react' import { Select } from '../Select/Select' import { SelectOption } from '../Select/SelectOption' import { Input } from '../input/Input' -import { Checkbox } from '../Checkbox' import { DateTimeInput } from '../input/DateTimeInput' import { MultiSelect } from '../MultiSelect/MultiSelect' import { MultiSelectOption } from '../MultiSelect/MultiSelectOption' @@ -35,6 +34,7 @@ export interface FilterPopUpBaseProps extends PopUpProps { onOperatorChange: (operator: FilterOperator) => void, onRemove: () => void, allowedOperators: FilterOperator[], + operatorOverrides?: FilterOperator[], noParameterRequired?: boolean, } @@ -45,10 +45,21 @@ export const FilterBasePopUp = forwardRef( onOperatorChange, onRemove, allowedOperators, + operatorOverrides, noParameterRequired = false, ...props }: FilterPopUpBaseProps, ref) { const translation = useHightideTranslation() + const operators = useMemo(() => { + if(!operatorOverrides || operatorOverrides.length === 0) return allowedOperators + return allowedOperators.filter(op => operatorOverrides.includes(op)) + }, [allowedOperators, operatorOverrides]) + + useEffect(() => { + if(operators.length === 0) { + onRemove() + } + }, [operators, onRemove]) return ( ( }} iconAppearance="right" > - {allowedOperators.map((op) => ( + {operators.map((op) => ( @@ -87,13 +98,13 @@ export const FilterBasePopUp = forwardRef( - + @@ -142,32 +153,18 @@ export const TextFilterPopUp = forwardRef(func { onValueChange({ dataType: 'text', operator, - parameter: { ...parameter, searchText }, + parameter: { ...parameter, stringValue: searchText }, }) }} className="min-w-64" /> -
- { - onValueChange({ - dataType: 'text', - operator, - parameter: { ...parameter, isCaseSensitive }, - }) - }} - /> - -
) @@ -212,7 +209,7 @@ export const NumberFilterPopUp = forwardRef(fu { @@ -220,7 +217,7 @@ export const NumberFilterPopUp = forwardRef(fu onValueChange({ dataType: 'number', operator, - parameter: { ...parameter, minNumber: isNaN(num) ? undefined : num }, + parameter: { ...parameter, numberMin: isNaN(num) ? undefined : num }, }) }} className="min-w-64" @@ -230,7 +227,7 @@ export const NumberFilterPopUp = forwardRef(fu { @@ -238,7 +235,7 @@ export const NumberFilterPopUp = forwardRef(fu onValueChange({ dataType: 'number', operator, - parameter: { ...parameter, maxNumber: isNaN(num) ? undefined : num }, + parameter: { ...parameter, numberMax: isNaN(num) ? undefined : num }, }) }} className="min-w-64" @@ -247,7 +244,7 @@ export const NumberFilterPopUp = forwardRef(fu { @@ -255,7 +252,7 @@ export const NumberFilterPopUp = forwardRef(fu onValueChange({ dataType: 'number', operator, - parameter: { ...parameter, compareValue: isNaN(num) ? undefined : num }, + parameter: { ...parameter, numberValue: isNaN(num) ? undefined : num }, }) }} className="min-w-64" @@ -304,29 +301,29 @@ export const DateFilterPopUp = forwardRef(func { - if (dateValue && parameter.maxDate && dateValue > parameter.maxDate) { - if (!parameter.minDate) { + if (dateValue && parameter.dateMax && dateValue > parameter.dateMax) { + if (!parameter.dateMin) { onValueChange({ dataType: 'date', operator, - parameter: { ...parameter, minDate: parameter.maxDate, maxDate: dateValue }, + parameter: { ...parameter, dateMin: parameter.dateMax, dateMax: dateValue }, }) } else { - const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + const diff = parameter.dateMax.getTime() - parameter.dateMin.getTime() onValueChange({ dataType: 'date', operator, - parameter: { ...parameter, minDate: dateValue, maxDate: new Date(dateValue.getTime() + diff) }, + parameter: { ...parameter, dateMin: dateValue, dateMax: new Date(dateValue.getTime() + diff) }, }) } } else { onValueChange({ dataType: 'date', operator, - parameter: { ...parameter, minDate: dateValue }, + parameter: { ...parameter, dateMin: dateValue }, }) } setTemporaryMinDateValue(null) @@ -340,29 +337,29 @@ export const DateFilterPopUp = forwardRef(func { - if (dateValue && parameter.minDate && dateValue < parameter.minDate) { - if (!parameter.maxDate) { + if (dateValue && parameter.dateMin && dateValue < parameter.dateMin) { + if (!parameter.dateMax) { onValueChange({ dataType: 'date', operator, - parameter: { ...parameter, minDate: dateValue, maxDate: parameter.minDate }, + parameter: { ...parameter, dateMin: dateValue, dateMax: parameter.dateMin }, }) } else { - const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + const diff = parameter.dateMax.getTime() - parameter.dateMin.getTime() onValueChange({ dataType: 'date', operator, - parameter: { ...parameter, minDate: new Date(dateValue.getTime() - diff), maxDate: dateValue }, + parameter: { ...parameter, dateMin: new Date(dateValue.getTime() - diff), dateMax: dateValue }, }) } } else { onValueChange({ dataType: 'date', operator, - parameter: { ...parameter, maxDate: dateValue }, + parameter: { ...parameter, dateMax: dateValue }, }) } setTemporaryMaxDateValue(null) @@ -377,11 +374,11 @@ export const DateFilterPopUp = forwardRef(func { onValueChange({ ...value, - parameter: { ...parameter, compareDate }, + parameter: { ...parameter, dateValue: compareDate }, }) }} allowRemove={true} @@ -438,29 +435,29 @@ export const DatetimeFilterPopUp = forwardRef( { - if (dateValue && parameter.maxDate && dateValue > parameter.maxDate) { - if (!parameter.minDate) { + if (dateValue && parameter.dateMax && dateValue > parameter.dateMax) { + if (!parameter.dateMin) { onValueChange({ dataType: 'dateTime', operator, - parameter: { ...parameter, minDate: parameter.maxDate, maxDate: dateValue }, + parameter: { ...parameter, dateMin: parameter.dateMax, dateMax: dateValue }, }) } else { - const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + const diff = parameter.dateMax.getTime() - parameter.dateMin.getTime() onValueChange({ dataType: 'dateTime', operator, - parameter: { ...parameter, minDate: dateValue, maxDate: new Date(dateValue.getTime() + diff) }, + parameter: { ...parameter, dateMin: dateValue, dateMax: new Date(dateValue.getTime() + diff) }, }) } } else { onValueChange({ dataType: 'dateTime', operator, - parameter: { ...parameter, minDate: dateValue }, + parameter: { ...parameter, dateMin: dateValue }, }) } setTemporaryMinDateValue(null) @@ -473,29 +470,29 @@ export const DatetimeFilterPopUp = forwardRef( { - if (dateValue && parameter.minDate && dateValue < parameter.minDate) { - if (!parameter.maxDate) { + if (dateValue && parameter.dateMin && dateValue < parameter.dateMin) { + if (!parameter.dateMax) { onValueChange({ dataType: 'dateTime', operator, - parameter: { ...parameter, minDate: dateValue, maxDate: parameter.minDate }, + parameter: { ...parameter, dateMin: dateValue, dateMax: parameter.dateMin }, }) } else { - const diff = parameter.maxDate.getTime() - parameter.minDate.getTime() + const diff = parameter.dateMax.getTime() - parameter.dateMin.getTime() onValueChange({ dataType: 'dateTime', operator, - parameter: { ...parameter, minDate: new Date(dateValue.getTime() - diff), maxDate: dateValue }, + parameter: { ...parameter, dateMin: new Date(dateValue.getTime() - diff), dateMax: dateValue }, }) } } else { onValueChange({ dataType: 'dateTime', operator, - parameter: { ...parameter, maxDate: dateValue }, + parameter: { ...parameter, dateMax: dateValue }, }) } setTemporaryMaxDateValue(null) @@ -511,12 +508,12 @@ export const DatetimeFilterPopUp = forwardRef( { onValueChange({ dataType: 'dateTime', operator, - parameter: { ...parameter, compareDate }, + parameter: { ...parameter, dateValue: compareDate }, }) }} allowRemove={true} @@ -573,7 +570,7 @@ export const TagsFilterPopUp = forwardRef( return suggestion }, [value]) const parameter = value?.parameter ?? {} - const selectedTags = (Array.isArray(parameter.multiOptionSearch) ? parameter.multiOptionSearch : []) as string[] + const selectedTags = (Array.isArray(parameter.uuidValues) ? parameter.uuidValues : []) as string[] const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' @@ -598,7 +595,7 @@ export const TagsFilterPopUp = forwardRef( onValueChange({ dataType: 'multiTags', operator, - parameter: { ...parameter, multiOptionSearch: selected.length > 0 ? selected : undefined }, + parameter: { ...parameter, uuidValues: selected.length > 0 ? selected : undefined }, }) }} buttonProps={{ className: 'min-w-64' }} @@ -635,8 +632,8 @@ export const TagsSingleFilterPopUp = forwardRef 0 ? selected : undefined }, + parameter: { ...parameter, uuidValues: selected.length > 0 ? selected : undefined }, }) }} buttonProps={{ className: 'min-w-64' }} @@ -679,7 +676,7 @@ export const TagsSingleFilterPopUp = forwardRef(f export interface DataTypeFilterPopUpProps extends FilterPopUpProps { dataType: DataType, tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, + operatorOverrides?: FilterOperator[], } export const FilterPopUp = forwardRef(function FilterPopUp ({ name, diff --git a/src/components/user-interaction/data/filter-function.ts b/src/components/user-interaction/data/filter-function.ts index ce591f4..0e7ba36 100644 --- a/src/components/user-interaction/data/filter-function.ts +++ b/src/components/user-interaction/data/filter-function.ts @@ -6,16 +6,16 @@ import { type FilterOperator } from './FilterOperator' import type { DataType } from './data-types' export type FilterParameter = { - searchText?: string, - isCaseSensitive?: boolean, - compareValue?: number, - minNumber?: number, - maxNumber?: number, - compareDate?: Date, - minDate?: Date, - maxDate?: Date, - multiOptionSearch?: unknown[], - singleOptionSearch?: unknown, + stringValue?: string, + numberValue?: number, + numberMin?: number, + numberMax?: number, + booleanValue?: boolean, + dateValue?: Date, + dateMin?: Date, + dateMax?: Date, + uuidValue?: unknown, + uuidValues?: unknown[], } const allowedOperatorsByDataType: Record = { @@ -53,12 +53,12 @@ function isParameterValidForOperator( switch (dataType) { case 'text': { - return typeof parameter.searchText === 'string' + return typeof parameter.stringValue === 'string' } case 'number': { if (operator === 'between' || operator === 'notBetween') { - const min = parameter.minNumber - const max = parameter.maxNumber + const min = parameter.numberMin + const max = parameter.numberMax return ( typeof min === 'number' && !Number.isNaN(min) && @@ -67,14 +67,14 @@ function isParameterValidForOperator( min <= max ) } - const v = parameter.compareValue + const v = parameter.numberValue return typeof v === 'number' && !Number.isNaN(v) } case 'date': case 'dateTime': { if (operator === 'between' || operator === 'notBetween') { - const minDate = DateUtils.tryParseDate(parameter.minDate) - const maxDate = DateUtils.tryParseDate(parameter.maxDate) + const minDate = DateUtils.tryParseDate(parameter.dateMin) + const maxDate = DateUtils.tryParseDate(parameter.dateMax) if (!minDate || !maxDate) return false const minNorm = dataType === 'date' ? DateUtils.toOnlyDate(minDate).getTime() @@ -84,19 +84,19 @@ function isParameterValidForOperator( : DateUtils.toDateTimeOnly(maxDate).getTime() return minNorm <= maxNorm } - return DateUtils.tryParseDate(parameter.compareDate) != null + return DateUtils.tryParseDate(parameter.dateValue) != null } case 'boolean': return true case 'multiTags': { - return Array.isArray(parameter.multiOptionSearch) + return Array.isArray(parameter.uuidValues) } case 'singleTag': { if (operator === 'contains' || operator === 'notContains') { - return Array.isArray(parameter.multiOptionSearch) + return Array.isArray(parameter.uuidValues) } if(operator === 'equals' || operator === 'notEquals') { - return typeof parameter.singleOptionSearch === 'string' + return typeof parameter.uuidValue === 'string' } return true } @@ -124,10 +124,8 @@ export const FilterValueUtils = { * Filters a text value based on the provided filter value. */ function filterText(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { - const isCaseSensitive = parameter.isCaseSensitive ?? false - - const searchText = isCaseSensitive ? parameter.searchText ?? '' : (parameter.searchText ?? '').toLowerCase() - const cellText = isCaseSensitive ? value?.toString() ?? '' : value?.toString().toLowerCase() ?? '' + const searchText = parameter.stringValue ?? '' + const cellText = value?.toString() ?? '' switch (operator) { case 'equals': @@ -167,21 +165,21 @@ function filterNumber(value: unknown, operator: FilterOperator, parameter: Filte switch (operator) { case 'equals': - return value === parameter.compareValue + return value === parameter.numberValue case 'notEquals': - return value !== parameter.compareValue + return value !== parameter.numberValue case 'greaterThan': - return value > (parameter.compareValue ?? 0) + return value > (parameter.numberValue ?? 0) case 'greaterThanOrEqual': - return value >= (parameter.compareValue ?? 0) + return value >= (parameter.numberValue ?? 0) case 'lessThan': - return value < (parameter.compareValue ?? 0) + return value < (parameter.numberValue ?? 0) case 'lessThanOrEqual': - return value <= (parameter.compareValue ?? 0) + return value <= (parameter.numberValue ?? 0) case 'between': - return value >= (parameter.minNumber ?? -Infinity) && value <= (parameter.maxNumber ?? Infinity) + return value >= (parameter.numberMin ?? -Infinity) && value <= (parameter.numberMax ?? Infinity) case 'notBetween': - return value < (parameter.minNumber ?? -Infinity) || value > (parameter.maxNumber ?? Infinity) + return value < (parameter.numberMin ?? -Infinity) || value > (parameter.numberMax ?? Infinity) case 'isUndefined': return value === undefined || value === null case 'isNotUndefined': @@ -211,44 +209,44 @@ function filterDate(value: unknown, operator: FilterOperator, parameter: FilterP switch (operator) { case 'equals': { - const filterDate = DateUtils.tryParseDate(parameter.compareDate) + const filterDate = DateUtils.tryParseDate(parameter.dateValue) if (!filterDate) return false return normalizedDate.getTime() === DateUtils.toOnlyDate(filterDate).getTime() } case 'notEquals': { - const filterDate = DateUtils.tryParseDate(parameter.compareDate) + const filterDate = DateUtils.tryParseDate(parameter.dateValue) if (!filterDate) return false return normalizedDate.getTime() !== DateUtils.toOnlyDate(filterDate).getTime() } case 'greaterThan': { - const filterDate = DateUtils.tryParseDate(parameter.compareDate) + const filterDate = DateUtils.tryParseDate(parameter.dateValue) if (!filterDate) return false return normalizedDate > DateUtils.toOnlyDate(filterDate) } case 'greaterThanOrEqual': { - const filterDate = DateUtils.tryParseDate(parameter.compareDate) + const filterDate = DateUtils.tryParseDate(parameter.dateValue) if (!filterDate) return false return normalizedDate >= DateUtils.toOnlyDate(filterDate) } case 'lessThan': { - const filterDate = DateUtils.tryParseDate(parameter.compareDate) + const filterDate = DateUtils.tryParseDate(parameter.dateValue) if (!filterDate) return false return normalizedDate < DateUtils.toOnlyDate(filterDate) } case 'lessThanOrEqual': { - const filterDate = DateUtils.tryParseDate(parameter.compareDate) + const filterDate = DateUtils.tryParseDate(parameter.dateValue) if (!filterDate) return false return normalizedDate <= DateUtils.toOnlyDate(filterDate) } case 'between': { - const minDate = DateUtils.tryParseDate(parameter.minDate) - const maxDate = DateUtils.tryParseDate(parameter.maxDate) + const minDate = DateUtils.tryParseDate(parameter.dateMin) + const maxDate = DateUtils.tryParseDate(parameter.dateMax) if (!minDate || !maxDate) return false return normalizedDate >= DateUtils.toOnlyDate(minDate) && normalizedDate <= DateUtils.toOnlyDate(maxDate) } case 'notBetween': { - const minDate = DateUtils.tryParseDate(parameter.minDate) - const maxDate = DateUtils.tryParseDate(parameter.maxDate) + const minDate = DateUtils.tryParseDate(parameter.dateMin) + const maxDate = DateUtils.tryParseDate(parameter.dateMax) if (!minDate || !maxDate) return false return normalizedDate < DateUtils.toOnlyDate(minDate) || normalizedDate > DateUtils.toOnlyDate(maxDate) } @@ -276,44 +274,44 @@ function filterDateTime(value: unknown, operator: FilterOperator, parameter: Fil switch (operator) { case 'equals': { - const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + const filterDatetime = DateUtils.tryParseDate(parameter.dateValue) if (!filterDatetime) return false return normalizedDatetime.getTime() === DateUtils.toDateTimeOnly(filterDatetime).getTime() } case 'notEquals': { - const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + const filterDatetime = DateUtils.tryParseDate(parameter.dateValue) if (!filterDatetime) return false return normalizedDatetime.getTime() !== DateUtils.toDateTimeOnly(filterDatetime).getTime() } case 'greaterThan': { - const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + const filterDatetime = DateUtils.tryParseDate(parameter.dateValue) if (!filterDatetime) return false return normalizedDatetime > DateUtils.toDateTimeOnly(filterDatetime) } case 'greaterThanOrEqual': { - const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + const filterDatetime = DateUtils.tryParseDate(parameter.dateValue) if (!filterDatetime) return false return normalizedDatetime >= DateUtils.toDateTimeOnly(filterDatetime) } case 'lessThan': { - const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + const filterDatetime = DateUtils.tryParseDate(parameter.dateValue) if (!filterDatetime) return false return normalizedDatetime < DateUtils.toDateTimeOnly(filterDatetime) } case 'lessThanOrEqual': { - const filterDatetime = DateUtils.tryParseDate(parameter.compareDate) + const filterDatetime = DateUtils.tryParseDate(parameter.dateValue) if (!filterDatetime) return false return normalizedDatetime <= DateUtils.toDateTimeOnly(filterDatetime) } case 'between': { - const minDatetime = DateUtils.tryParseDate(parameter.minDate) - const maxDatetime = DateUtils.tryParseDate(parameter.maxDate) + const minDatetime = DateUtils.tryParseDate(parameter.dateMin) + const maxDatetime = DateUtils.tryParseDate(parameter.dateMax) if (!minDatetime || !maxDatetime) return false return normalizedDatetime >= DateUtils.toDateTimeOnly(minDatetime) && normalizedDatetime <= DateUtils.toDateTimeOnly(maxDatetime) } case 'notBetween': { - const minDatetime = DateUtils.tryParseDate(parameter.minDate) - const maxDatetime = DateUtils.tryParseDate(parameter.maxDate) + const minDatetime = DateUtils.tryParseDate(parameter.dateMin) + const maxDatetime = DateUtils.tryParseDate(parameter.dateMax) if (!minDatetime || !maxDatetime) return false return normalizedDatetime < DateUtils.toDateTimeOnly(minDatetime) || normalizedDatetime > DateUtils.toDateTimeOnly(maxDatetime) } @@ -346,28 +344,28 @@ function filterBoolean(value: unknown, operator: FilterOperator): boolean { function filterMultiTags(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { switch (operator) { case 'equals': { - if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return false - if (value.length !== parameter.multiOptionSearch.length) return false + if (!Array.isArray(value) || !Array.isArray(parameter.uuidValues)) return false + if (value.length !== parameter.uuidValues.length) return false const valueSet = new Set(value) - const searchTagsSet = new Set(parameter.multiOptionSearch) + const searchTagsSet = new Set(parameter.uuidValues) if (valueSet.size !== searchTagsSet.size) return false return Array.from(valueSet).every(tag => searchTagsSet.has(tag)) } case 'notEquals': { - if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return true - if (value.length !== parameter.multiOptionSearch.length) return true + if (!Array.isArray(value) || !Array.isArray(parameter.uuidValues)) return true + if (value.length !== parameter.uuidValues.length) return true const valueSet = new Set(value) - const searchTagsSet = new Set(parameter.multiOptionSearch) + const searchTagsSet = new Set(parameter.uuidValues) if (valueSet.size !== searchTagsSet.size) return true return !Array.from(valueSet).every(tag => searchTagsSet.has(tag)) } case 'contains': { - if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return false - return parameter.multiOptionSearch.every(tag => value.includes(tag)) + if (!Array.isArray(value) || !Array.isArray(parameter.uuidValues)) return false + return parameter.uuidValues.every(tag => value.includes(tag)) } case 'notContains': { - if (!Array.isArray(value) || !Array.isArray(parameter.multiOptionSearch)) return true - return !parameter.multiOptionSearch.every(tag => value.includes(tag)) + if (!Array.isArray(value) || !Array.isArray(parameter.uuidValues)) return true + return !parameter.uuidValues.every(tag => value.includes(tag)) } case 'isUndefined': return value === undefined || value === null @@ -384,13 +382,13 @@ function filterMultiTags(value: unknown, operator: FilterOperator, parameter: Fi function filterSingleTag(value: unknown, operator: FilterOperator, parameter: FilterParameter): boolean { switch (operator) { case 'equals': - return value === parameter.singleOptionSearch + return value === parameter.uuidValue case 'notEquals': - return value !== parameter.singleOptionSearch + return value !== parameter.uuidValue case 'contains': - return parameter.multiOptionSearch?.includes(value) ?? false + return parameter.uuidValues?.includes(value) ?? false case 'notContains': - return !(parameter.multiOptionSearch?.includes(value) ?? false) + return !(parameter.uuidValues?.includes(value) ?? false) case 'isUndefined': return value === undefined || value === null case 'isNotUndefined': @@ -456,93 +454,93 @@ export function useFilterValueTranslation(): (value: FilterValue, options?: Filt switch (value.operator) { case 'equals': if (value.dataType === 'date' || value.dataType === 'dateTime') { - return translation('rEquals', { value: formatDateParam(p.compareDate, locale, dateFormat) ?? '-' }) + return translation('rEquals', { value: formatDateParam(p.dateValue, locale, dateFormat) ?? '-' }) } if (value.dataType === 'singleTag') { - return translation('rEquals', { value: tagToLabel(tags, p.singleOptionSearch) }) + return translation('rEquals', { value: tagToLabel(tags, p.uuidValue) }) } if (value.dataType === 'multiTags') { - const valueStr = (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + const valueStr = (p.uuidValues ?? []).map(v => tagToLabel(tags, v)).join(', ') return translation('rEquals', { value: valueStr }) } - return translation('rEquals', { value: String(p.searchText ?? p.compareValue ?? '') }) + return translation('rEquals', { value: String(p.stringValue ?? p.numberValue ?? '') }) case 'notEquals': if (value.dataType === 'date' || value.dataType === 'dateTime') { - return translation('rNotEquals', { value: formatDateParam(p.compareDate, locale, dateFormat) }) + return translation('rNotEquals', { value: formatDateParam(p.dateValue, locale, dateFormat) }) } if (value.dataType === 'singleTag') { - return translation('rNotEquals', { value: tagToLabel(tags, p.singleOptionSearch) }) + return translation('rNotEquals', { value: tagToLabel(tags, p.uuidValue) }) } if (value.dataType === 'multiTags') { - const valueStr = (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + const valueStr = (p.uuidValues ?? []).map(v => tagToLabel(tags, v)).join(', ') return translation('rNotEquals', { value: valueStr }) } - return translation('rNotEquals', { value: String(p.searchText ?? p.compareValue ?? '') }) + return translation('rNotEquals', { value: String(p.stringValue ?? p.numberValue ?? '') }) case 'contains': if (value.dataType === 'multiTags' || value.dataType === 'singleTag') { const valueStr = value.dataType === 'singleTag' - ? tagToLabel(tags, p.singleOptionSearch) - : (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + ? tagToLabel(tags, p.uuidValue) + : (p.uuidValues ?? []).map(v => tagToLabel(tags, v)).join(', ') return translation('rContains', { value: valueStr }) } - return translation('rContains', { value: String(p.searchText ?? '') }) + return translation('rContains', { value: String(p.stringValue ?? '') }) case 'notContains': if (value.dataType === 'multiTags' || value.dataType === 'singleTag') { const valueStr = value.dataType === 'singleTag' - ? tagToLabel(tags, p.singleOptionSearch) - : (p.multiOptionSearch ?? []).map(v => tagToLabel(tags, v)).join(', ') + ? tagToLabel(tags, p.uuidValue) + : (p.uuidValues ?? []).map(v => tagToLabel(tags, v)).join(', ') return translation('rNotContains', { value: valueStr }) } - return translation('rNotContains', { value: `"${String(p.searchText ?? '')}"` }) + return translation('rNotContains', { value: `"${String(p.stringValue ?? '')}"` }) case 'startsWith': - return translation('rStartsWith', { value: `"${String(p.searchText ?? '')}"` }) + return translation('rStartsWith', { value: `"${String(p.stringValue ?? '')}"` }) case 'endsWith': - return translation('rEndsWith', { value: `"${String(p.searchText ?? '')}"` }) + return translation('rEndsWith', { value: `"${String(p.stringValue ?? '')}"` }) case 'greaterThan': return translation('rGreaterThan', { value: value.dataType === 'date' || value.dataType === 'dateTime' - ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' - : String(p.compareValue ?? '-'), + ? formatDateParam(p.dateValue, locale, dateFormat) ?? '-' + : String(p.numberValue ?? '-'), }) case 'greaterThanOrEqual': return translation('rGreaterThanOrEqual', { value: value.dataType === 'date' || value.dataType === 'dateTime' - ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' - : String(p.compareValue ?? '-'), + ? formatDateParam(p.dateValue, locale, dateFormat) ?? '-' + : String(p.numberValue ?? '-'), }) case 'lessThan': return translation('rLessThan', { value: value.dataType === 'date' || value.dataType === 'dateTime' - ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' - : String(p.compareValue ?? '-'), + ? formatDateParam(p.dateValue, locale, dateFormat) ?? '-' + : String(p.numberValue ?? '-'), }) case 'lessThanOrEqual': return translation('rLessThanOrEqual', { value: value.dataType === 'date' || value.dataType === 'dateTime' - ? formatDateParam(p.compareDate, locale, dateFormat) ?? '-' - : String(p.compareValue ?? '-'), + ? formatDateParam(p.dateValue, locale, dateFormat) ?? '-' + : String(p.numberValue ?? '-'), }) case 'between': if (value.dataType === 'date' || value.dataType === 'dateTime') { return translation('rBetween', { - value1: formatDateParam(p.minDate, locale, dateFormat) ?? '-', - value2: formatDateParam(p.maxDate, locale, dateFormat) ?? '-', + value1: formatDateParam(p.dateMin, locale, dateFormat) ?? '-', + value2: formatDateParam(p.dateMax, locale, dateFormat) ?? '-', }) } return translation('rBetween', { - value1: String(p.minNumber ?? '-'), - value2: String(p.maxNumber ?? '-'), + value1: String(p.numberMin ?? '-'), + value2: String(p.numberMax ?? '-'), }) case 'notBetween': if (value.dataType === 'date' || value.dataType === 'dateTime') { return translation('rNotBetween', { - value1: formatDateParam(p.minDate, locale, dateFormat) ?? '-', - value2: formatDateParam(p.maxDate, locale, dateFormat) ?? '-', + value1: formatDateParam(p.dateMin, locale, dateFormat) ?? '-', + value2: formatDateParam(p.dateMax, locale, dateFormat) ?? '-', }) } return translation('rNotBetween', { - value1: String(p.minNumber ?? '-'), - value2: String(p.maxNumber ?? '-'), + value1: String(p.numberMin ?? '-'), + value2: String(p.numberMax ?? '-'), }) case 'isTrue': return translation('isTrue') diff --git a/src/components/user-interaction/input/DateTimeInput.tsx b/src/components/user-interaction/input/DateTimeInput.tsx index 607481e..af3b511 100644 --- a/src/components/user-interaction/input/DateTimeInput.tsx +++ b/src/components/user-interaction/input/DateTimeInput.tsx @@ -67,18 +67,16 @@ export const DateTimeInput = forwardRef(fu defaultValue: initialValue, }) const [dialogValue, setDialogValue] = useState(state) - const [stringInputState, setStringInputState] = useState<{ state: string, date?: Date }>({ + const [stringInputState, setStringInputState] = useState<{ state: string, date?: Date, mode: DateTimeFormat }>({ state: state ? DateUtils.toInputString(state, mode, precision) : '', date: undefined, + mode, }) - useEffect(() => { - setDialogValue(state) - setStringInputState({ - state: state ? DateUtils.toInputString(state, mode) : '', - date: undefined, - }) - }, [mode, state]) + const safeInputString = useMemo(() => { + if(!state) return '' + return stringInputState.mode !== mode ? DateUtils.toInputString(state, mode, precision) : stringInputState.state + }, [stringInputState.mode, stringInputState.state, mode, state, precision]) const changeOpenWrapper = useCallback((isOpen: boolean) => { onDialogOpeningChange?.(isOpen) @@ -114,7 +112,7 @@ export const DateTimeInput = forwardRef(fu {...props} ref={innerRef} id={ids.input} - value={stringInputState.state} + value={safeInputString} onClick={(event) => { event.preventDefault() @@ -122,6 +120,11 @@ export const DateTimeInput = forwardRef(fu onFocus={(event) => { event.preventDefault() }} + onKeyDown={(event) => { + if(event.key === ' ') { + event.preventDefault() + } + }} onChange={(event) => { const date = new Date(event.target.value ?? '') const isValid = !isNaN(date.getTime()) @@ -137,6 +140,7 @@ export const DateTimeInput = forwardRef(fu setStringInputState({ state: event.target.value, date: isValid ? date : undefined, + mode, }) }} onBlur={(event) => { @@ -152,6 +156,7 @@ export const DateTimeInput = forwardRef(fu setStringInputState({ state: state ? DateUtils.toInputString(state, mode) : '', date: undefined, + mode, }) } }} diff --git a/src/components/user-interaction/input/FlexibleDateTimeInput.tsx b/src/components/user-interaction/input/FlexibleDateTimeInput.tsx index 0f4f8ea..8cac318 100644 --- a/src/components/user-interaction/input/FlexibleDateTimeInput.tsx +++ b/src/components/user-interaction/input/FlexibleDateTimeInput.tsx @@ -27,22 +27,27 @@ export const FlexibleDateTimeInput = forwardRef fixedTimeOverride ?? new Date(23, 59, 59, 999), [fixedTimeOverride]) - const [preferredMode, setPreferredMode] = useState(defaultMode) - const mode = useMemo(() => { - if(!value) return preferredMode + const fixedTime = useMemo(() => fixedTimeOverride ?? new Date(1970, 0, 1, 23, 59, 59, 999), [fixedTimeOverride]) + const [preferredMode, setPreferredMode] = useState(() => { + if(!value) return defaultMode if(DateUtils.sameTime(value, fixedTime, true, true)) { return 'date' } return 'dateTime' - }, [preferredMode, value, fixedTime]) + }) + return ( { + if(preferredMode === 'date') + setValue(DateUtils.withTime(value, fixedTime)) + else + setValue(DateUtils.isLastMillisecondOfDay(value) ? new Date(value.getTime() - 1) : new Date(value.getTime() + 1)) + }} actions={[ ...actions, { const newMode = preferredMode === 'date' ? 'dateTime' : 'date' - setPreferredMode(prev => prev === 'date' ? 'dateTime' : 'date') if(value) { if(newMode === 'date') { setValue(DateUtils.withTime(value, fixedTime)) @@ -61,6 +65,7 @@ export const FlexibleDateTimeInput = forwardRef {preferredMode === 'date' ? : } diff --git a/stories/Layout/Table/FilterListTable.stories.tsx b/stories/Layout/Table/FilterListTable.stories.tsx index 5441f3d..abbc2b6 100644 --- a/stories/Layout/Table/FilterListTable.stories.tsx +++ b/stories/Layout/Table/FilterListTable.stories.tsx @@ -36,7 +36,7 @@ const createRow = (): Row => ({ hasChildren: faker.datatype.boolean(), }) -const AgeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopUpBuilderProps) => { +const AgeFilterPopUp = ({ value, onValueChange, onRemove, name, onClose: close, ...props }: FilterListPopUpBuilderProps) => { const translation = useHightideTranslation() const id = useId() const ids = { @@ -61,7 +61,9 @@ const AgeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopU return ( onValueChange({ dataType: 'number', parameter, operator: newOperator })} onRemove={onRemove} @@ -73,9 +75,9 @@ const AgeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopU buttonProps={{ id: ids.range }} - value={parameter.minNumber !== undefined && parameter.maxNumber !== undefined ? [parameter.minNumber, parameter.maxNumber] : null} + value={parameter.numberMin !== undefined && parameter.numberMax !== undefined ? [parameter.numberMin, parameter.numberMax] : null} onValueChange={(newRange) => { - onValueChange({ ...value, parameter: { ...parameter, minNumber: newRange[0], maxNumber: newRange[1] } }) + onValueChange({ ...value, parameter: { ...parameter, numberMin: newRange[0], numberMax: newRange[1] } }) }} compareFunction={(a, b) => { if (a === null || b === null) return false @@ -92,7 +94,7 @@ const AgeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopU { @@ -100,7 +102,7 @@ const AgeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopU onValueChange({ dataType: 'number', operator, - parameter: { ...parameter, compareValue: isNaN(num) ? undefined : num }, + parameter: { ...parameter, numberValue: isNaN(num) ? undefined : num }, }) }} className="min-w-64"