Fix: Metadata time Picker (#12796)

### What problem does this PR solve?

Fix: Metadata time Picker

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2026-01-23 16:55:43 +08:00
committed by GitHub
parent e1df82946e
commit 11470906cf
4 changed files with 143 additions and 77 deletions

View File

@ -10,6 +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 { useTranslation } from 'react-i18next';
import { Button } from './button';
import { TimePicker } from './time-picker'; import { TimePicker } from './time-picker';
// import TimePicker from 'react-time-picker'; // import TimePicker from 'react-time-picker';
interface DateInputProps extends Omit< interface DateInputProps extends Omit<
@ -22,9 +24,8 @@ interface DateInputProps extends Omit<
dateFormat?: string; dateFormat?: string;
timeFormat?: string; timeFormat?: string;
showTimeSelectOnly?: boolean; showTimeSelectOnly?: boolean;
showTimeInput?: boolean;
timeInputLabel?: string;
locale?: Locale; // Support for internationalization locale?: Locale; // Support for internationalization
openChange?: (open: boolean) => void;
} }
const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>( const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
@ -37,41 +38,45 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
timeFormat = 'HH:mm:ss', timeFormat = 'HH:mm:ss',
showTimeSelect = false, showTimeSelect = false,
showTimeSelectOnly = false, showTimeSelectOnly = false,
showTimeInput = false, openChange,
timeInputLabel = '',
...props ...props
}, },
ref, ref,
) => { ) => {
const { t } = useTranslation();
const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(
value,
);
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const handleDateSelect = (date: Date | undefined) => { const handleDateSelect = (date: Date | undefined) => {
if (value) { if (selectedDate) {
const valueDate = dayjs(value); const valueDate = dayjs(selectedDate);
date?.setHours(valueDate.hour()); date?.setHours(valueDate.hour());
date?.setMinutes(valueDate.minute()); date?.setMinutes(valueDate.minute());
date?.setSeconds(valueDate.second()); date?.setSeconds(valueDate.second());
} }
onChange?.(date); setSelectedDate(date);
// setOpen(false); // onChange?.(date);
}; };
const handleTimeSelect = (date: Date | undefined) => { const handleTimeSelect = (date: Date | undefined) => {
const valueDate = dayjs(value); const valueDate = dayjs(selectedDate);
if (value) { if (selectedDate) {
date?.setFullYear(valueDate.year()); date?.setFullYear(valueDate.year());
date?.setMonth(valueDate.month()); date?.setMonth(valueDate.month());
date?.setDate(valueDate.date()); date?.setDate(valueDate.date());
} }
if (date) { if (date) {
onChange?.(date); // onChange?.(date);
setSelectedDate(date);
} else { } else {
valueDate?.hour(0); valueDate?.hour(0);
valueDate?.minute(0); valueDate?.minute(0);
valueDate?.second(0); valueDate?.second(0);
onChange?.(valueDate.toDate()); // onChange?.(valueDate.toDate());
setSelectedDate(valueDate.toDate());
} }
// setOpen(false);
}; };
// Determine display format based on the type of date picker // Determine display format based on the type of date picker
@ -84,14 +89,23 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
// Format the date according to the specified format // Format the date according to the specified format
const formattedValue = React.useMemo(() => { const formattedValue = React.useMemo(() => {
return value && !isNaN(value.getTime()) return selectedDate && !isNaN(selectedDate.getTime())
? dayjs(value).format(displayFormat) ? dayjs(selectedDate).format(displayFormat)
: ''; : '';
}, [value, displayFormat]); }, [selectedDate, displayFormat]);
const handleOpenChange = (open: boolean) => {
setOpen(open);
openChange?.(open);
};
return ( return (
<div className="grid gap-2"> <div className="grid gap-2">
<Popover open={open} onOpenChange={setOpen}> <Popover
open={open}
onOpenChange={handleOpenChange}
disableOutsideClick
>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div className="relative"> <div className="relative">
<Input <Input
@ -113,12 +127,12 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
<PopoverContent className="w-auto p-2" align="start"> <PopoverContent className="w-auto p-2" align="start">
<Calendar <Calendar
mode="single" mode="single"
selected={value} selected={selectedDate}
onSelect={handleDateSelect} onSelect={handleDateSelect}
/> />
{showTimeSelect && ( {showTimeSelect && (
<TimePicker <TimePicker
value={value} value={selectedDate}
onChange={(value: Date | undefined) => { onChange={(value: Date | undefined) => {
handleTimeSelect(value); handleTimeSelect(value);
}} }}
@ -126,6 +140,29 @@ const DateInput = React.forwardRef<HTMLInputElement, DateInputProps>(
/> />
// <TimePicker onChange={onChange} value={value} /> // <TimePicker onChange={onChange} value={value} />
)} )}
<div className="w-full flex justify-end mt-2">
<Button
variant="ghost"
type="button"
className="text-sm mr-2"
onClick={() => {
onChange?.(value);
handleOpenChange(false);
}}
>
{t('common.cancel')}
</Button>
<Button
type="button"
className="text-sm text-text-primary-inverse "
onClick={() => {
onChange?.(selectedDate);
handleOpenChange(false);
}}
>
{t('common.confirm')}
</Button>
</div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>

View File

@ -5,20 +5,32 @@ import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Popover = (props: PopoverPrimitive.PopoverProps) => { interface PopoverProps extends PopoverPrimitive.PopoverProps {
const { children, open: openState, onOpenChange } = props; disableOutsideClick?: boolean;
}
const Popover = ({
children,
open: openState,
onOpenChange,
disableOutsideClick = false,
}: PopoverProps) => {
const [open, setOpen] = React.useState(true); const [open, setOpen] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
setOpen(!!openState); setOpen(!!openState);
}, [openState]); }, [openState]);
const handleOnOpenChange = React.useCallback( const handleOnOpenChange = React.useCallback(
(e: boolean) => { (e: boolean) => {
if (disableOutsideClick && !e) {
return;
}
if (onOpenChange) { if (onOpenChange) {
onOpenChange?.(e); onOpenChange?.(e);
} }
setOpen(e); setOpen(e);
}, },
[onOpenChange], [onOpenChange, disableOutsideClick],
); );
return ( return (
<PopoverPrimitive.Root open={open} onOpenChange={handleOnOpenChange}> <PopoverPrimitive.Root open={open} onOpenChange={handleOnOpenChange}>

View File

@ -153,7 +153,7 @@ const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
bordered = true, bordered = true,
disabledTime, disabledTime,
hideDisabledOptions = false, hideDisabledOptions = false,
inputReadOnly = false, inputReadOnly = true,
use12Hours = false, use12Hours = false,
size = 'middle', size = 'middle',
status, status,
@ -495,47 +495,48 @@ const TimePicker = forwardRef<HTMLDivElement, TimePickerProps>(
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
modal={false} // Use non-modal dialog box, consistent with Ant Design behavior modal={false} // Use non-modal dialog box, consistent with Ant Design behavior
> >
<div className="relative"> <PopoverTrigger asChild>
<Input <div className="relative">
type="text" <Input
value={inputValue} type="text"
onChange={handleInputChange} value={inputValue}
placeholder={placeholder} onChange={handleInputChange}
disabled={disabled} placeholder={placeholder}
readOnly={inputReadOnly} disabled={disabled}
className={cn( readOnly={inputReadOnly}
'pl-3 pr-8 py-2 font-normal', className={cn(
size === 'large' && 'h-10 text-base', 'pl-3 pr-8 py-2 font-normal',
size === 'small' && 'h-8 text-sm', size === 'large' && 'h-10 text-base',
status === 'error' && 'border-red-500', size === 'small' && 'h-8 text-sm',
status === 'warning' && '!border-yellow-500', status === 'error' && 'border-red-500',
!bordered && 'border-transparent', status === 'warning' && '!border-yellow-500',
'cursor-pointer', !bordered && 'border-transparent',
)} 'cursor-pointer',
autoFocus={autoFocus} )}
/> autoFocus={autoFocus}
<div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center"> />
{allowClear && value && inputValue && ( <div className="absolute right-3 top-1/2 transform -translate-y-1/2 flex items-center">
<button {allowClear && value && inputValue && (
type="button" <button
onClick={(e) => { type="button"
e.stopPropagation(); onClick={(e) => {
handleClear(); e.stopPropagation();
}} handleClear();
className="mr-2 text-muted-foreground hover:text-foreground" }}
> className="mr-2 text-muted-foreground hover:text-foreground"
{clearIcon || '✕'} >
</button> {clearIcon || '✕'}
)} </button>
<PopoverTrigger asChild> )}
<div className="cursor-pointer"> <div className="cursor-pointer">
{suffixIcon || ( {suffixIcon || (
<Clock size={16} className="text-muted-foreground" /> <Clock size={16} className="text-muted-foreground" />
)} )}
</div> </div>
</PopoverTrigger> </div>
</div> </div>
</div> </PopoverTrigger>
<PopoverContent <PopoverContent
className={cn('w-auto p-3', popupClassName)} className={cn('w-auto p-3', popupClassName)}
align={ align={

View File

@ -67,23 +67,27 @@ export const useMetadataColumns = ({
setEditingValue({ field, value, newValue: value }); setEditingValue({ field, value, newValue: value });
}; };
const saveEditedValue = useCallback(() => { const saveEditedValue = useCallback(
if (editingValue) { (newValue?: { field: string; value: string; newValue: string }) => {
setTableData((prev) => { const realValue = newValue || editingValue;
return prev.map((row) => { if (realValue) {
if (row.field === editingValue.field) { setTableData((prev) => {
const updatedValues = row.values.map((v) => return prev.map((row) => {
v === editingValue.value ? editingValue.newValue : v, if (row.field === realValue.field) {
); const updatedValues = row.values.map((v) =>
return { ...row, values: updatedValues }; v === realValue.value ? realValue.newValue : v,
} );
return row; return { ...row, values: updatedValues };
}
return row;
});
}); });
}); setEditingValue(null);
setEditingValue(null); setShouldSave(true);
setShouldSave(true); }
} },
}, [editingValue, setTableData, setShouldSave]); [editingValue, setTableData, setShouldSave],
);
const cancelEditValue = () => { const cancelEditValue = () => {
setEditingValue(null); setEditingValue(null);
@ -214,15 +218,23 @@ export const useMetadataColumns = ({
<DateInput <DateInput
value={new Date(editingValue.newValue)} value={new Date(editingValue.newValue)}
onChange={(value) => { onChange={(value) => {
setEditingValue({ console.log('value', value);
const newValue = {
...editingValue, ...editingValue,
newValue: formatDate( newValue: formatDate(
value, value,
'YYYY-MM-DDTHH:mm:ss', 'YYYY-MM-DDTHH:mm:ss',
), ),
}); };
setEditingValue(newValue);
saveEditedValue(newValue);
// onValueChange(index, formatDate(value), true); // onValueChange(index, formatDate(value), true);
}} }}
// openChange={(open) => {
// console.log('open', open);
// if (!open) {
// }
// }}
showTimeSelect={true} showTimeSelect={true}
/> />
)} )}
@ -261,7 +273,11 @@ export const useMetadataColumns = ({
aria-label="Edit" aria-label="Edit"
> >
<div className="flex gap-1 items-center"> <div className="flex gap-1 items-center">
<div className="text-sm truncate max-w-24">{value}</div> <div className="text-sm truncate max-w-24">
{row.original.valueType === metadataValueTypeEnum.time
? formatDate(value, 'DD/MM/YYYY HH:mm:ss')
: value}
</div>
{isDeleteSingleValue && ( {isDeleteSingleValue && (
<Button <Button
variant={'delete'} variant={'delete'}