add date picker operators to smart playlist

This commit is contained in:
jeffvli
2025-11-29 16:31:10 -08:00
parent 6094a520e2
commit a6945bc1f3
4 changed files with 186 additions and 8 deletions
@@ -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%"
/> />
) : ( ) : (
+34 -3
View File
@@ -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}
/>
);
};