mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-03 00:55:10 +08:00
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:
@ -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 (
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -92,4 +92,4 @@ function Calendar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Calendar, DateRange };
|
export { Calendar, type DateRange };
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
641
web/src/components/ui/time-picker.tsx
Normal file
641
web/src/components/ui/time-picker.tsx
Normal 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 };
|
||||||
@ -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 },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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' && (
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user