mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add date picker operators to smart playlist
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Filters } from '/@/renderer/components/query-builder';
|
import { Filters } from '/@/renderer/components/query-builder';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
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 { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
import { Select } from '/@/shared/components/select/select';
|
import { Select } from '/@/shared/components/select/select';
|
||||||
@@ -33,9 +34,47 @@ interface QueryOptionProps {
|
|||||||
selectData?: { label: string; value: string }[];
|
selectData?: { label: string; value: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
const QueryValueInput = ({ data, defaultValue, onChange, operator, type, ...props }: any) => {
|
||||||
const [numberRange, setNumberRange] = useState<number[]>([0, 0]);
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store date range as strings for state management
|
||||||
|
const [dateRange, setDateRange] = useState<[null | string, null | string]>(() => {
|
||||||
|
if (defaultValue && Array.isArray(defaultValue)) {
|
||||||
|
return [
|
||||||
|
typeof defaultValue[0] === 'string' ? defaultValue[0] : null,
|
||||||
|
typeof defaultValue[1] === 'string' ? defaultValue[1] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [null, null];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync dateRange state when defaultValue changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (operator === 'inTheRangeDate' && defaultValue && Array.isArray(defaultValue)) {
|
||||||
|
setDateRange([
|
||||||
|
typeof defaultValue[0] === 'string' ? defaultValue[0] : null,
|
||||||
|
typeof defaultValue[1] === 'string' ? defaultValue[1] : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, [defaultValue, operator]);
|
||||||
|
|
||||||
|
// Check if operator requires DatePicker
|
||||||
|
const isDatePickerOperator =
|
||||||
|
operator === 'beforeDate' || operator === 'afterDate' || operator === 'inTheRangeDate';
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return (
|
return (
|
||||||
@@ -49,8 +88,72 @@ const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'date':
|
case 'date':
|
||||||
|
if (isDatePickerOperator && operator !== 'inTheRangeDate') {
|
||||||
|
const dateValue = defaultValue ? parseDateValue(defaultValue) : 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" {...props} />;
|
return <TextInput onChange={onChange} size="sm" {...props} />;
|
||||||
case 'dateRange':
|
case 'dateRange':
|
||||||
|
if (operator === 'inTheRangeDate') {
|
||||||
|
return (
|
||||||
|
<Group gap="sm" 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
@@ -201,8 +304,13 @@ export const QueryBuilderOption = ({
|
|||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
maxWidth={170}
|
maxWidth={170}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
|
operator={operator}
|
||||||
size="sm"
|
size="sm"
|
||||||
type={operator === 'inTheRange' ? 'dateRange' : fieldType}
|
type={
|
||||||
|
operator === 'inTheRange' || operator === 'inTheRangeDate'
|
||||||
|
? 'dateRange'
|
||||||
|
: fieldType
|
||||||
|
}
|
||||||
width="25%"
|
width="25%"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -19,9 +19,18 @@ export const parseQueryBuilderChildren = (groups: QueryBuilderGroup[], data: any
|
|||||||
for (const rule of group.rules) {
|
for (const rule of group.rules) {
|
||||||
if (rule.field && rule.operator) {
|
if (rule.field && rule.operator) {
|
||||||
const [table, field] = rule.field.split('.');
|
const [table, field] = rule.field.split('.');
|
||||||
const operator = rule.operator;
|
let operator = rule.operator;
|
||||||
const value = field !== 'releaseDate' ? rule.value : new Date(rule.value);
|
const value = field !== 'releaseDate' ? rule.value : new Date(rule.value);
|
||||||
|
|
||||||
|
// Transform date picker operators back to original operators
|
||||||
|
if (operator === 'beforeDate') {
|
||||||
|
operator = 'before';
|
||||||
|
} else if (operator === 'afterDate') {
|
||||||
|
operator = 'after';
|
||||||
|
} else if (operator === 'inTheRangeDate') {
|
||||||
|
operator = 'inTheRange';
|
||||||
|
}
|
||||||
|
|
||||||
switch (table) {
|
switch (table) {
|
||||||
default:
|
default:
|
||||||
query[rootType].push({
|
query[rootType].push({
|
||||||
@@ -56,7 +65,7 @@ export const convertQueryGroupToNDQuery = (filter: QueryBuilderGroup) => {
|
|||||||
for (const rule of filter.rules) {
|
for (const rule of filter.rules) {
|
||||||
if (rule.field && rule.operator) {
|
if (rule.field && rule.operator) {
|
||||||
const [field] = rule.field.split('.');
|
const [field] = rule.field.split('.');
|
||||||
const operator = rule.operator;
|
let operator = rule.operator;
|
||||||
let value = rule.value;
|
let value = rule.value;
|
||||||
|
|
||||||
const booleanFields = NDSongQueryFields.filter(
|
const booleanFields = NDSongQueryFields.filter(
|
||||||
@@ -68,6 +77,14 @@ export const convertQueryGroupToNDQuery = (filter: QueryBuilderGroup) => {
|
|||||||
value = value === 'true';
|
value = value === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (operator === 'beforeDate') {
|
||||||
|
operator = 'before';
|
||||||
|
} else if (operator === 'afterDate') {
|
||||||
|
operator = 'after';
|
||||||
|
} else if (operator === 'inTheRangeDate') {
|
||||||
|
operator = 'inTheRange';
|
||||||
|
}
|
||||||
|
|
||||||
switch (field) {
|
switch (field) {
|
||||||
default:
|
default:
|
||||||
rootQuery[rootQueryType].push({
|
rootQuery[rootQueryType].push({
|
||||||
@@ -103,7 +120,7 @@ export const convertNDQueryToQueryGroup = (query: Record<string, any>) => {
|
|||||||
const group = convertNDQueryToQueryGroup(rule);
|
const group = convertNDQueryToQueryGroup(rule);
|
||||||
rootGroup.group.push(group);
|
rootGroup.group.push(group);
|
||||||
} else {
|
} else {
|
||||||
const operator = Object.keys(rule)[0];
|
let operator = Object.keys(rule)[0];
|
||||||
const field = Object.keys(rule[operator])[0];
|
const field = Object.keys(rule[operator])[0];
|
||||||
let value = rule[operator][field];
|
let value = rule[operator][field];
|
||||||
|
|
||||||
@@ -116,6 +133,20 @@ export const convertNDQueryToQueryGroup = (query: Record<string, any>) => {
|
|||||||
value = value.toString();
|
value = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dateFields = NDSongQueryFields.filter(
|
||||||
|
(queryField) => queryField.type === 'date' || queryField.type === 'dateRange',
|
||||||
|
).map((field) => field.value);
|
||||||
|
|
||||||
|
if (dateFields.includes(field)) {
|
||||||
|
if (operator === 'before') {
|
||||||
|
operator = 'beforeDate';
|
||||||
|
} else if (operator === 'after') {
|
||||||
|
operator = 'afterDate';
|
||||||
|
} else if (operator === 'inTheRange') {
|
||||||
|
operator = 'inTheRangeDate';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootGroup.rules.push({
|
rootGroup.rules.push({
|
||||||
field,
|
field,
|
||||||
operator,
|
operator,
|
||||||
|
|||||||
@@ -192,6 +192,9 @@ export const NDSongQueryDateOperators = [
|
|||||||
{ label: 'is in the last', value: 'inTheLast' },
|
{ label: 'is in the last', value: 'inTheLast' },
|
||||||
{ label: 'is not in the last', value: 'notInTheLast' },
|
{ label: 'is not in the last', value: 'notInTheLast' },
|
||||||
{ label: 'is in the range', value: 'inTheRange' },
|
{ label: 'is in the range', value: 'inTheRange' },
|
||||||
|
{ label: 'is before (date)', value: 'beforeDate' },
|
||||||
|
{ label: 'is after (date)', value: 'afterDate' },
|
||||||
|
{ label: 'is in the range (date)', value: 'inTheRangeDate' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NDSongQueryStringOperators = [
|
export const NDSongQueryStringOperators = [
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { DateInputProps as MantineDateInputProps } from '@mantine/dates';
|
import type {
|
||||||
|
DateInputProps as MantineDateInputProps,
|
||||||
|
DateTimePickerProps as MantineDateTimeInputProps,
|
||||||
|
} from '@mantine/dates';
|
||||||
|
|
||||||
import { DateInput as MantineDateInput } from '@mantine/dates';
|
import {
|
||||||
|
DateInput as MantineDateInput,
|
||||||
|
DateTimePicker as MantineDateTimeInput,
|
||||||
|
} from '@mantine/dates';
|
||||||
|
|
||||||
import styles from './date-picker.module.css';
|
import styles from './date-picker.module.css';
|
||||||
|
|
||||||
@@ -33,3 +39,33 @@ export const DateInput = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface DateTimeInputProps extends MantineDateTimeInputProps {
|
||||||
|
maxWidth?: number | string;
|
||||||
|
width?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DateTimeInput = ({
|
||||||
|
classNames,
|
||||||
|
maxWidth,
|
||||||
|
size = 'sm',
|
||||||
|
style,
|
||||||
|
width,
|
||||||
|
...props
|
||||||
|
}: DateTimeInputProps) => {
|
||||||
|
return (
|
||||||
|
<MantineDateTimeInput
|
||||||
|
classNames={{
|
||||||
|
input: styles.input,
|
||||||
|
label: styles.label,
|
||||||
|
required: styles.required,
|
||||||
|
root: styles.root,
|
||||||
|
section: styles.section,
|
||||||
|
...classNames,
|
||||||
|
}}
|
||||||
|
size={size}
|
||||||
|
style={{ maxWidth, width, ...style }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user