Compare commits

...

4 Commits

Author SHA1 Message Date
Evan Rusackas
256bf5a4ba fix: convert it() to test() per project conventions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 10:45:42 -08:00
Evan Rusackas
49dc8b557f fix: Address bot review comments
- Add await to userEvent calls in test to prevent flaky behavior
- Simplify calculateStep to use consistent approach for all ranges
- Remove floating-point string parsing that could produce incorrect results
  for values like 0.07-0.05 = 0.020000000000000004
- Add fallback for edge cases where step could be 0

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 10:45:01 -08:00
Evan Rusackas
d76e5ab529 fix: Address review feedback for Range filter decimal support
- Remove redundant `|| 0.001` fallback since Math.pow() can never return falsy
- Change Math.ceil to Math.round for more balanced step sizes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 10:45:00 -08:00
Evan Rusackas
fd1265e9e4 fix(filters): Enable decimal values in Range filter slider
Fixes #34737

The Range filter was defaulting to integer steps, making it impossible to select decimal values via the slider. This was particularly problematic for columns with small decimal ranges (e.g., 0.03 to 1.08).

This fix adds dynamic step size calculation based on the data range:
- For small ranges (< 1), calculates appropriate decimal precision
- For larger ranges, uses steps that provide ~100 increments
- Ensures step values are "nice" numbers (0.001, 0.01, 0.1, 1, 10, etc.)

The solution maintains backward compatibility and works with all range types (small decimals, large numbers, negative values).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-10 10:45:00 -08:00
2 changed files with 149 additions and 0 deletions

View File

@@ -320,4 +320,127 @@ describe('RangeFilterPlugin', () => {
expect(sliders.length).toBeGreaterThan(0);
});
});
describe('Decimal value handling', () => {
test('should handle decimal ranges correctly (0.03 to 1.08)', () => {
const decimalProps = {
queriesData: [
{
rowcount: 1,
colnames: ['min', 'max'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
data: [{ min: 0.03, max: 1.08 }],
applied_filters: [],
rejected_filters: [],
},
],
filterState: { value: [0.5, 0.8] },
};
getWrapper(decimalProps);
const inputs = screen.getAllByRole('spinbutton');
expect(inputs).toHaveLength(2);
expect(inputs[0]).toHaveValue('0.5');
expect(inputs[1]).toHaveValue('0.8');
// Verify the slider exists and can handle decimal values
const sliders = screen.getAllByRole('slider');
expect(sliders.length).toBeGreaterThan(0);
});
test('should calculate appropriate step size for small decimal ranges', () => {
const smallRangeProps = {
queriesData: [
{
rowcount: 1,
colnames: ['min', 'max'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
data: [{ min: 0.001, max: 0.01 }],
applied_filters: [],
rejected_filters: [],
},
],
filterState: { value: [0.005, 0.008] },
};
getWrapper(smallRangeProps);
const inputs = screen.getAllByRole('spinbutton');
expect(inputs[0]).toHaveValue('0.005');
expect(inputs[1]).toHaveValue('0.008');
});
test('should handle very large ranges with appropriate step size', () => {
const largeRangeProps = {
queriesData: [
{
rowcount: 1,
colnames: ['min', 'max'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
data: [{ min: 0, max: 1000000 }],
applied_filters: [],
rejected_filters: [],
},
],
filterState: { value: [100000, 500000] },
};
getWrapper(largeRangeProps);
const inputs = screen.getAllByRole('spinbutton');
expect(inputs[0]).toHaveValue('100000');
expect(inputs[1]).toHaveValue('500000');
});
test('should handle negative decimal ranges', () => {
const negativeDecimalProps = {
queriesData: [
{
rowcount: 1,
colnames: ['min', 'max'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
data: [{ min: -1.5, max: 2.5 }],
applied_filters: [],
rejected_filters: [],
},
],
filterState: { value: [-0.5, 1.5] },
};
getWrapper(negativeDecimalProps);
const inputs = screen.getAllByRole('spinbutton');
expect(inputs[0]).toHaveValue('-0.5');
expect(inputs[1]).toHaveValue('1.5');
});
test('should allow decimal input via keyboard', async () => {
const decimalProps = {
queriesData: [
{
rowcount: 1,
colnames: ['min', 'max'],
coltypes: [GenericDataType.Numeric, GenericDataType.Numeric],
data: [{ min: 0, max: 10 }],
applied_filters: [],
rejected_filters: [],
},
],
filterState: { value: [null, null] },
};
getWrapper(decimalProps);
const inputs = screen.getAllByRole('spinbutton');
const fromInput = inputs[0];
await userEvent.clear(fromInput);
await userEvent.type(fromInput, '2.5');
await userEvent.tab();
expect(setDataMask).toHaveBeenCalledWith(
expect.objectContaining({
filterState: expect.objectContaining({
value: [2.5, null],
}),
}),
);
});
});
});

View File

@@ -234,6 +234,30 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
const [row] = data;
// @ts-expect-error
const { min, max }: { min: number; max: number } = row;
// Calculate appropriate step size for decimal values
// Uses a consistent approach for all ranges to avoid floating-point string parsing issues
const calculateStep = useCallback((minValue: number, maxValue: number) => {
const range = maxValue - minValue;
if (range <= 0) return 0.01;
// Calculate step to give approximately 100 steps across the range
const idealSteps = 100;
let step = range / idealSteps;
// Round step to a nice value (0.0001, 0.001, 0.01, 0.1, 1, 10, etc.)
const magnitude = Math.pow(10, Math.floor(Math.log10(step)));
step = Math.round(step / magnitude) * magnitude;
// Ensure we don't return 0 for very small ranges
return step || 0.0001;
}, []);
const sliderStep = useMemo(
() =>
min !== undefined && max !== undefined ? calculateStep(min, max) : 0.01,
[min, max, calculateStep],
);
const { groupby, enableSingleValue, enableEmptyFilter, defaultValue } =
formData;
@@ -548,6 +572,7 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
<Slider
min={min}
max={max}
step={sliderStep}
value={Array.isArray(sliderValue) ? sliderValue[0] : sliderValue}
onChange={handleSliderChange}
tooltip={{
@@ -562,6 +587,7 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
<Slider
min={min}
max={max}
step={sliderStep}
range
value={Array.isArray(sliderValue) ? sliderValue : [min, sliderValue]}
onChange={handleSliderChange}