Refactor ObjectProperty component to replace DatePicker with TimeEdit for enhanced time expression handling. Introduce TimeEdit component for flexible time input, supporting relative time expressions and improved user interaction.

This commit is contained in:
Tom Butcher 2026-07-04 17:33:59 +01:00
parent b230302745
commit bdb2327aac
2 changed files with 187 additions and 12 deletions

View File

@ -6,11 +6,11 @@ import {
InputNumber,
Form,
Select,
DatePicker,
Switch
} from 'antd'
import IdDisplay from './IdDisplay'
import TimeDisplay from './TimeDisplay'
import TimeEdit from './TimeEdit'
import dayjs from 'dayjs'
import EmailDisplay from './EmailDisplay'
import UrlDisplay from './UrlDisplay'
@ -51,6 +51,10 @@ import { round } from '../utils/Utils'
const { Text } = Typography
const timeEditFormItemProps = {
getValueProps: (v) => ({ value: v ? dayjs(v) : null })
}
const ObjectProperty = ({
type = 'text',
prefix,
@ -708,14 +712,7 @@ const ObjectProperty = ({
/>
)
case 'dateTime':
return (
<DatePicker
showTime
style={{ width: '100%' }}
{...inputProps}
{...(useFormItem ? {} : { value: value ? dayjs(value) : null })}
/>
)
return <TimeEdit {...inputProps} />
case 'country':
return <CountrySelect {...inputProps} />
case 'color':
@ -854,9 +851,7 @@ const ObjectProperty = ({
{...(type === 'color'
? { valuePropName: 'value', getValueFromEvent: (v) => v }
: {})}
{...(type === 'dateTime'
? { getValueProps: (v) => ({ value: v ? dayjs(v) : null }) }
: {})}
{...(type === 'dateTime' ? timeEditFormItemProps : {})}
>
{renderInput()}
</Form.Item>

View File

@ -0,0 +1,180 @@
import { useState, useRef } from 'react'
import PropTypes from 'prop-types'
import { DatePicker, Input } from 'antd'
import dayjs from 'dayjs'
import FunctionIcon from '../../Icons/FunctionIcon'
const OPERATOR_KEYS = ['+', '-']
const TIME_UNITS = {
s: 'second',
m: 'minute',
h: 'hour',
d: 'day',
w: 'week',
M: 'month',
y: 'year'
}
const TERM_REGEX = /([+-])(\d+(?:\.\d+)?)([smhdwMy])/g
const RELATIVE_TERM_START = /[+-]\d+(?:\.\d+)?[smhdwMy]/
const DATE_EXPR_FORMAT = 'YYYY-MM-DD HH:mm:ss'
function formatExprBase(date) {
if (date == null || !dayjs(date).isValid()) return ''
return dayjs(date).format(DATE_EXPR_FORMAT)
}
function applyTimeExpr(fallbackBase, expr) {
const trimmed = String(expr).trim()
if (!trimmed) return null
const termStart = trimmed.search(RELATIVE_TERM_START)
if (termStart === -1) return null
let base
let termsPart
if (termStart === 0) {
base =
fallbackBase && dayjs(fallbackBase).isValid()
? dayjs(fallbackBase)
: dayjs()
termsPart = trimmed
} else {
const baseStr = trimmed.slice(0, termStart).trim()
const parsed = dayjs(baseStr)
base = parsed.isValid()
? parsed
: fallbackBase && dayjs(fallbackBase).isValid()
? dayjs(fallbackBase)
: dayjs()
termsPart = trimmed.slice(termStart)
}
const termsStr = termsPart.replace(/\s/g, '')
const terms = [...termsStr.matchAll(TERM_REGEX)]
if (!terms.length) return null
const consumed = terms.map((t) => t[0]).join('')
if (consumed !== termsStr) return null
let result = base
for (const [, sign, amount, unit] of terms) {
const unitKey = TIME_UNITS[unit]
if (!unitKey) return null
const delta = sign === '+' ? parseFloat(amount) : -parseFloat(amount)
result = result.add(delta, unitKey)
}
return result
}
const TimeEdit = ({
value,
onChange,
onBlur,
disabled,
...rest
}) => {
const [isExprMode, setIsExprMode] = useState(false)
const [exprValue, setExprValue] = useState('')
const inputRef = useRef(null)
const dayjsValue =
value == null ? null : dayjs.isDayjs(value) ? value : dayjs(value)
const pickerValue =
dayjsValue && dayjsValue.isValid() ? dayjsValue : null
const switchToExprMode = (initialValue) => {
setIsExprMode(true)
setExprValue(initialValue)
setTimeout(() => {
const input = inputRef.current?.getElementsByTagName('input')[0]
input?.focus()
}, 0)
}
const exitExprMode = (result) => {
setIsExprMode(false)
setExprValue('')
if (result != null && result.isValid()) {
onChange?.(result)
}
}
const commitExpr = () => {
const expr = exprValue.trim()
if (expr && expr.match(/[+-]/) && expr.match(/[smhdwMy]/)) {
const result = applyTimeExpr(pickerValue, expr)
if (result != null) {
exitExprMode(result)
return
}
}
exitExprMode(null)
}
const handleDatePickerKeyDown = (e) => {
if (OPERATOR_KEYS.includes(e.key)) {
e.preventDefault()
e.stopPropagation()
switchToExprMode(formatExprBase(pickerValue) + e.key)
}
}
const handleInputKeyDown = (e) => {
if (e.key === 'Enter' || e.key === '=') {
e.preventDefault()
const result = applyTimeExpr(pickerValue, exprValue)
if (result != null) {
exitExprMode(result)
}
}
}
const handleInputBlur = (e) => {
commitExpr()
onBlur?.(e)
}
if (isExprMode) {
return (
<div className='input-number-cal' ref={inputRef}>
<Input
value={exprValue}
onChange={(e) => setExprValue(e.target.value)}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
disabled={disabled}
placeholder='+1d, -2m, +1h'
style={{ width: '100%' }}
/>
<div className='input-number-cal-icon'>
<FunctionIcon style={{ fontSize: 24 }} />
</div>
</div>
)
}
return (
<DatePicker
showTime
style={{ width: '100%' }}
value={pickerValue}
onChange={onChange}
onKeyDown={handleDatePickerKeyDown}
disabled={disabled}
{...rest}
/>
)
}
TimeEdit.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
onChange: PropTypes.func,
onBlur: PropTypes.func,
disabled: PropTypes.bool
}
export default TimeEdit