diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx
index 7c4186f..4eac600 100644
--- a/src/components/Dashboard/common/ObjectProperty.jsx
+++ b/src/components/Dashboard/common/ObjectProperty.jsx
@@ -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 (
-
- )
+ return
case 'country':
return
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()}
diff --git a/src/components/Dashboard/common/TimeEdit.jsx b/src/components/Dashboard/common/TimeEdit.jsx
new file mode 100644
index 0000000..1bc380a
--- /dev/null
+++ b/src/components/Dashboard/common/TimeEdit.jsx
@@ -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 (
+
+
setExprValue(e.target.value)}
+ onKeyDown={handleInputKeyDown}
+ onBlur={handleInputBlur}
+ disabled={disabled}
+ placeholder='+1d, -2m, +1h'
+ style={{ width: '100%' }}
+ />
+
+
+
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+TimeEdit.propTypes = {
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ onChange: PropTypes.func,
+ onBlur: PropTypes.func,
+ disabled: PropTypes.bool
+}
+
+export default TimeEdit