mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-22 03:44:34 +02:00
378 lines
13 KiB
TypeScript
378 lines
13 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
import { Filters } from '/@/renderer/components/query-builder';
|
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|
import { DateInput } from '/@/shared/components/date-picker/date-picker';
|
|
import { Group } from '/@/shared/components/group/group';
|
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
|
import { Select } from '/@/shared/components/select/select';
|
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
|
import { QueryBuilderRule } from '/@/shared/types/types';
|
|
|
|
type DeleteArgs = {
|
|
groupIndex: number[];
|
|
level: number;
|
|
uniqueId: string;
|
|
};
|
|
|
|
interface QueryOptionProps {
|
|
data: QueryBuilderRule;
|
|
filters: Filters;
|
|
groupIndex: number[];
|
|
level: number;
|
|
noRemove: boolean;
|
|
onChangeField: (args: any) => void;
|
|
onChangeOperator: (args: any) => void;
|
|
onChangeValue: (args: any) => void;
|
|
onDeleteRule: (args: DeleteArgs) => void;
|
|
operators: {
|
|
boolean: { label: string; value: string }[];
|
|
date: { label: string; value: string }[];
|
|
number: { label: string; value: string }[];
|
|
string: { label: string; value: string }[];
|
|
};
|
|
selectData?: { label: string; value: string }[];
|
|
}
|
|
|
|
const QueryValueInput = ({
|
|
data,
|
|
defaultValue,
|
|
onChange,
|
|
operator,
|
|
type,
|
|
value: valueProp,
|
|
...props
|
|
}: any) => {
|
|
const [numberRange, setNumberRange] = useState<number[]>([0, 0]);
|
|
|
|
// Parse date value helper - converts date string (YYYY-MM-DD) to Date for display
|
|
const parseDateValue = (val: any): Date | null => {
|
|
if (!val) return null;
|
|
if (val instanceof Date) return val;
|
|
if (typeof val === 'string') {
|
|
// Handle YYYY-MM-DD format strings
|
|
const parsed = new Date(val);
|
|
if (isNaN(parsed.getTime())) return null;
|
|
return parsed;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const value = valueProp !== undefined ? valueProp : defaultValue;
|
|
|
|
// Store date range as strings for state management
|
|
const [dateRange, setDateRange] = useState<[null | string, null | string]>(() => {
|
|
const currentValue = value !== undefined ? value : defaultValue;
|
|
if (currentValue && Array.isArray(currentValue)) {
|
|
return [
|
|
typeof currentValue[0] === 'string' ? currentValue[0] : null,
|
|
typeof currentValue[1] === 'string' ? currentValue[1] : null,
|
|
];
|
|
}
|
|
return [null, null];
|
|
});
|
|
|
|
// Sync dateRange state when value changes
|
|
useEffect(() => {
|
|
const currentValue = value !== undefined ? value : defaultValue;
|
|
if (operator === 'inTheRangeDate' && currentValue && Array.isArray(currentValue)) {
|
|
setDateRange([
|
|
typeof currentValue[0] === 'string' ? currentValue[0] : null,
|
|
typeof currentValue[1] === 'string' ? currentValue[1] : null,
|
|
]);
|
|
}
|
|
}, [value, defaultValue, operator]);
|
|
|
|
// Sync numberRange state when value changes
|
|
useEffect(() => {
|
|
const currentValue = value !== undefined ? value : defaultValue;
|
|
if (operator === 'inTheRange' && currentValue && Array.isArray(currentValue)) {
|
|
setNumberRange([
|
|
typeof currentValue[0] === 'number'
|
|
? currentValue[0]
|
|
: Number(currentValue[0]) || 0,
|
|
typeof currentValue[1] === 'number'
|
|
? currentValue[1]
|
|
: Number(currentValue[1]) || 0,
|
|
]);
|
|
}
|
|
}, [value, defaultValue, operator]);
|
|
|
|
// Check if operator requires DatePicker
|
|
const isDatePickerOperator =
|
|
operator === 'beforeDate' || operator === 'afterDate' || operator === 'inTheRangeDate';
|
|
|
|
const BooleanSelectComponent = useMemo(
|
|
() => (
|
|
<Select
|
|
data={[
|
|
{ label: 'true', value: 'true' },
|
|
{ label: 'false', value: 'false' },
|
|
]}
|
|
onChange={onChange}
|
|
value={value}
|
|
{...props}
|
|
/>
|
|
),
|
|
[onChange, props, value],
|
|
);
|
|
|
|
if (operator === 'isMissing' || operator === 'isPresent') {
|
|
return BooleanSelectComponent;
|
|
}
|
|
|
|
switch (type) {
|
|
case 'boolean':
|
|
return BooleanSelectComponent;
|
|
case 'date':
|
|
if (isDatePickerOperator && operator !== 'inTheRangeDate') {
|
|
const dateValue = value ? parseDateValue(value) : null;
|
|
return (
|
|
<DateInput
|
|
clearable
|
|
defaultLevel="year"
|
|
maxWidth={170}
|
|
onChange={(date) => {
|
|
// DateInput returns string in 'YYYY-MM-DD' format (local timezone)
|
|
// Return raw string value - no transformation needed
|
|
onChange(date || '');
|
|
}}
|
|
size="sm"
|
|
value={dateValue}
|
|
valueFormat="YYYY-MM-DD"
|
|
width="25%"
|
|
/>
|
|
);
|
|
}
|
|
return <TextInput onChange={onChange} size="sm" value={value} {...props} />;
|
|
case 'dateRange':
|
|
if (operator === 'inTheRangeDate') {
|
|
return (
|
|
<Group gap="sm" grow wrap="nowrap">
|
|
<DateInput
|
|
clearable
|
|
defaultLevel="year"
|
|
maxWidth={81}
|
|
onChange={(date) => {
|
|
// DateInput returns string in 'YYYY-MM-DD' format (local timezone)
|
|
const newRange: [null | string, null | string] = [
|
|
date || null,
|
|
dateRange[1],
|
|
];
|
|
setDateRange(newRange);
|
|
// Return raw string values - no transformation needed
|
|
onChange([date || null, dateRange[1] || null]);
|
|
}}
|
|
size="sm"
|
|
value={dateRange[0] ? parseDateValue(dateRange[0]) : null}
|
|
valueFormat="YYYY-MM-DD"
|
|
width="10%"
|
|
/>
|
|
<DateInput
|
|
clearable
|
|
defaultLevel="year"
|
|
maxWidth={81}
|
|
onChange={(date) => {
|
|
// DateInput returns string in 'YYYY-MM-DD' format (local timezone)
|
|
const newRange: [null | string, null | string] = [
|
|
dateRange[0],
|
|
date || null,
|
|
];
|
|
setDateRange(newRange);
|
|
// Return raw string values - no transformation needed
|
|
onChange([dateRange[0] || null, date || null]);
|
|
}}
|
|
size="sm"
|
|
value={dateRange[1] ? parseDateValue(dateRange[1]) : null}
|
|
valueFormat="YYYY-MM-DD"
|
|
width="10%"
|
|
/>
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<NumberInput
|
|
{...props}
|
|
maxWidth={81}
|
|
onChange={(e) => {
|
|
const newRange = [Number(e) || 0, numberRange[1]];
|
|
setNumberRange(newRange);
|
|
onChange(newRange);
|
|
}}
|
|
value={numberRange[0] || undefined}
|
|
width="10%"
|
|
/>
|
|
<NumberInput
|
|
{...props}
|
|
maxWidth={81}
|
|
onChange={(e) => {
|
|
const newRange = [numberRange[0], Number(e) || 0];
|
|
setNumberRange(newRange);
|
|
onChange(newRange);
|
|
}}
|
|
value={numberRange[1] || undefined}
|
|
width="10%"
|
|
/>
|
|
</>
|
|
);
|
|
case 'number':
|
|
return (
|
|
<NumberInput
|
|
onChange={onChange}
|
|
size="sm"
|
|
value={
|
|
value !== undefined && value !== null && value !== ''
|
|
? Number(value)
|
|
: undefined
|
|
}
|
|
{...props}
|
|
/>
|
|
);
|
|
case 'playlist':
|
|
return <Select data={data} onChange={onChange} value={value} {...props} />;
|
|
case 'string':
|
|
return <TextInput onChange={onChange} size="sm" value={value || ''} {...props} />;
|
|
|
|
default:
|
|
return <></>;
|
|
}
|
|
};
|
|
|
|
export const QueryBuilderOption = ({
|
|
data,
|
|
filters,
|
|
groupIndex,
|
|
level,
|
|
noRemove,
|
|
onChangeField,
|
|
onChangeOperator,
|
|
onChangeValue,
|
|
onDeleteRule,
|
|
operators,
|
|
selectData,
|
|
}: QueryOptionProps) => {
|
|
const { field, operator, uniqueId, value } = data;
|
|
|
|
const handleDeleteRule = () => {
|
|
onDeleteRule({ groupIndex, level, uniqueId });
|
|
};
|
|
|
|
const handleChangeField = (e: any) => {
|
|
onChangeField({ groupIndex, level, uniqueId, value: e });
|
|
};
|
|
|
|
const handleChangeOperator = (e: any) => {
|
|
onChangeOperator({ groupIndex, level, uniqueId, value: e });
|
|
};
|
|
|
|
const handleChangeValue = (e: any) => {
|
|
const isDirectValue =
|
|
typeof e === 'string' || typeof e === 'number' || typeof e === 'undefined';
|
|
|
|
if (isDirectValue) {
|
|
return onChangeValue({
|
|
groupIndex,
|
|
level,
|
|
uniqueId,
|
|
value: e,
|
|
});
|
|
}
|
|
|
|
// const isDate = e instanceof Date;
|
|
|
|
// if (isDate) {
|
|
// return onChangeValue({
|
|
// groupIndex,
|
|
// level,
|
|
// uniqueId,
|
|
// value: dayjs(e).format('YYYY-MM-DD'),
|
|
// });
|
|
// }
|
|
|
|
const isArray = Array.isArray(e);
|
|
|
|
if (isArray) {
|
|
return onChangeValue({
|
|
groupIndex,
|
|
level,
|
|
uniqueId,
|
|
value: e,
|
|
});
|
|
}
|
|
|
|
return onChangeValue({
|
|
groupIndex,
|
|
level,
|
|
uniqueId,
|
|
value: e.currentTarget.value,
|
|
});
|
|
};
|
|
|
|
// Handle both grouped and flat filter data
|
|
const flatFilters = filters.some((f: any) => f.group && f.items)
|
|
? filters.flatMap((group: any) => group.items || [])
|
|
: filters;
|
|
const fieldType = flatFilters.find((f: any) => f.value === field)?.type;
|
|
const operatorsByFieldType = operators[fieldType as keyof typeof operators];
|
|
const ml = 20;
|
|
|
|
return (
|
|
<Group gap="sm" ml={ml}>
|
|
<Select
|
|
data={filters}
|
|
maxWidth={170}
|
|
onChange={handleChangeField}
|
|
searchable
|
|
size="sm"
|
|
value={field}
|
|
width="25%"
|
|
/>
|
|
<Select
|
|
data={operatorsByFieldType || []}
|
|
disabled={!field}
|
|
maxWidth={170}
|
|
onChange={handleChangeOperator}
|
|
searchable
|
|
size="sm"
|
|
value={operator}
|
|
width="25%"
|
|
/>
|
|
{field ? (
|
|
<QueryValueInput
|
|
data={selectData || []}
|
|
maxWidth={170}
|
|
onChange={handleChangeValue}
|
|
operator={operator}
|
|
size="sm"
|
|
type={
|
|
operator === 'inTheRange' || operator === 'inTheRangeDate'
|
|
? 'dateRange'
|
|
: fieldType
|
|
}
|
|
value={value}
|
|
width="25%"
|
|
/>
|
|
) : (
|
|
<TextInput
|
|
disabled
|
|
maxWidth={170}
|
|
onChange={handleChangeValue}
|
|
size="sm"
|
|
value={value || ''}
|
|
width="25%"
|
|
/>
|
|
)}
|
|
<ActionIcon
|
|
disabled={noRemove}
|
|
icon="remove"
|
|
onClick={handleDeleteRule}
|
|
px={5}
|
|
size="sm"
|
|
variant="subtle"
|
|
/>
|
|
</Group>
|
|
);
|
|
};
|