fix large integer precision handling in Vue int widgets (#5787)

## Summary

Fixed increment/decrement button lockup in number widgets when values
exceed JavaScript's safe integer limit (2^53 - 1).

## Changes

- **What**: Added precision-aware button disabling and user feedback to
`WidgetInputNumberInput` component using
[Number.isSafeInteger()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger)
- We still need to support values greater than 2^53 because they may be
in workflows.

## Review Focus

JavaScript floating-point precision behavior at scale - buttons hide
when arithmetic operations like `value + 1` would be unreliable due to
IEEE 754 limitations. Test coverage includes edge cases (NaN, Infinity)
and boundary conditions at MAX_SAFE_INTEGER.

```mermaid
graph TD
    A[User Input] --> B{Value > 2^53?}
    B -->|No| C[Show Buttons]
    B -->|Yes| D[Hide Buttons]
    D --> E[Show Tooltip]
    E --> F[User Can Still Type]
    
    style A fill:#f9f9f9,stroke:#333,color:#000
    style F fill:#f9f9f9,stroke:#333,color:#000
```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5787-fix-large-integer-precision-handling-in-Vue-int-widgets-27a6d73d365081d9ae00e485740cfafb)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2025-09-26 18:11:41 -07:00
committed by GitHub
parent edcbcdfa84
commit 75d31f9d57
2 changed files with 198 additions and 25 deletions

View File

@@ -2,6 +2,7 @@
import InputNumber from 'primevue/inputnumber'
import { computed } from 'vue'
import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import {
@@ -14,10 +15,19 @@ import WidgetLayoutField from './layout/WidgetLayoutField.vue'
const props = defineProps<{
widget: SimplifiedWidget<number>
modelValue: number
readonly?: boolean
}>()
const modelValue = defineModel<number>({ default: 0 })
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
const { localValue, onChange } = useNumberWidgetValue(
props.widget,
props.modelValue,
emit
)
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
@@ -53,34 +63,52 @@ const stepValue = computed(() => {
const useGrouping = computed(() => {
return props.widget.options?.useGrouping === true
})
// Check if increment/decrement buttons should be disabled due to precision limits
const buttonsDisabled = computed(() => {
const currentValue = localValue.value || 0
return !Number.isSafeInteger(currentValue)
})
// Tooltip message for disabled buttons
const buttonTooltip = computed(() => {
if (props.readonly) return null
if (buttonsDisabled.value) {
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
}
return null
})
</script>
<template>
<WidgetLayoutField :widget>
<InputNumber
v-model="modelValue"
v-bind="filteredProps"
show-buttons
button-layout="horizontal"
size="small"
:disabled="readonly"
:step="stepValue"
:use-grouping="useGrouping"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
:pt="{
incrementButton:
'!rounded-r-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40',
decrementButton:
'!rounded-l-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40'
}"
>
<template #incrementicon>
<span class="pi pi-plus text-sm" />
</template>
<template #decrementicon>
<span class="pi pi-minus text-sm" />
</template>
</InputNumber>
<div v-tooltip="buttonTooltip">
<InputNumber
v-model="localValue"
v-bind="filteredProps"
:show-buttons="!buttonsDisabled"
button-layout="horizontal"
size="small"
:disabled="readonly"
:step="stepValue"
:use-grouping="useGrouping"
:class="cn(WidgetInputBaseClass, 'w-full text-xs')"
:pt="{
incrementButton:
'!rounded-r-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40',
decrementButton:
'!rounded-l-lg bg-transparent border-none hover:bg-zinc-500/30 active:bg-zinc-500/40'
}"
@update:model-value="onChange"
>
<template #incrementicon>
<span class="pi pi-plus text-sm" />
</template>
<template #decrementicon>
<span class="pi pi-minus text-sm" />
</template>
</InputNumber>
</div>
</WidgetLayoutField>
</template>