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:
parent
b230302745
commit
bdb2327aac
@ -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>
|
||||
|
||||
180
src/components/Dashboard/common/TimeEdit.jsx
Normal file
180
src/components/Dashboard/common/TimeEdit.jsx
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user