diff --git a/packages/core/client/src/flow/actions/formAssignRules.tsx b/packages/core/client/src/flow/actions/formAssignRules.tsx index 126f910f209..5287150c122 100644 --- a/packages/core/client/src/flow/actions/formAssignRules.tsx +++ b/packages/core/client/src/flow/actions/formAssignRules.tsx @@ -89,6 +89,7 @@ const FormAssignRulesUI = observer( showValueEditorWhenNoField isTitleFieldCandidate={isTitleFieldCandidate} onSyncAssociationTitleField={onSyncAssociationTitleField} + enableDateVariableAsConstant /> ); }, diff --git a/packages/core/client/src/flow/actions/linkageRules.tsx b/packages/core/client/src/flow/actions/linkageRules.tsx index 99fb0c14785..1285396db50 100644 --- a/packages/core/client/src/flow/actions/linkageRules.tsx +++ b/packages/core/client/src/flow/actions/linkageRules.tsx @@ -908,6 +908,7 @@ const FieldAssignRulesActionComponent: React.FC< fixedMode={fixedMode} isTitleFieldCandidate={isTitleFieldCandidate} onSyncAssociationTitleField={onSyncAssociationTitleField} + enableDateVariableAsConstant /> ); }; diff --git a/packages/core/client/src/flow/components/FieldAssignExactDatePicker.tsx b/packages/core/client/src/flow/components/FieldAssignExactDatePicker.tsx new file mode 100644 index 00000000000..9a7623c63c7 --- /dev/null +++ b/packages/core/client/src/flow/components/FieldAssignExactDatePicker.tsx @@ -0,0 +1,225 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import React, { useMemo, useState } from 'react'; +import { DatePicker, Select, Space } from 'antd'; +import { dayjs, getDateTimeFormat, getPickerFormat } from '@nocobase/utils/client'; +import { useFlowContext } from '@nocobase/flow-engine'; +import { inferPickerType } from '../../schema-component'; + +const stripTimeFromFormat = (format?: string) => + format ? format.replace(/\s*HH?:mm(?::ss)?(?:\.SSS)?/g, '').trim() : format; + +export type ExactDatePickerMode = 'year' | 'quarter' | 'month' | 'date'; + +export type ExactDatePickerValue = dayjs.Dayjs | [dayjs.Dayjs, dayjs.Dayjs] | null; + +export interface FieldAssignExactDatePickerProps { + picker?: ExactDatePickerMode; + format?: string; + showTime?: boolean; + timeFormat?: string; + value?: unknown; + isRange?: boolean; + onChange?: (value: ExactDatePickerValue) => void; + style?: React.CSSProperties; +} + +function getRawSingleValue(value: unknown, isRange: boolean): unknown { + if (!isRange) { + return value; + } + if (!Array.isArray(value)) return undefined; + return value[0]; +} + +function inferPickerFromRawValue(value: unknown, fallback: ExactDatePickerMode): ExactDatePickerMode { + if (typeof value === 'string') { + return inferPickerType(value, fallback) as ExactDatePickerMode; + } + + return fallback; +} + +function parseDateByFormat(value: string, format: string): dayjs.Dayjs | null { + const raw = String(value || '').trim(); + if (!raw) return null; + + const hasTimezone = /(?:Z|[+-]\d{2}:\d{2})$/i.test(raw); + if (hasTimezone) { + const parsed = dayjs(raw); + if (parsed.isValid()) { + return parsed; + } + } + + if (format) { + const strict = dayjs(raw, format, true); + if (strict.isValid()) { + return strict; + } + } + + const fallback = dayjs(raw); + if (fallback.isValid()) { + return fallback; + } + + if (format) { + const loose = dayjs(raw, format); + if (loose.isValid()) { + return loose; + } + } + + return null; +} + +function parseDateFromRawValue(value: unknown, format: string): dayjs.Dayjs | null { + if (dayjs.isDayjs(value)) { + return value; + } + + if (value instanceof Date) { + const parsedDate = dayjs(value); + return parsedDate.isValid() ? parsedDate : null; + } + + if (typeof value === 'string') { + return parseDateByFormat(value, format); + } + + return null; +} + +function parseRangeValue(value: unknown, format: string): [dayjs.Dayjs, dayjs.Dayjs] | null { + if (!Array.isArray(value)) { + return null; + } + + const left = parseDateFromRawValue(value[0], format); + const right = parseDateFromRawValue(value[1], format); + + if (!left || !right) { + return null; + } + + return [left, right]; +} + +export const FieldAssignExactDatePicker: React.FC = (props) => { + const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style } = props; + const flowCtx = useFlowContext(); + const t = flowCtx.model.translate.bind(flowCtx.model); + + const rawSingleValue = useMemo(() => getRawSingleValue(value, isRange), [isRange, value]); + + const [targetPicker, setTargetPicker] = useState(() => { + return inferPickerFromRawValue(rawSingleValue, picker); + }); + + const getResolvedFormat = (nextPicker: ExactDatePickerMode) => { + const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker); + const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat; + return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat); + }; + + const resolvedFormat = useMemo( + () => getResolvedFormat(targetPicker), + [targetPicker, format, picker, showTime, timeFormat], + ); + + const singleValue = useMemo(() => { + if (isRange) return null; + return parseDateFromRawValue(value, resolvedFormat); + }, [isRange, value, resolvedFormat]); + + const rangeValue = useMemo(() => { + if (!isRange) return null; + return parseRangeValue(value, resolvedFormat); + }, [isRange, value, resolvedFormat]); + + const pickerOptions = useMemo( + () => [ + { label: t('Date'), value: 'date' }, + { label: t('Month'), value: 'month' }, + { label: t('Quarter'), value: 'quarter' }, + { label: t('Year'), value: 'year' }, + ], + [t], + ); + + const handlePickerTypeChange = (nextPicker: ExactDatePickerMode) => { + setTargetPicker(nextPicker); + + if (!onChange) { + return; + } + + if (isRange) { + if (rangeValue) { + onChange(rangeValue); + } + return; + } + + if (singleValue) { + onChange(singleValue); + } + }; + + const singlePickerProps: any = { + utc: true, + underFilter: true, + format: resolvedFormat, + picker: targetPicker, + inputReadOnly: flowCtx.isMobileLayout, + showTime: showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false, + value: singleValue, + onChange: (nextDate: dayjs.Dayjs | null) => { + if (nextDate && dayjs.isDayjs(nextDate)) { + onChange?.(nextDate); + return; + } + onChange?.(null); + }, + style: { flex: 1 }, + }; + + const rangePickerProps: any = { + utc: true, + underFilter: true, + format: resolvedFormat, + picker: targetPicker, + inputReadOnly: flowCtx.isMobileLayout, + showTime: showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')] } : false, + value: rangeValue, + onChange: (nextDates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => { + if (Array.isArray(nextDates) && nextDates[0] && nextDates[1]) { + onChange?.([nextDates[0], nextDates[1]]); + return; + } + onChange?.(null); + }, + style: { flex: 1 }, + }; + + return ( + + + {['past', 'next'].includes(selectedType) && [ + { + inputProps?.onChange?.({ + ...(parsedValue as any), + type: selectedType, + number: nextNumber, + unit: (parsedValue as any)?.unit || 'day', + }); + }} + />, + `} readOnly style={{ width: '100%' }} /> @@ -600,6 +1111,8 @@ export const FieldAssignValueInput: React.FC = ({ return C; }, [flowCtx]); + const ConstantEditor = useDateVariableConstant ? DateVariableConstantEditor : ConstantValueEditor; + const metaTree = React.useMemo<() => Promise>(() => { return async () => { const base = (await flowCtx.getPropertyMetaTree?.()) || []; @@ -612,14 +1125,39 @@ export const FieldAssignValueInput: React.FC = ({ name: 'constant', type: 'string', paths: ['constant'], - render: ConstantValueEditor, + render: ConstantEditor, }, { title: tExpr('Null'), name: 'null', type: 'object', paths: ['null'], render: NullComponent }, { title: tExpr('RunJS'), name: 'runjs', type: 'object', paths: ['runjs'], render: RunJSComponent }, ...mergedBase, ]; }; - }, [flowCtx, ConstantValueEditor, NullComponent, RunJSComponent]); + }, [flowCtx, ConstantEditor, NullComponent, RunJSComponent]); + + const displayValue = React.useMemo(() => { + if (!useDateVariableConstant) { + return value; + } + + if (isCtxDateExpression(value)) { + const parsed = parseCtxDateExpression(value); + return typeof parsed === 'undefined' ? '' : parsed; + } + + return value; + }, [useDateVariableConstant, value]); + + const handleVariableInputChange = React.useCallback( + (nextValue: any) => { + if (!useDateVariableConstant) { + onChange(nextValue); + return; + } + + onChange(normalizeDateVariableOutput(nextValue, dateVariableComponentProps)); + }, + [dateVariableComponentProps, onChange, useDateVariableConstant], + ); if (!fieldPath) { // 不可用占位 @@ -628,22 +1166,24 @@ export const FieldAssignValueInput: React.FC = ({ return ( { const firstPath = meta?.paths?.[0]; - if (firstPath === 'constant') return ConstantValueEditor; + if (firstPath === 'constant') return ConstantEditor; if (firstPath === 'null') return NullComponent; if (firstPath === 'runjs') return RunJSComponent; return null; }, resolveValueFromPath: (item) => { const firstPath = item?.paths?.[0]; - if (firstPath === 'constant') return ''; + if (firstPath === 'constant') { + return useDateVariableConstant ? { type: 'today' } : ''; + } if (firstPath === 'null') return null; if (firstPath === 'runjs') return { code: '', version: 'v1' }; return undefined; @@ -651,6 +1191,9 @@ export const FieldAssignValueInput: React.FC = ({ resolvePathFromValue: (currentValue) => { if (currentValue === null) return ['null']; if (isRunJSValue(currentValue)) return ['runjs']; + if (useDateVariableConstant && isCtxDateExpression(currentValue)) { + return ['constant']; + } return isVariableExpression(currentValue) ? parseValueToPath(currentValue) : ['constant']; }, }} diff --git a/packages/core/client/src/flow/components/__tests__/FieldAssignExactDatePicker.test.tsx b/packages/core/client/src/flow/components/__tests__/FieldAssignExactDatePicker.test.tsx new file mode 100644 index 00000000000..8e5887793e9 --- /dev/null +++ b/packages/core/client/src/flow/components/__tests__/FieldAssignExactDatePicker.test.tsx @@ -0,0 +1,121 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { act, render } from '@nocobase/test/client'; +import { dayjs } from '@nocobase/utils/client'; +import { FieldAssignExactDatePicker } from '../FieldAssignExactDatePicker'; + +let capturedDatePickerProps: any; +let capturedRangePickerProps: any; +let capturedSelectProps: any; + +vi.mock('@nocobase/flow-engine', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useFlowContext: () => ({ + isMobileLayout: false, + model: { + translate: (value: string) => value, + }, + }), + }; +}); + +vi.mock('antd', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + DatePicker: Object.assign( + (props: any) => { + capturedDatePickerProps = props; + return
; + }, + { + RangePicker: (props: any) => { + capturedRangePickerProps = props; + return
; + }, + }, + ), + Select: (props: any) => { + capturedSelectProps = props; + return
; + }, + Space: { + Compact: ({ children }: { children: React.ReactNode }) =>
{children}
, + }, + }; +}); + +describe('FieldAssignExactDatePicker', () => { + beforeEach(() => { + capturedDatePickerProps = undefined; + capturedRangePickerProps = undefined; + capturedSelectProps = undefined; + }); + + it('keeps single ISO date semantic under weekday-only format', () => { + render( + , + ); + + expect(dayjs.isDayjs(capturedDatePickerProps?.value)).toBe(true); + expect(capturedDatePickerProps?.value?.year()).toBe(2037); + expect(capturedDatePickerProps?.value?.date()).toBe(6); + }); + + it('emits dayjs value on single picker change', () => { + const onChange = vi.fn(); + render(); + + act(() => { + capturedDatePickerProps?.onChange?.(dayjs('2037-03-07T01:02:03.000Z')); + }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(dayjs.isDayjs(onChange.mock.calls[0][0])).toBe(true); + }); + + it('keeps range ISO date semantic under weekday-only format', () => { + render( + , + ); + + expect(Array.isArray(capturedRangePickerProps?.value)).toBe(true); + expect(dayjs.isDayjs(capturedRangePickerProps?.value?.[0])).toBe(true); + expect(dayjs.isDayjs(capturedRangePickerProps?.value?.[1])).toBe(true); + expect(capturedRangePickerProps?.value?.[0]?.year()).toBe(2037); + expect(capturedRangePickerProps?.value?.[1]?.year()).toBe(2037); + expect(capturedRangePickerProps?.value?.[0]?.date()).toBe(6); + expect(capturedRangePickerProps?.value?.[1]?.date()).toBe(7); + }); + + it('keeps semantic date when switching picker granularity', () => { + const onChange = vi.fn(); + render(); + + act(() => { + capturedSelectProps?.onChange?.('month'); + }); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(dayjs.isDayjs(onChange.mock.calls[0][0])).toBe(true); + expect(onChange.mock.calls[0][0].year()).toBe(2037); + expect(onChange.mock.calls[0][0].date()).toBe(6); + }); +}); diff --git a/packages/core/client/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx b/packages/core/client/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx index 29bf9681060..0b22138f3ff 100644 --- a/packages/core/client/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +++ b/packages/core/client/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx @@ -22,6 +22,7 @@ const { mockFieldAssignValueInput } = vi.hoisted(() => ({ data-extra={Array.isArray(props?.extraMetaTree) ? 'yes' : 'no'} data-assoc-label={String(props?.associationFieldNamesOverride?.label || '')} data-assoc-value={String(props?.associationFieldNamesOverride?.value || '')} + data-date-constant={props?.enableDateVariableAsConstant ? 'yes' : 'no'} /> )), })); @@ -729,4 +730,29 @@ describe('FieldAssignRulesEditor', () => { const merged = mergeItemMetaTreeForAssignValue(base, extra); expect(merged).toEqual(base); }); + + it('passes enableDateVariableAsConstant to value input', () => { + const value: FieldAssignRuleItem[] = [ + { + key: '1', + enable: true, + targetPath: 'createdAt', + mode: 'assign', + }, + ]; + + const { getByTestId } = render( + wrap( + , + ), + ); + + expect(getByTestId('mock-value-input').getAttribute('data-date-constant')).toBe('yes'); + }); }); diff --git a/packages/core/client/src/flow/components/__tests__/FieldAssignValueInput.exactDate.test.tsx b/packages/core/client/src/flow/components/__tests__/FieldAssignValueInput.exactDate.test.tsx new file mode 100644 index 00000000000..93bae84ac6f --- /dev/null +++ b/packages/core/client/src/flow/components/__tests__/FieldAssignValueInput.exactDate.test.tsx @@ -0,0 +1,180 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { describe, expect, it } from 'vitest'; +import { parseCtxDateExpression } from '@nocobase/flow-engine'; +import { dayjs } from '@nocobase/utils/client'; +import { + normalizeDateVariableExactValue, + normalizeDateVariableOutput, + toExactPickerDisplayValue, + type DateVariableExactNormalizeMode, +} from '../FieldAssignValueInput'; + +function normalizeExact(rawValue: any, mode: DateVariableExactNormalizeMode, format: string, showTime = false) { + return normalizeDateVariableExactValue(rawValue, { + exactNormalizeMode: mode, + format, + showTime, + }); +} + +describe('FieldAssignValueInput date exact normalization', () => { + it('normalizes date exact value from MMMM Do YYYY to YYYY-MM-DD', () => { + const normalized = normalizeExact('February 12th 2026', 'date', 'MMMM Do YYYY'); + expect(normalized).toBe('2026-02-12'); + + const serialized = normalizeDateVariableOutput(normalized, { + picker: 'date', + showTime: false, + timeFormat: 'HH:mm:ss', + format: 'MMMM Do YYYY', + exactNormalizeMode: 'date', + }); + + expect(typeof serialized).toBe('string'); + expect(parseCtxDateExpression(serialized)).toBe('2026-02-12'); + }); + + it('normalizes datetimeNoTz exact value to stable datetime string', () => { + const normalized = normalizeExact('12/02/2026 23:45:01', 'datetimeNoTz', 'DD/MM/YYYY HH:mm:ss', true); + expect(normalized).toBe('2026-02-12 23:45:01'); + + const serialized = normalizeDateVariableOutput(normalized, { + picker: 'date', + showTime: true, + timeFormat: 'HH:mm:ss', + format: 'DD/MM/YYYY HH:mm:ss', + exactNormalizeMode: 'datetimeNoTz', + }); + + expect(parseCtxDateExpression(serialized)).toBe('2026-02-12 23:45:01'); + }); + + it('normalizes tz-aware datetime exact value to ISO string', () => { + const normalized = normalizeExact('12/02/2026 23:45:01', 'iso', 'DD/MM/YYYY HH:mm:ss', true); + expect(typeof normalized).toBe('string'); + expect(normalized).toContain('T'); + + const serialized = normalizeDateVariableOutput(normalized, { + picker: 'date', + showTime: true, + timeFormat: 'HH:mm:ss', + format: 'DD/MM/YYYY HH:mm:ss', + exactNormalizeMode: 'iso', + }); + + const parsed = parseCtxDateExpression(serialized); + expect(typeof parsed).toBe('string'); + expect(parsed).toContain('T'); + }); + + it('keeps preset/relative objects unchanged', () => { + const preset = { type: 'today' }; + const relative = { type: 'next', unit: 'day', number: 2 }; + + const serializedPreset = normalizeDateVariableOutput(preset, { + picker: 'date', + showTime: false, + timeFormat: 'HH:mm:ss', + format: 'YYYY-MM-DD', + exactNormalizeMode: 'date', + }); + const serializedRelative = normalizeDateVariableOutput(relative, { + picker: 'date', + showTime: false, + timeFormat: 'HH:mm:ss', + format: 'YYYY-MM-DD', + exactNormalizeMode: 'date', + }); + + expect(parseCtxDateExpression(serializedPreset)).toEqual(preset); + expect(parseCtxDateExpression(serializedRelative)).toEqual(relative); + }); + + it('falls back to original value when parsing fails', () => { + const raw = 'Not a valid date'; + const normalized = normalizeExact(raw, 'iso', 'MMMM Do YYYY', true); + expect(normalized).toBe(raw); + + const serialized = normalizeDateVariableOutput(raw, { + picker: 'date', + showTime: true, + timeFormat: 'HH:mm:ss', + format: 'MMMM Do YYYY', + exactNormalizeMode: 'iso', + }); + + expect(parseCtxDateExpression(serialized)).toBe(raw); + }); + + it('supports weekday-only display by normalizing dayjs exact value', () => { + const normalized = normalizeExact(dayjs('2026-02-13', 'YYYY-MM-DD'), 'date', 'dddd'); + expect(normalized).toBe('2026-02-13'); + + const serialized = normalizeDateVariableOutput(dayjs('2026-02-13', 'YYYY-MM-DD'), { + picker: 'date', + showTime: false, + timeFormat: 'HH:mm:ss', + format: 'dddd', + exactNormalizeMode: 'date', + }); + + expect(parseCtxDateExpression(serialized)).toBe('2026-02-13'); + }); + + it('keeps exact picker display date with weekday-only format', () => { + const display = toExactPickerDisplayValue('2026-02-13 18:14:15', { + format: 'dddd HH:mm:ss', + isRange: false, + }); + + expect(dayjs.isDayjs(display)).toBe(true); + expect((display as any).format('YYYY-MM-DD HH:mm:ss')).toBe('2026-02-13 18:14:15'); + }); + + it('keeps exact picker display date for ISO input with weekday-only format', () => { + const display = toExactPickerDisplayValue('2037-03-06T12:34:56.000Z', { + format: 'dddd HH:mm:ss', + isRange: false, + }); + + expect(dayjs.isDayjs(display)).toBe(true); + expect((display as any).year()).toBe(2037); + expect((display as any).date()).toBe(6); + }); + + it('keeps exact range picker display dates with weekday-only format', () => { + const display = toExactPickerDisplayValue(['2026-02-13 18:14:15', '2026-02-14 01:02:03'], { + format: 'dddd HH:mm:ss', + isRange: true, + }); + + expect(Array.isArray(display)).toBe(true); + expect(dayjs.isDayjs((display as any)?.[0])).toBe(true); + expect(dayjs.isDayjs((display as any)?.[1])).toBe(true); + expect((display as any)[0].format('YYYY-MM-DD HH:mm:ss')).toBe('2026-02-13 18:14:15'); + expect((display as any)[1].format('YYYY-MM-DD HH:mm:ss')).toBe('2026-02-14 01:02:03'); + }); + + it('keeps exact range picker display dates for ISO input with weekday-only format', () => { + const display = toExactPickerDisplayValue(['2037-03-06T12:34:56.000Z', '2037-03-07T01:02:03.000Z'], { + format: 'dddd HH:mm:ss', + isRange: true, + }); + + expect(Array.isArray(display)).toBe(true); + expect(dayjs.isDayjs((display as any)?.[0])).toBe(true); + expect(dayjs.isDayjs((display as any)?.[1])).toBe(true); + expect((display as any)[0].year()).toBe(2037); + expect((display as any)[1].year()).toBe(2037); + expect((display as any)[0].date()).toBe(6); + expect((display as any)[1].date()).toBe(7); + }); +}); diff --git a/packages/core/client/src/flow/models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent.tsx b/packages/core/client/src/flow/models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent.tsx index ac5dc979db7..13cc49957ce 100644 --- a/packages/core/client/src/flow/models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent.tsx +++ b/packages/core/client/src/flow/models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent.tsx @@ -15,29 +15,37 @@ import { FilterRangePicker } from './FilterRangePicker'; import { FilterDatePicker } from './FilterDatePicker'; import { useFlowEngine } from '@nocobase/flow-engine'; -const getOptions = (t) => [ - { - value: 'exact', - label: t('Exact day'), - }, - { value: 'past', label: t('Past') }, - { value: 'next', label: t('Next') }, - { value: 'today', label: t('Today') }, - { value: 'yesterday', label: t('Yesterday') }, - { value: 'tomorrow', label: t('Tomorrow') }, - { value: 'thisWeek', label: t('This Week') }, - { value: 'lastWeek', label: t('Last Week') }, - { value: 'nextWeek', label: t('Next Week') }, - { value: 'thisMonth', label: t('This Month') }, - { value: 'lastMonth', label: t('Last Month') }, - { value: 'nextMonth', label: t('Next Month') }, - { value: 'thisQuarter', label: t('This Quarter') }, - { value: 'lastQuarter', label: t('Last Quarter') }, - { value: 'nextQuarter', label: t('Next Quarter') }, - { value: 'thisYear', label: t('This Year') }, - { value: 'lastYear', label: t('Last Year') }, - { value: 'nextYear', label: t('Next Year') }, -]; +const getOptions = (t, includeNow = false) => { + const options = [ + { + value: 'exact', + label: t('Exact day'), + }, + { value: 'past', label: t('Past') }, + { value: 'next', label: t('Next') }, + { value: 'today', label: t('Today') }, + { value: 'yesterday', label: t('Yesterday') }, + { value: 'tomorrow', label: t('Tomorrow') }, + { value: 'thisWeek', label: t('This Week') }, + { value: 'lastWeek', label: t('Last Week') }, + { value: 'nextWeek', label: t('Next Week') }, + { value: 'thisMonth', label: t('This Month') }, + { value: 'lastMonth', label: t('Last Month') }, + { value: 'nextMonth', label: t('Next Month') }, + { value: 'thisQuarter', label: t('This Quarter') }, + { value: 'lastQuarter', label: t('Last Quarter') }, + { value: 'nextQuarter', label: t('Next Quarter') }, + { value: 'thisYear', label: t('This Year') }, + { value: 'lastYear', label: t('Last Year') }, + { value: 'nextYear', label: t('Next Year') }, + ]; + + if (includeNow) { + options.splice(3, 0, { value: 'now', label: t('Now') }); + } + + return options; +}; type SmartDatePickerProps = { isRange?: boolean; @@ -63,12 +71,12 @@ const SmartDatePicker: React.FC = (props) => { }; export const DateFilterDynamicComponent = (props) => { - const { value, onChange } = props; + const { value, onChange, includeNow = false, ...restProps } = props; const flowEngine = useFlowEngine(); const t = flowEngine.translate.bind(flowEngine); const { token } = theme.useToken(); const [open, setOpen] = useState(false); - const options = useMemo(() => getOptions(t), [t]); + const options = useMemo(() => getOptions(t, includeNow), [t, includeNow]); const handleSelect = (val) => { setOpen(false); if (val === 'exact') { @@ -119,12 +127,12 @@ export const DateFilterDynamicComponent = (props) => { ); }; return ( - +