Fix: Metadata supports precise time selection (#12785)

### What problem does this PR solve?

Fix: Metadata supports precise time selection

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2026-01-23 09:33:34 +08:00
committed by GitHub
parent 7c9b6e032b
commit e9453a3971
12 changed files with 796 additions and 82 deletions

View File

@ -15,7 +15,7 @@ import { useForm } from 'react-hook-form';
import { z, ZodArray, ZodString } from 'zod'; import { z, ZodArray, ZodString } from 'zod';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input, SearchInput } from '@/components/ui/input';
import { Form, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { Form, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { t } from 'i18next'; import { t } from 'i18next';
@ -249,23 +249,23 @@ function CheckboxFormMultiple({
return ( return (
<FormItem className="space-y-4" key={x.field}> <FormItem className="space-y-4" key={x.field}>
<div> <div>
<div className="flex justify-between items-center mb-2"> <div className="flex flex-col items-start justify-between mb-2">
<FormLabel className="text-text-primary text-sm"> <FormLabel className="text-text-primary text-sm">
{x.label} {x.label}
</FormLabel> </FormLabel>
{x.canSearch && ( {x.canSearch && (
<Input <SearchInput
placeholder={t('common.search') + '...'} placeholder={t('common.search') + '...'}
value={searchTerms[x.field] || ''} value={searchTerms[x.field] || ''}
onChange={(e) => onChange={(e) =>
handleSearchChange(x.field, e.target.value) handleSearchChange(x.field, e.target.value)
} }
className="h-8 w-32 ml-2" className="h-8 w-full"
/> />
)} )}
</div> </div>
</div> </div>
<div className="space-y-4 max-h-[300px] overflow-auto scrollbar-thin"> <div className="space-y-4 max-h-[300px] overflow-auto scrollbar-auto">
{!!filteredItem.list?.length && {!!filteredItem.list?.length &&
filteredItem.list.map((item) => { filteredItem.list.map((item) => {
return ( return (

View File

@ -22,7 +22,7 @@ export const MetadataFilterSchema = {
z.object({ z.object({
key: z.string(), key: z.string(),
op: z.string(), op: z.string(),
value: z.string(), value: z.union([z.string(), z.array(z.string())]),
}), }),
) )
.optional(), .optional(),

View File

@ -73,6 +73,7 @@ export function MetadataFilterConditions({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useFormContext(); const form = useFormContext();
const op = useWatch({ name: `${name}.${index}.op` });
const key = useWatch({ name: fieldName }); const key = useWatch({ name: fieldName });
const valueOptions = useMemo(() => { const valueOptions = useMemo(() => {
if (!key || !metadata?.data || !metadata?.data[key]) return []; if (!key || !metadata?.data || !metadata?.data[key]) return [];
@ -85,6 +86,23 @@ export function MetadataFilterConditions({
return []; return [];
}, [key]); }, [key]);
const handleChangeOp = useCallback(
(value: string) => {
form.setValue(`${name}.${index}.op`, value);
if (
!['in', 'not in'].includes(value) &&
!['in', 'not in'].includes(op)
) {
return;
}
if (value === 'in' || value === 'not in') {
form.setValue(`${name}.${index}.value`, []);
} else {
form.setValue(`${name}.${index}.value`, '');
}
},
[form, index, op],
);
return ( return (
<div className="flex gap-1"> <div className="flex gap-1">
<Card <Card
@ -118,6 +136,9 @@ export function MetadataFilterConditions({
<FormControl> <FormControl>
<RAGFlowSelect <RAGFlowSelect
{...field} {...field}
onChange={(value) => {
handleChangeOp(value);
}}
options={switchOperatorOptions} options={switchOperatorOptions}
onlyShowSelectedIcon onlyShowSelectedIcon
triggerClassName="w-30 bg-transparent border-none" triggerClassName="w-30 bg-transparent border-none"
@ -133,27 +154,30 @@ export function MetadataFilterConditions({
<FormField <FormField
control={form.control} control={form.control}
name={`${name}.${index}.value`} name={`${name}.${index}.value`}
render={({ field: valueField }) => ( render={({ field: valueField }) => {
<FormItem> return (
<FormControl> <FormItem>
{canReference ? ( <FormControl>
<PromptEditor {canReference ? (
{...valueField} <PromptEditor
multiLine={false} {...valueField}
showToolbar={false} multiLine={false}
></PromptEditor> showToolbar={false}
) : ( ></PromptEditor>
<InputSelect ) : (
placeholder={t('common.pleaseInput')} <InputSelect
{...valueField} placeholder={t('common.pleaseInput')}
options={valueOptions} {...valueField}
className="w-full" options={valueOptions}
/> className="w-full"
)} multi={op === 'in' || op === 'not in'}
</FormControl> />
<FormMessage /> )}
</FormItem> </FormControl>
)} <FormMessage />
</FormItem>
);
}}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -92,4 +92,4 @@ function Calendar({
); );
} }
export { Calendar, DateRange }; export { Calendar, type DateRange };

View File

@ -10,7 +10,8 @@ import { Locale } from 'date-fns';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Calendar as CalendarIcon } from 'lucide-react'; import { Calendar as CalendarIcon } from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { TimePicker } from './time-picker';
// import TimePicker from 'react-time-picker';
interface DateInputProps extends Omit< interface DateInputProps extends Omit<
React.InputHTMLAttributes<HTMLInputElement>, React.InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange' 'value' | 'onChange'
@ -45,8 +46,32 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const handleDateSelect = (date: Date | undefined) => { const handleDateSelect = (date: Date | undefined) => {
if (value) {
const valueDate = dayjs(value);
date?.setHours(valueDate.hour());
date?.setMinutes(valueDate.minute());
date?.setSeconds(valueDate.second());
}
onChange?.(date); onChange?.(date);
setOpen(false); // setOpen(false);
};
const handleTimeSelect = (date: Date | undefined) => {
const valueDate = dayjs(value);
if (value) {
date?.setFullYear(valueDate.year());
date?.setMonth(valueDate.month());
date?.setDate(valueDate.date());
}
if (date) {
onChange?.(date);
} else {
valueDate?.hour(0);
valueDate?.minute(0);
valueDate?.second(0);
onChange?.(valueDate.toDate());
}
// setOpen(false);
}; };
// Determine display format based on the type of date picker // Determine display format based on the type of date picker
@ -90,12 +115,17 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
mode="single" mode="single"
selected={value} selected={value}
onSelect={handleDateSelect} onSelect={handleDateSelect}
initialFocus
{...(showTimeSelect && {
showTimeInput,
timeInputLabel,
})}
/> />
{showTimeSelect && (
<TimePicker
value={value}
onChange={(value: Date | undefined) => {
handleTimeSelect(value);
}}
showNow
/>
// <TimePicker onChange={onChange} value={value} />
)}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>

View File

@ -308,12 +308,6 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
} else { } else {
isAlreadySelected = (normalizedValue as string[]).includes(inputValue); isAlreadySelected = (normalizedValue as string[]).includes(inputValue);
} }
console.log(
'showInputAsOption',
hasLabelMatch,
isAlreadySelected,
inputValue.toString().trim(),
);
return ( return (
!hasLabelMatch && !hasLabelMatch &&
!isAlreadySelected && !isAlreadySelected &&
@ -350,9 +344,9 @@ const InputSelect = React.forwardRef<HTMLInputElement, InputSelectProps>(
return ( return (
<div <div
key={`${tagValue}-${index}`} key={`${tagValue}-${index}`}
className="flex items-center bg-bg-card text-text-primary rounded px-2 py-1 text-xs mr-1 mb-1 border border-border-card" className="flex items-center bg-bg-card text-text-primary rounded px-2 py-1 text-xs mr-1 mb-1 border border-border-card truncate"
> >
{option.label} <div className="flex-1 truncate">{option.label}</div>
<button <button
type="button" type="button"
className="ml-1 text-text-secondary hover:text-text-primary focus:outline-none" className="ml-1 text-text-secondary hover:text-text-primary focus:outline-none"

View File

@ -0,0 +1,641 @@
import { cn } from '@/lib/utils';
import { Clock } from 'lucide-react';
import * as React from 'react';
import { forwardRef } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
interface DisabledTimes {
disabledHours?: () => number[];
disabledMinutes?: (hour: number) => number[];
disabledSeconds?: (hour: number, minute: number) => number[];
}
interface TimePickerProps extends Omit<
React.HTMLAttributes<HTMLDivElement>,
'value' | 'onChange' | 'defaultValue'
> {
value?: Date;
onChange?: (date: Date | undefined) => void;
format?: string; // Time display format
disabled?: boolean;
placeholder?: string;
className?: string;
hourStep?: number;
minuteStep?: number;
secondStep?: number;
allowClear?: boolean; // Whether to show clear button
autoFocus?: boolean; // Auto focus
bordered?: boolean; // Whether to show border
disabledTime?: () => DisabledTimes; // Disabled time options
hideDisabledOptions?: boolean; // Hide disabled options
inputReadOnly?: boolean; // Set input as readonly
use12Hours?: boolean; // Use 12-hour format
size?: 'large' | 'middle' | 'small'; // Input size
status?: 'error' | 'warning'; // Validation status
onOpenChange?: (open: boolean) => void; // Callback when panel opens/closes
open?: boolean; // Whether panel is open
popupClassName?: string; // Popup class name
popupStyle?: React.CSSProperties; // Popup style object
renderExtraFooter?: () => React.ReactNode; // Custom content at bottom of picker
showNow?: boolean; // Whether panel shows "Now" button
defaultValue?: Date; // Default time
cellRender?: (
current: number,
info: {
originNode: React.ReactNode;
today: Date;
range?: 'start' | 'end';
subType: 'hour' | 'minute' | 'second' | 'meridiem';
},
) => React.ReactNode; // Customize cell content
suffixIcon?: React.ReactNode; // Custom suffix icon
clearIcon?: React.ReactNode; // Custom clear icon
addon?: () => React.ReactNode; // Extra popup content
minuteOptions?: number[]; // Minute options
secondOptions?: number[]; // Second options
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight'; // Placement of picker popup
}
// Scroll picker component
interface ScrollPickerProps {
options: string[];
value: string;
onChange: (value: string) => void;
disabledOptions?: number[];
className?: string;
}
const ScrollPicker = React.memo<ScrollPickerProps>(
({ options, value, onChange, disabledOptions = [], className }) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const selectedItemRef = React.useRef<HTMLDivElement>(null);
// Scroll to the selected item and make it top
React.useEffect(() => {
if (containerRef.current && selectedItemRef.current) {
const container = containerRef.current;
const selectedItem = selectedItemRef.current;
const itemHeight = selectedItem.clientHeight;
const itemIndex = options.indexOf(value);
if (itemIndex !== -1) {
// Calculate the scroll distance to make the selected item top
const scrollTop = itemIndex * itemHeight;
container.scrollTop = scrollTop;
}
}
}, [value, options]);
return (
<div className={cn('relative h-48 overflow-hidden', className)}>
<div
ref={containerRef}
// onWheel={handleScroll}
className="h-full overflow-y-auto scrollbar-none hover:scrollbar-auto"
>
{options.map((option, index) => {
const isDisabled = disabledOptions.includes(index);
const isSelected = option === value;
return (
<div
key={`${option}-${index}`}
ref={isSelected ? selectedItemRef : null}
onClick={() => {
if (!isDisabled) onChange(option);
}}
className={cn(
'h-8 flex items-center justify-center cursor-pointer text-sm',
'transition-colors duration-150',
{
'text-text-primary bg-bg-card': isSelected,
'text-text-disabled hover:bg-bg-card':
!isSelected && !isDisabled,
'text-text-disabled cursor-not-allowed opacity-50':
isDisabled,
},
)}
>
{option}
</div>
);
})}
<div className="h-[calc(100%-32px)]"></div>
</div>
{/* Add a transparent top bar for visual distinction */}
{/* <div className="absolute top-0 left-0 right-0 h-8 border-b border-gray-600 pointer-events-none"></div> */}
</div>
);
},
);
ScrollPicker.displayName = 'ScrollPicker';
const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
(
{
value,
onChange,
format = 'HH:mm:ss',
disabled = false,
placeholder = 'Select time',
className,
hourStep = 1,
minuteStep = 1,
secondStep = 1,
allowClear = false,
autoFocus = false,
bordered = true,
disabledTime,
hideDisabledOptions = false,
inputReadOnly = false,
use12Hours = false,
size = 'middle',
status,
onOpenChange,
open,
popupClassName,
popupStyle,
renderExtraFooter,
showNow = false,
defaultValue,
placement = 'bottomLeft',
suffixIcon,
clearIcon,
addon,
minuteOptions,
secondOptions,
...props
},
ref,
) => {
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState('');
const internalOpen = open !== undefined ? open : isOpen;
// Initialize default value
React.useEffect(() => {
if (!value && defaultValue) {
onChange?.(defaultValue);
}
}, [value, defaultValue, onChange]);
// Update input field value
React.useEffect(() => {
if (value) {
const hours = value.getHours();
const minutes = value.getMinutes();
const seconds = value.getSeconds();
// Format output according to format
let formatted = '';
if (use12Hours) {
let displayHour = hours % 12;
if (displayHour === 0) displayHour = 12;
formatted = `${String(displayHour).padStart(2, '0')}`;
if (format.toLowerCase().includes('mm')) {
formatted += `:${String(minutes).padStart(2, '0')}`;
}
if (format.toLowerCase().includes('ss')) {
formatted += `:${String(seconds).padStart(2, '0')}`;
}
if (format.toLowerCase().includes('a')) {
formatted += ` ${hours >= 12 ? 'PM' : 'AM'}`;
} else if (format.toLowerCase().includes('A')) {
formatted += ` ${hours >= 12 ? 'pm' : 'am'}`;
}
} else {
formatted = String(hours).padStart(2, '0');
if (format.toLowerCase().includes('mm')) {
formatted += `:${String(minutes).padStart(2, '0')}`;
}
if (format.toLowerCase().includes('ss')) {
formatted += `:${String(seconds).padStart(2, '0')}`;
}
}
setInputValue(formatted);
} else {
setInputValue('');
}
}, [value, format, use12Hours]);
const handleOpenChange = (newOpen: boolean) => {
setIsOpen(newOpen);
onOpenChange?.(newOpen);
};
// Handle input field changes
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setInputValue(newValue);
// Try to parse the input time string
if (newValue) {
const parsedDate = parseTimeInput(newValue, format, use12Hours);
if (parsedDate) {
onChange?.(parsedDate);
}
} else {
onChange?.(undefined);
}
};
// Parse time input string
const parseTimeInput = (
input: string,
fmt: string,
use12H: boolean,
): Date | undefined => {
// Remove spaces and normalize input
const normalizedInput = input.trim();
// Define time regular expression pattern
let regex: RegExp;
// let hasAmPm = false;
if (use12H) {
if (fmt.toLowerCase().includes('ss')) {
regex = /^(\d{1,2}):(\d{1,2}):(\d{1,2})\s*(AM|PM|am|pm)?$/;
// hasAmPm = true;
} else {
regex = /^(\d{1,2}):(\d{1,2})\s*(AM|PM|am|pm)?$/;
// hasAmPm = true;
}
} else {
if (fmt.toLowerCase().includes('ss')) {
regex = /^(\d{1,2}):(\d{1,2}):(\d{1,2})$/;
} else {
regex = /^(\d{1,2}):(\d{1,2})$/;
}
}
const match = normalizedInput.match(regex);
if (!match) return undefined;
const [hourStr, minuteStr, secondStr, ampmStr] = match;
let hour = parseInt(hourStr, 10);
const minute = parseInt(minuteStr, 10);
const second = secondStr ? parseInt(secondStr, 10) : 0;
// Validate time range
if (minute >= 60 || second >= 60) return undefined;
if (use12H && ampmStr) {
const isPM = ampmStr.toLowerCase() === 'pm';
if (hour < 1 || hour > 12) return undefined;
if (isPM && hour !== 12) {
hour += 12;
} else if (!isPM && hour === 12) {
hour = 0;
}
} else if (!use12H) {
if (hour > 23) return undefined;
}
const newDate = value ? new Date(value) : new Date();
newDate.setHours(hour, minute, second, 0);
return newDate;
};
// Determine whether to show seconds based on format
const showSeconds = format.toLowerCase().includes('ss');
// Handle time changes
const handleHourChange = (hourStr: string) => {
let hour = parseInt(hourStr, 10);
// Convert to 24-hour format if using 12-hour format
if (use12Hours) {
const currentHour = value?.getHours() || 0;
const isAM = currentHour < 12;
if (hour === 12) {
hour = isAM ? 0 : 12;
} else {
hour = isAM
? parseInt(hour.toString(), 10)
: parseInt(hour.toString(), 10) + 12;
}
}
const newDate = value ? new Date(value) : new Date();
newDate.setHours(hour);
onChange?.(newDate);
};
const handleMinuteChange = (minuteStr: string) => {
// if (!value) return;
const minute = parseInt(minuteStr, 10);
const newDate = value ? new Date(value) : new Date();
newDate.setMinutes(minute);
onChange?.(newDate);
};
const handleSecondChange = (secondStr: string) => {
// if (!value) return;
const second = parseInt(secondStr, 10);
const newDate = value ? new Date(value) : new Date();
newDate.setSeconds(second);
onChange?.(newDate);
};
const handleAmPmChange = (ampm: 'AM' | 'PM') => {
if (!value || !use12Hours) return;
const newDate = new Date(value);
const currentHour = newDate.getHours();
if (ampm === 'AM' && currentHour >= 12) {
newDate.setHours(currentHour - 12);
} else if (ampm === 'PM' && currentHour < 12) {
newDate.setHours(currentHour + 12);
}
onChange?.(newDate);
};
const getDisabledTimes = React.useCallback(() => {
if (!disabledTime)
return {
disabledHours: [] as number[],
disabledMinutes: () => [] as number[],
disabledSeconds: () => [] as number[],
};
const disabled = disabledTime();
return {
disabledHours: disabled.disabledHours?.() || [],
disabledMinutes: disabled.disabledMinutes
? (hour: number) => disabled.disabledMinutes!(hour)
: () => [] as number[],
disabledSeconds: disabled.disabledSeconds
? (hour: number, minute: number) =>
disabled.disabledSeconds!(hour, minute)
: () => [] as number[],
};
}, [disabledTime]);
// Generate time options
const generateTimeOptions = (
step: number,
max: number,
disabledOptions: number[] = [],
customOptions?: number[],
) => {
let options: number[];
if (customOptions && customOptions.length > 0) {
options = customOptions;
} else {
options = [];
for (let i = 0; i <= max; i += step) {
options.push(i);
}
}
// Filter out disabled options
if (hideDisabledOptions) {
options = options.filter((option) => !disabledOptions.includes(option));
}
return options.map((num) => String(num).padStart(2, '0'));
};
const disabledTimes = getDisabledTimes();
const hours = use12Hours
? generateTimeOptions(
hourStep,
11,
disabledTimes.disabledHours.filter((h) => h <= 11),
)
: generateTimeOptions(hourStep, 23, disabledTimes.disabledHours);
const minutes = generateTimeOptions(
minuteStep,
59,
value ? disabledTimes.disabledMinutes(value.getHours()) : [],
minuteOptions,
);
const seconds = generateTimeOptions(
secondStep,
59,
value
? disabledTimes.disabledSeconds(value.getHours(), value.getMinutes())
: [],
secondOptions,
);
// Get current time value
const hourValue = React.useMemo(() => {
if (!value) return '00';
let hour = value.getHours();
if (use12Hours) {
hour = hour % 12;
if (hour === 0) hour = 12;
}
return hour.toString().padStart(2, '0');
}, [value, use12Hours]);
const minuteValue = React.useMemo(() => {
if (!value) return '00';
return value.getMinutes().toString().padStart(2, '0');
}, [value]);
const secondValue = React.useMemo(() => {
if (!value) return '00';
return value.getSeconds().toString().padStart(2, '0');
}, [value]);
const ampmValue = React.useMemo(() => {
if (!value || !use12Hours) return 'AM';
return value.getHours() >= 12 ? 'PM' : 'AM';
}, [value, use12Hours]);
// Handle clear operation
const handleClear = () => {
onChange?.(undefined);
setInputValue('');
handleOpenChange(false);
};
// Handle Now button
const handleSetNow = () => {
onChange?.(new Date());
handleOpenChange(false);
};
return (
<div ref={ref} className={cn('w-full', className)} {...props}>
<Popover
open={internalOpen}
onOpenChange={handleOpenChange}
modal={false} // Use non-modal dialog box, consistent with Ant Design behavior
>
<div className="relative">
<Input
type="text"
value={inputValue}
onChange={handleInputChange}
placeholder={placeholder}
disabled={disabled}
readOnly={inputReadOnly}
className={cn(
'pl-3 pr-8 py-2 font-normal',
size === 'large' && 'h-10 text-base',
size === 'small' && 'h-8 text-sm',
status === 'error' && 'border-red-500',
status === 'warning' && '!border-yellow-500',
!bordered && 'border-transparent',
'cursor-pointer',
)}
autoFocus={autoFocus}
/>
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center">
{allowClear && value && inputValue && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleClear();
}}
className="mr-2 text-muted-foreground hover:text-foreground"
>
{clearIcon || '✕'}
</button>
)}
<PopoverTrigger asChild>
<div className="cursor-pointer">
{suffixIcon || (
<Clock size={16} className="text-muted-foreground" />
)}
</div>
</PopoverTrigger>
</div>
</div>
<PopoverContent
className={cn('w-auto p-3', popupClassName)}
align={
(placement.replace(/top|bottom/, '').toLowerCase() as
| 'start'
| 'center'
| 'end') || 'start'
}
side={placement.startsWith('top') ? 'top' : 'bottom'}
style={popupStyle}
avoidCollisions={true}
>
<div className="flex items-center space-x-0">
{/* <Clock className="text-muted-foreground" size={16} /> */}
<div className="flex space-x-2">
{/* Hour selection */}
<div className="flex flex-col">
<ScrollPicker
options={hours}
value={hourValue}
onChange={handleHourChange}
className="w-14"
/>
</div>
{format.toLowerCase().includes('mm') && (
<>
{/* Minute selection */}
<div className="flex flex-col">
<ScrollPicker
options={minutes}
value={minuteValue}
onChange={handleMinuteChange}
className="w-14"
/>
</div>
</>
)}
{showSeconds && (
<>
{/* Second selection */}
<div className="flex flex-col">
<ScrollPicker
options={seconds}
value={secondValue}
onChange={handleSecondChange}
className="w-14"
/>
</div>
</>
)}
{use12Hours && (
<div className="flex flex-col ml-1">
<ScrollPicker
options={['AM', 'PM']}
value={ampmValue}
onChange={(val) => handleAmPmChange(val as 'AM' | 'PM')}
className="w-12"
/>
</div>
)}
</div>
</div>
{/* Extra footer content */}
{renderExtraFooter && (
<div className="mt-2 pt-2 border-t border-border-button">
{renderExtraFooter()}
</div>
)}
{/* Now button */}
{showNow && (
<div className="mt-2 pt-2 border-t border-border-button">
<Button
variant="outline"
size="sm"
onClick={handleSetNow}
className="w-full text-xs"
>
Now
</Button>
</div>
)}
{/* Addon content */}
{addon && (
<div className="mt-2 pt-2 border-t border-gray-200">
{addon()}
</div>
)}
</PopoverContent>
</Popover>
</div>
);
},
);
TimePicker.displayName = 'TimePicker';
export { TimePicker, type TimePickerProps };

View File

@ -11,7 +11,11 @@ import { RowSelectionState } from '@tanstack/react-table';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { MetadataType, metadataValueTypeEnum } from '../constant'; import {
DEFAULT_VALUE_TYPE,
MetadataType,
metadataValueTypeEnum,
} from '../constant';
import { import {
IBuiltInMetadataItem, IBuiltInMetadataItem,
IMetaDataReturnJSONSettings, IMetaDataReturnJSONSettings,
@ -182,7 +186,7 @@ export const useMetadataOperations = () => {
key, key,
match: originalValue, match: originalValue,
value: newValuesRes, value: newValuesRes,
type, valueType: type || DEFAULT_VALUE_TYPE,
}; };
return { return {
...prev, ...prev,
@ -193,7 +197,7 @@ export const useMetadataOperations = () => {
...prev, ...prev,
updates: [ updates: [
...prev.updates, ...prev.updates,
{ key, match: originalValue, value: newValuesRes, type }, { key, match: originalValue, value: newValuesRes, valueType: type },
], ],
}; };
}); });

View File

@ -107,7 +107,7 @@ interface UpdateOperation {
key: string; key: string;
match: string; match: string;
value: string | string[]; value: string | string[];
type?: MetadataValueType; valueType?: MetadataValueType;
} }
export interface MetadataOperations { export interface MetadataOperations {

View File

@ -1,6 +1,8 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { DateInput } from '@/components/ui/input-date';
import { formatDate } from '@/utils/date';
import { ColumnDef, Row, Table } from '@tanstack/react-table'; import { ColumnDef, Row, Table } from '@tanstack/react-table';
import { import {
ListChevronsDownUp, ListChevronsDownUp,
@ -14,6 +16,7 @@ import {
getMetadataValueTypeLabel, getMetadataValueTypeLabel,
MetadataDeleteMap, MetadataDeleteMap,
MetadataType, MetadataType,
metadataValueTypeEnum,
} from './constant'; } from './constant';
import { IMetaDataTableData } from './interface'; import { IMetaDataTableData } from './interface';
@ -80,7 +83,7 @@ export const useMetadataColumns = ({
setEditingValue(null); setEditingValue(null);
setShouldSave(true); setShouldSave(true);
} }
}, [editingValue, setTableData]); }, [editingValue, setTableData, setShouldSave]);
const cancelEditValue = () => { const cancelEditValue = () => {
setEditingValue(null); setEditingValue(null);
@ -192,14 +195,6 @@ export const useMetadataColumns = ({
), ),
cell: ({ row }) => { cell: ({ row }) => {
const values = row.getValue('values') as Array<string>; const values = row.getValue('values') as Array<string>;
// const supportsEnum = isMetadataValueTypeWithEnum(
// row.original.valueType,
// );
// if (!supportsEnum || !Array.isArray(values) || values.length === 0) {
// return <div></div>;
// }
const displayedValues = expanded ? values : values.slice(0, 2); const displayedValues = expanded ? values : values.slice(0, 2);
const hasMore = Array.isArray(values) && values.length > 2; const hasMore = Array.isArray(values) && values.length > 2;
@ -214,26 +209,46 @@ export const useMetadataColumns = ({
return isEditing ? ( return isEditing ? (
<div key={value}> <div key={value}>
<Input {row.original.valueType ===
type="text" metadataValueTypeEnum.time && (
value={editingValue.newValue} <DateInput
onChange={(e) => value={new Date(editingValue.newValue)}
setEditingValue({ onChange={(value) => {
...editingValue, setEditingValue({
newValue: e.target.value, ...editingValue,
}) newValue: formatDate(
} value,
onBlur={saveEditedValue} 'YYYY-MM-DDTHH:mm:ss',
onKeyDown={(e) => { ),
if (e.key === 'Enter') { });
saveEditedValue(); // onValueChange(index, formatDate(value), true);
} else if (e.key === 'Escape') { }}
cancelEditValue(); showTimeSelect={true}
/>
)}
{row.original.valueType !==
metadataValueTypeEnum.time && (
<Input
type="text"
value={editingValue.newValue}
onChange={(e) =>
setEditingValue({
...editingValue,
newValue: e.target.value,
})
} }
}} onBlur={saveEditedValue}
autoFocus onKeyDown={(e) => {
// className="text-sm min-w-20 max-w-32 outline-none bg-transparent px-1 py-0.5" if (e.key === 'Enter') {
/> saveEditedValue();
} else if (e.key === 'Escape') {
cancelEditValue();
}
}}
autoFocus
// className="text-sm min-w-20 max-w-32 outline-none bg-transparent px-1 py-0.5"
/>
)}
</div> </div>
) : ( ) : (
<Button <Button

View File

@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { DateInput } from '@/components/ui/input-date'; import { DateInput } from '@/components/ui/input-date';
import { Modal } from '@/components/ui/modal/modal'; import { Modal } from '@/components/ui/modal/modal';
import { formatPureDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Plus, Trash2 } from 'lucide-react'; import { Plus, Trash2 } from 'lucide-react';
import { memo, useMemo, useRef, useState } from 'react'; import { memo, useMemo, useRef, useState } from 'react';
@ -46,11 +46,10 @@ const ValueInputItem = memo(
try { try {
// Using dayjs to parse date strings in various formats including DD/MM/YYYY // Using dayjs to parse date strings in various formats including DD/MM/YYYY
const parsedDate = dayjs(item, [ const parsedDate = dayjs(item, [
'YYYY-MM-DD HH:mm:ss',
'DD/MM/YYYY HH:mm:ss',
'YYYY-MM-DD', 'YYYY-MM-DD',
'DD/MM/YYYY', 'DD/MM/YYYY',
'MM/DD/YYYY',
'DD-MM-YYYY',
'MM-DD-YYYY',
]); ]);
if (!parsedDate.isValid()) { if (!parsedDate.isValid()) {
@ -67,6 +66,7 @@ const ValueInputItem = memo(
} }
return item; return item;
}, [item, type]); }, [item, type]);
return ( return (
<div <div
key={`value-item-${index}`} key={`value-item-${index}`}
@ -77,8 +77,13 @@ const ValueInputItem = memo(
<DateInput <DateInput
value={value as Date} value={value as Date}
onChange={(value) => { onChange={(value) => {
onValueChange(index, formatPureDate(value), true); onValueChange(
index,
formatDate(value, 'YYYY-MM-DDTHH:mm:ss'),
true,
);
}} }}
showTimeSelect={true}
/> />
)} )}
{type !== 'time' && ( {type !== 'time' && (

View File

@ -1,10 +1,11 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export function formatDate(date: any) { export function formatDate(date: any, format?: string) {
const thisFormat = format || 'DD/MM/YYYY HH:mm:ss';
if (!date) { if (!date) {
return ''; return '';
} }
return dayjs(date).format('DD/MM/YYYY HH:mm:ss'); return dayjs(date).format(thisFormat);
} }
export function formatTime(date: any) { export function formatTime(date: any) {