Finalize base features for smart playlist editor
This commit is contained in:
parent
0c7a0cc88a
commit
df4f05b14c
6 changed files with 739 additions and 624 deletions
|
@ -353,40 +353,74 @@ export type NDPlaylistSongList = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NDSongQueryFields = [
|
export const NDSongQueryFields = [
|
||||||
{ label: 'Title', value: 'title' },
|
{ label: 'Album', type: 'string', value: 'album' },
|
||||||
{ label: 'Album', value: 'album' },
|
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
|
||||||
{ label: 'Artist', value: 'artist' },
|
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
|
||||||
{ label: 'Album artist', value: 'albumartist' },
|
{ label: 'Album Type', type: 'string', value: 'albumtype' },
|
||||||
{ label: 'Has cover art', value: 'hascoverart' },
|
{ label: 'Artist', type: 'string', value: 'artist' },
|
||||||
{ label: 'Track number', value: 'tracknumber' },
|
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
|
||||||
{ label: 'Disc number', value: 'discnumber' },
|
{ label: 'BPM', type: 'number', value: 'bpm' },
|
||||||
{ label: 'Year', value: 'year' },
|
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
|
||||||
{ label: 'Size', value: 'size' },
|
{ label: 'Channels', type: 'number', value: 'channels' },
|
||||||
{ label: 'Is compilation', value: 'compilation' },
|
{ label: 'Comment', type: 'string', value: 'comment' },
|
||||||
{ label: 'Date added', value: 'dateadded' },
|
{ label: 'Date Added', type: 'date', value: 'dateadded' },
|
||||||
{ label: 'Date modified', value: 'datemodified' },
|
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
|
||||||
{ label: 'Disc subtitle', value: 'discsubtitle' },
|
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
|
||||||
{ label: 'Comment', value: 'comment' },
|
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
|
||||||
{ label: 'Lyrics', value: 'lyrics' },
|
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
|
||||||
{ label: 'Sort title', value: 'sorttitle' },
|
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
|
||||||
{ label: 'Sort album', value: 'sortalbum' },
|
{ label: 'Duration', type: 'number', value: 'duration' },
|
||||||
{ label: 'Sort artist', value: 'sortartist' },
|
{ label: 'File Path', type: 'string', value: 'filepath' },
|
||||||
{ label: 'Sort album artist', value: 'sortalbumartist' },
|
{ label: 'File Type', type: 'string', value: 'filetype' },
|
||||||
{ label: 'Album type', value: 'albumtype' },
|
{ label: 'Genre', type: 'string', value: 'genre' },
|
||||||
{ label: 'Album comment', value: 'albumcomment' },
|
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
|
||||||
{ label: 'Catalog number', value: 'catalognumber' },
|
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
|
||||||
{ label: 'File path', value: 'filepath' },
|
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
|
||||||
{ label: 'File type', value: 'filetype' },
|
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
|
||||||
{ label: 'Duration', value: 'duration' },
|
{ label: 'Name', type: 'string', value: 'title' },
|
||||||
{ label: 'Bitrate', value: 'bitrate' },
|
{ label: 'Play Count', type: 'number', value: 'playcount' },
|
||||||
{ label: 'BPM', value: 'bpm' },
|
{ label: 'Rating', type: 'number', value: 'rating' },
|
||||||
{ label: 'Channels', value: 'channels' },
|
{ label: 'Size', type: 'number', value: 'size' },
|
||||||
{ label: 'Genre', value: 'genre' },
|
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
|
||||||
{ label: 'Is favorite', value: 'loved' },
|
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
|
||||||
{ label: 'Date favorited', value: 'dateloved' },
|
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
|
||||||
{ label: 'Last played', value: 'lastplayed' },
|
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
|
||||||
{ label: 'Play count', value: 'playcount' },
|
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
|
||||||
{ label: 'Rating', value: 'rating' },
|
{ label: 'Year', type: 'number', value: 'year' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NDSongQueryDateOperators = [
|
||||||
|
{ label: 'is', value: 'is' },
|
||||||
|
{ label: 'is not', value: 'isNot' },
|
||||||
|
{ label: 'is before', value: 'before' },
|
||||||
|
{ label: 'is after', value: 'after' },
|
||||||
|
{ label: 'is in the last', value: 'inTheLast' },
|
||||||
|
{ label: 'is not in the last', value: 'notInTheLast' },
|
||||||
|
{ label: 'is in the range', value: 'inTheRange' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NDSongQueryStringOperators = [
|
||||||
|
{ label: 'is', value: 'is' },
|
||||||
|
{ label: 'is not', value: 'isNot' },
|
||||||
|
{ label: 'contains', value: 'contains' },
|
||||||
|
{ label: 'does not contain', value: 'notContains' },
|
||||||
|
{ label: 'starts with', value: 'startsWith' },
|
||||||
|
{ label: 'ends with', value: 'endsWith' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NDSongQueryBooleanOperators = [
|
||||||
|
{ label: 'is', value: 'is' },
|
||||||
|
{ label: 'is not', value: 'isNot' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NDSongQueryNumberOperators = [
|
||||||
|
{ label: 'is', value: 'is' },
|
||||||
|
{ label: 'is not', value: 'isNot' },
|
||||||
|
{ label: 'contains', value: 'contains' },
|
||||||
|
{ label: 'does not contain', value: 'notContains' },
|
||||||
|
{ label: 'is greater than', value: 'gt' },
|
||||||
|
{ label: 'is less than', value: 'lt' },
|
||||||
|
{ label: 'is in the range', value: 'inTheRange' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type NDUserListParams = {
|
export type NDUserListParams = {
|
||||||
|
|
|
@ -30,7 +30,7 @@ type DeleteArgs = {
|
||||||
};
|
};
|
||||||
interface QueryBuilderProps {
|
interface QueryBuilderProps {
|
||||||
data: Record<string, any>;
|
data: Record<string, any>;
|
||||||
filters: { label: string; value: string }[];
|
filters: { label: string; type: string; value: string }[];
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
onAddRule: (args: AddArgs) => void;
|
onAddRule: (args: AddArgs) => void;
|
||||||
|
@ -39,8 +39,16 @@ interface QueryBuilderProps {
|
||||||
onChangeOperator: (args: any) => void;
|
onChangeOperator: (args: any) => void;
|
||||||
onChangeType: (args: any) => void;
|
onChangeType: (args: any) => void;
|
||||||
onChangeValue: (args: any) => void;
|
onChangeValue: (args: any) => void;
|
||||||
|
onClearFilters: () => void;
|
||||||
onDeleteRule: (args: DeleteArgs) => void;
|
onDeleteRule: (args: DeleteArgs) => void;
|
||||||
onDeleteRuleGroup: (args: DeleteArgs) => void;
|
onDeleteRuleGroup: (args: DeleteArgs) => void;
|
||||||
|
onResetFilters: () => void;
|
||||||
|
operators: {
|
||||||
|
boolean: { label: string; value: string }[];
|
||||||
|
date: { label: string; value: string }[];
|
||||||
|
number: { label: string; value: string }[];
|
||||||
|
string: { label: string; value: string }[];
|
||||||
|
};
|
||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,8 +61,11 @@ export const QueryBuilder = ({
|
||||||
onAddRuleGroup,
|
onAddRuleGroup,
|
||||||
onChangeType,
|
onChangeType,
|
||||||
onChangeField,
|
onChangeField,
|
||||||
|
operators,
|
||||||
onChangeOperator,
|
onChangeOperator,
|
||||||
onChangeValue,
|
onChangeValue,
|
||||||
|
onClearFilters,
|
||||||
|
onResetFilters,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
filters,
|
filters,
|
||||||
|
@ -95,7 +106,7 @@ export const QueryBuilder = ({
|
||||||
>
|
>
|
||||||
<RiAddLine size={20} />
|
<RiAddLine size={20} />
|
||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu>
|
<DropdownMenu position="bottom-start">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
p={0}
|
p={0}
|
||||||
|
@ -107,18 +118,33 @@ export const QueryBuilder = ({
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
<DropdownMenu.Item onClick={handleAddRuleGroup}>Add rule group</DropdownMenu.Item>
|
<DropdownMenu.Item onClick={handleAddRuleGroup}>Add rule group</DropdownMenu.Item>
|
||||||
|
|
||||||
{level > 0 && (
|
{level > 0 && (
|
||||||
<DropdownMenu.Item onClick={handleDeleteRuleGroup}>
|
<DropdownMenu.Item onClick={handleDeleteRuleGroup}>
|
||||||
Remove rule group
|
Remove rule group
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
)}
|
)}
|
||||||
|
{level === 0 && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Divider />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$danger
|
||||||
|
onClick={onResetFilters}
|
||||||
|
>
|
||||||
|
Reset to default
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$danger
|
||||||
|
onClick={onClearFilters}
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Group>
|
</Group>
|
||||||
<AnimatePresence
|
<AnimatePresence initial={false}>
|
||||||
key="advanced-filter-option"
|
|
||||||
initial={false}
|
|
||||||
>
|
|
||||||
{data?.rules?.map((rule: QueryBuilderRule) => (
|
{data?.rules?.map((rule: QueryBuilderRule) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={rule.uniqueId}
|
key={rule.uniqueId}
|
||||||
|
@ -131,9 +157,9 @@ export const QueryBuilder = ({
|
||||||
data={rule}
|
data={rule}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
groupIndex={groupIndex || []}
|
groupIndex={groupIndex || []}
|
||||||
// groupValue={groupValue}
|
|
||||||
level={level}
|
level={level}
|
||||||
noRemove={data?.rules?.length === 1}
|
noRemove={data?.rules?.length === 1}
|
||||||
|
operators={operators}
|
||||||
onChangeField={onChangeField}
|
onChangeField={onChangeField}
|
||||||
onChangeOperator={onChangeOperator}
|
onChangeOperator={onChangeOperator}
|
||||||
onChangeValue={onChangeValue}
|
onChangeValue={onChangeValue}
|
||||||
|
@ -143,10 +169,7 @@ export const QueryBuilder = ({
|
||||||
))}
|
))}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
{data?.group && (
|
{data?.group && (
|
||||||
<AnimatePresence
|
<AnimatePresence initial={false}>
|
||||||
key="advanced-filter-group"
|
|
||||||
initial={false}
|
|
||||||
>
|
|
||||||
{data.group?.map((group: QueryBuilderGroup, index: number) => (
|
{data.group?.map((group: QueryBuilderGroup, index: number) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={group.uniqueId}
|
key={group.uniqueId}
|
||||||
|
@ -160,6 +183,7 @@ export const QueryBuilder = ({
|
||||||
filters={filters}
|
filters={filters}
|
||||||
groupIndex={[...(groupIndex || []), index]}
|
groupIndex={[...(groupIndex || []), index]}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
|
operators={operators}
|
||||||
uniqueId={group.uniqueId}
|
uniqueId={group.uniqueId}
|
||||||
onAddRule={onAddRule}
|
onAddRule={onAddRule}
|
||||||
onAddRuleGroup={onAddRuleGroup}
|
onAddRuleGroup={onAddRuleGroup}
|
||||||
|
@ -167,8 +191,10 @@ export const QueryBuilder = ({
|
||||||
onChangeOperator={onChangeOperator}
|
onChangeOperator={onChangeOperator}
|
||||||
onChangeType={onChangeType}
|
onChangeType={onChangeType}
|
||||||
onChangeValue={onChangeValue}
|
onChangeValue={onChangeValue}
|
||||||
|
onClearFilters={onClearFilters}
|
||||||
onDeleteRule={onDeleteRule}
|
onDeleteRule={onDeleteRule}
|
||||||
onDeleteRuleGroup={onDeleteRuleGroup}
|
onDeleteRuleGroup={onDeleteRuleGroup}
|
||||||
|
onResetFilters={onResetFilters}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,27 +1,11 @@
|
||||||
import { Group } from '@mantine/core';
|
import { Group } from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
import { useState } from 'react';
|
||||||
import { RiSubtractLine } from 'react-icons/ri';
|
import { RiSubtractLine } from 'react-icons/ri';
|
||||||
import { Button } from '/@/renderer/components/button';
|
import { Button } from '/@/renderer/components/button';
|
||||||
import { TextInput } from '/@/renderer/components/input';
|
import { NumberInput, TextInput } from '/@/renderer/components/input';
|
||||||
import { Select } from '/@/renderer/components/select';
|
import { Select } from '/@/renderer/components/select';
|
||||||
import { QueryBuilderRule } from '/@/renderer/types';
|
import { QueryBuilderRule } from '/@/renderer/types';
|
||||||
|
|
||||||
const operators = [
|
|
||||||
{ label: 'is', value: 'is' },
|
|
||||||
{ label: 'is not', value: 'isNot' },
|
|
||||||
{ label: 'is greater than', value: 'gt' },
|
|
||||||
{ label: 'is less than', value: 'lt' },
|
|
||||||
{ label: 'contains', value: 'contains' },
|
|
||||||
{ label: 'does not contain', value: 'notContains' },
|
|
||||||
{ label: 'starts with', value: 'startsWith' },
|
|
||||||
{ label: 'ends with', value: 'endsWith' },
|
|
||||||
{ label: 'is in the range', value: 'inTheRange' },
|
|
||||||
{ label: 'before', value: 'before' },
|
|
||||||
{ label: 'after', value: 'after' },
|
|
||||||
{ label: 'is in the last', value: 'inTheLast' },
|
|
||||||
{ label: 'is not in the last', value: 'notInTheLast' },
|
|
||||||
];
|
|
||||||
|
|
||||||
type DeleteArgs = {
|
type DeleteArgs = {
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
|
@ -30,22 +14,98 @@ type DeleteArgs = {
|
||||||
|
|
||||||
interface QueryOptionProps {
|
interface QueryOptionProps {
|
||||||
data: QueryBuilderRule;
|
data: QueryBuilderRule;
|
||||||
filters: { label: string; value: string }[];
|
filters: { label: string; type: string; value: string }[];
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
// groupValue: string;
|
|
||||||
level: number;
|
level: number;
|
||||||
noRemove: boolean;
|
noRemove: boolean;
|
||||||
onChangeField: (args: any) => void;
|
onChangeField: (args: any) => void;
|
||||||
onChangeOperator: (args: any) => void;
|
onChangeOperator: (args: any) => void;
|
||||||
onChangeValue: (args: any) => void;
|
onChangeValue: (args: any) => void;
|
||||||
onDeleteRule: (args: DeleteArgs) => 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 }[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QueryValueInput = ({ onChange, type, ...props }: any) => {
|
||||||
|
const [numberRange, setNumberRange] = useState([0, 0]);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
size="sm"
|
||||||
|
onChange={onChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'number':
|
||||||
|
return (
|
||||||
|
<NumberInput
|
||||||
|
size="sm"
|
||||||
|
onChange={onChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'date':
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
size="sm"
|
||||||
|
onChange={onChange}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'dateRange':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NumberInput
|
||||||
|
{...props}
|
||||||
|
defaultValue={props.defaultValue?.[0]}
|
||||||
|
maxWidth={81}
|
||||||
|
width="10%"
|
||||||
|
onChange={(e) => {
|
||||||
|
const newRange = [e || 0, numberRange[1]];
|
||||||
|
setNumberRange(newRange);
|
||||||
|
onChange(newRange);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
{...props}
|
||||||
|
defaultValue={props.defaultValue?.[1]}
|
||||||
|
maxWidth={81}
|
||||||
|
width="10%"
|
||||||
|
onChange={(e) => {
|
||||||
|
const newRange = [numberRange[0], e || 0];
|
||||||
|
setNumberRange(newRange);
|
||||||
|
onChange(newRange);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
data={[]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const QueryBuilderOption = ({
|
export const QueryBuilderOption = ({
|
||||||
data,
|
data,
|
||||||
filters,
|
filters,
|
||||||
level,
|
level,
|
||||||
onDeleteRule,
|
onDeleteRule,
|
||||||
|
operators,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
noRemove,
|
noRemove,
|
||||||
onChangeField,
|
onChangeField,
|
||||||
|
@ -67,8 +127,6 @@ export const QueryBuilderOption = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeValue = (e: any) => {
|
const handleChangeValue = (e: any) => {
|
||||||
console.log('e', e);
|
|
||||||
|
|
||||||
const isDirectValue =
|
const isDirectValue =
|
||||||
typeof e === 'string' ||
|
typeof e === 'string' ||
|
||||||
typeof e === 'number' ||
|
typeof e === 'number' ||
|
||||||
|
@ -84,14 +142,25 @@ export const QueryBuilderOption = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDate = e instanceof Date;
|
// const isDate = e instanceof Date;
|
||||||
|
|
||||||
if (isDate) {
|
// if (isDate) {
|
||||||
|
// return onChangeValue({
|
||||||
|
// groupIndex,
|
||||||
|
// level,
|
||||||
|
// uniqueId,
|
||||||
|
// value: dayjs(e).format('YYYY-MM-DD'),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
const isArray = Array.isArray(e);
|
||||||
|
|
||||||
|
if (isArray) {
|
||||||
return onChangeValue({
|
return onChangeValue({
|
||||||
groupIndex,
|
groupIndex,
|
||||||
level,
|
level,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
value: dayjs(e).format('YYYY-MM-DD'),
|
value: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,233 +172,8 @@ export const QueryBuilderOption = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// const filterOperatorMap = {
|
const fieldType = filters.find((f) => f.value === field)?.type;
|
||||||
// date: (
|
const operatorsByFieldType = operators[fieldType as keyof typeof operators];
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={DATE_FILTER_OPTIONS_DATA}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={operator}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeOperator}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// id: (
|
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={ID_FILTER_OPTIONS_DATA}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={operator}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeOperator}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// number: (
|
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={NUMBER_FILTER_OPTIONS_DATA}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={operator}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeOperator}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// string: (
|
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={STRING_FILTER_OPTIONS_DATA}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={operator}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeOperator}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const filterInputValueMap = {
|
|
||||||
// 'albumArtists.genres.id': (
|
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={genresData?.albumArtist || []}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albumArtists.name': (
|
|
||||||
// <TextInput
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albumArtists.ratings.value': (
|
|
||||||
// <NumberInput
|
|
||||||
// max={5}
|
|
||||||
// maxWidth={175}
|
|
||||||
// min={0}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albums.dateAdded': (
|
|
||||||
// <DatePicker
|
|
||||||
// initialLevel="year"
|
|
||||||
// maxDate={dayjs(new Date()).year(3000).toDate()}
|
|
||||||
// maxWidth={175}
|
|
||||||
// minDate={dayjs(new Date()).year(1950).toDate()}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albums.genres.id': (
|
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={genresData?.album || []}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albums.name': (
|
|
||||||
// <TextInput
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albums.playCount': (
|
|
||||||
// <NumberInput
|
|
||||||
// maxWidth={175}
|
|
||||||
// min={0}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={(e) => handleChangeValue(e)}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albums.ratings.value': (
|
|
||||||
// <NumberInput
|
|
||||||
// max={5}
|
|
||||||
// maxWidth={175}
|
|
||||||
// min={0}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albums.releaseDate': (
|
|
||||||
// <DatePicker
|
|
||||||
// initialLevel="year"
|
|
||||||
// maxDate={dayjs(new Date()).year(3000).toDate()}
|
|
||||||
// maxWidth={175}
|
|
||||||
// minDate={dayjs(new Date()).year(1950).toDate()}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'albums.releaseYear': (
|
|
||||||
// <NumberInput
|
|
||||||
// maxWidth={175}
|
|
||||||
// min={0}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'artists.genres.id': (
|
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={genresData?.artist || []}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'artists.name': (
|
|
||||||
// <TextInput
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'artists.ratings.value': (
|
|
||||||
// <NumberInput
|
|
||||||
// max={5}
|
|
||||||
// maxWidth={175}
|
|
||||||
// min={0}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'songs.genres.id': (
|
|
||||||
// <Select
|
|
||||||
// searchable
|
|
||||||
// data={genresData?.song || []}
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'songs.name': (
|
|
||||||
// <TextInput
|
|
||||||
// maxWidth={175}
|
|
||||||
// size="xs"
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'songs.playCount': (
|
|
||||||
// <NumberInput
|
|
||||||
// maxWidth={175}
|
|
||||||
// min={0}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// 'songs.ratings.value': (
|
|
||||||
// <NumberInput
|
|
||||||
// max={5}
|
|
||||||
// maxWidth={175}
|
|
||||||
// min={0}
|
|
||||||
// size="xs"
|
|
||||||
// value={value}
|
|
||||||
// width="20%"
|
|
||||||
// onChange={handleChangeValue}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
// };
|
|
||||||
|
|
||||||
const ml = (level + 1) * 10;
|
const ml = (level + 1) * 10;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -337,7 +181,7 @@ export const QueryBuilderOption = ({
|
||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
data={filters}
|
data={filters}
|
||||||
maxWidth={175}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
value={field}
|
value={field}
|
||||||
width="20%"
|
width="20%"
|
||||||
|
@ -345,19 +189,20 @@ export const QueryBuilderOption = ({
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
data={operators}
|
data={operatorsByFieldType || []}
|
||||||
disabled={!field}
|
disabled={!field}
|
||||||
maxWidth={175}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
value={operator}
|
value={operator}
|
||||||
width="20%"
|
width="20%"
|
||||||
onChange={handleChangeOperator}
|
onChange={handleChangeOperator}
|
||||||
/>
|
/>
|
||||||
{field ? (
|
{field ? (
|
||||||
<TextInput
|
<QueryValueInput
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
maxWidth={175}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
type={operator === 'inTheRange' ? 'dateRange' : fieldType}
|
||||||
width="20%"
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
|
@ -365,14 +210,12 @@ export const QueryBuilderOption = ({
|
||||||
<TextInput
|
<TextInput
|
||||||
disabled
|
disabled
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
maxWidth={175}
|
maxWidth={170}
|
||||||
size="sm"
|
size="sm"
|
||||||
width="20%"
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* // filterOperatorMap[ // OPTIONS_MAP[field as keyof typeof OPTIONS_MAP].type as keyof typeof
|
|
||||||
filterOperatorMap // ] */}
|
|
||||||
<Button
|
<Button
|
||||||
disabled={noRemove}
|
disabled={noRemove}
|
||||||
px={5}
|
px={5}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useParams } from 'react-router';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
import { PlaylistSongListQuery, ServerType, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
@ -79,10 +79,14 @@ const HeaderItems = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface PlaylistDetailHeaderProps {
|
interface PlaylistDetailHeaderProps {
|
||||||
|
handleToggleShowQueryBuilder: () => void;
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailSongListHeader = ({ tableRef }: PlaylistDetailHeaderProps) => {
|
export const PlaylistDetailSongListHeader = ({
|
||||||
|
tableRef,
|
||||||
|
handleToggleShowQueryBuilder,
|
||||||
|
}: PlaylistDetailHeaderProps) => {
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
@ -365,18 +369,36 @@ export const PlaylistDetailSongListHeader = ({ tableRef }: PlaylistDetailHeaderP
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
<DropdownMenu.Item onClick={() => handlePlay(Play.NOW)}>Play</DropdownMenu.Item>
|
<DropdownMenu.Item onClick={() => handlePlay(Play.NOW)}>Play</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => handlePlay(Play.LAST)}>
|
||||||
|
Add to queue (last)
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item onClick={() => handlePlay(Play.NEXT)}>
|
||||||
|
Add to queue (next)
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
disabled
|
disabled
|
||||||
onClick={() => handlePlay(Play.NEXT)}
|
onClick={() => handlePlay(Play.LAST)}
|
||||||
>
|
>
|
||||||
Add to queue (last)
|
Edit playlist
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
disabled
|
disabled
|
||||||
onClick={() => handlePlay(Play.LAST)}
|
onClick={() => handlePlay(Play.LAST)}
|
||||||
>
|
>
|
||||||
Add to queue (next)
|
Delete playlist
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
{server?.type === ServerType.NAVIDROME && !detailQuery?.data?.rules && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Divider />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$danger
|
||||||
|
onClick={handleToggleShowQueryBuilder}
|
||||||
|
>
|
||||||
|
Toggle smart playlist editor
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
@ -1,17 +1,33 @@
|
||||||
import { useState, useImperativeHandle, forwardRef } from 'react';
|
import { useState } from 'react';
|
||||||
import { Flex, Group, ScrollArea } from '@mantine/core';
|
import { Group } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
import clone from 'lodash/clone';
|
import clone from 'lodash/clone';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import setWith from 'lodash/setWith';
|
import setWith from 'lodash/setWith';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { NDSongQueryFields } from '/@/renderer/api/navidrome.types';
|
import {
|
||||||
import { Button, DropdownMenu, NumberInput, QueryBuilder } from '/@/renderer/components';
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
MotionFlex,
|
||||||
|
NumberInput,
|
||||||
|
QueryBuilder,
|
||||||
|
ScrollArea,
|
||||||
|
Select,
|
||||||
|
} from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
convertNDQueryToQueryGroup,
|
convertNDQueryToQueryGroup,
|
||||||
convertQueryGroupToNDQuery,
|
convertQueryGroupToNDQuery,
|
||||||
} from '/@/renderer/features/playlists/utils';
|
} from '/@/renderer/features/playlists/utils';
|
||||||
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
|
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
|
||||||
import { RiMore2Fill } from 'react-icons/ri';
|
import { RiMore2Fill, RiSaveLine } from 'react-icons/ri';
|
||||||
|
import { SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||||
|
import {
|
||||||
|
NDSongQueryBooleanOperators,
|
||||||
|
NDSongQueryDateOperators,
|
||||||
|
NDSongQueryFields,
|
||||||
|
NDSongQueryNumberOperators,
|
||||||
|
NDSongQueryStringOperators,
|
||||||
|
} from '/@/renderer/api/navidrome.types';
|
||||||
|
|
||||||
type AddArgs = {
|
type AddArgs = {
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
|
@ -25,322 +41,410 @@ type DeleteArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PlaylistQueryBuilderProps {
|
interface PlaylistQueryBuilderProps {
|
||||||
onSave: (parsedFilter: any) => void;
|
isSaving?: boolean;
|
||||||
onSaveAs: (parsedFilter: any) => void;
|
limit?: number;
|
||||||
|
onSave: (
|
||||||
|
parsedFilter: any,
|
||||||
|
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
|
||||||
|
) => void;
|
||||||
|
onSaveAs: (
|
||||||
|
parsedFilter: any,
|
||||||
|
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
|
||||||
|
) => void;
|
||||||
query: any;
|
query: any;
|
||||||
|
sortBy: SongListSort;
|
||||||
|
sortOrder: SortOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistQueryBuilder = forwardRef(
|
const DEFAULT_QUERY = {
|
||||||
({ query, onSave, onSaveAs }: PlaylistQueryBuilderProps, ref) => {
|
group: [],
|
||||||
const [filters, setFilters] = useState<any>(
|
rules: [
|
||||||
convertNDQueryToQueryGroup(query) || {
|
{
|
||||||
all: [],
|
field: '',
|
||||||
},
|
operator: '',
|
||||||
);
|
uniqueId: nanoid(),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'all' as 'all' | 'any',
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
};
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
export const PlaylistQueryBuilder = ({
|
||||||
reset() {
|
sortOrder,
|
||||||
setFilters({
|
sortBy,
|
||||||
all: [],
|
limit,
|
||||||
});
|
isSaving,
|
||||||
},
|
query,
|
||||||
}));
|
onSave,
|
||||||
|
onSaveAs,
|
||||||
|
}: PlaylistQueryBuilderProps) => {
|
||||||
|
const [filters, setFilters] = useState<QueryBuilderGroup>(
|
||||||
|
query ? convertNDQueryToQueryGroup(query) : DEFAULT_QUERY,
|
||||||
|
);
|
||||||
|
|
||||||
const setFilterHandler = (newFilters: QueryBuilderGroup) => {
|
const extraFiltersForm = useForm({
|
||||||
setFilters(newFilters);
|
initialValues: {
|
||||||
// onSave(newFilters);
|
limit,
|
||||||
};
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleResetFilters = () => {
|
||||||
onSave(convertQueryGroupToNDQuery(filters));
|
if (query) {
|
||||||
};
|
setFilters(convertNDQueryToQueryGroup(query));
|
||||||
|
} else {
|
||||||
|
setFilters(DEFAULT_QUERY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveAs = () => {
|
const handleClearFilters = () => {
|
||||||
onSaveAs(convertQueryGroupToNDQuery(filters));
|
setFilters(DEFAULT_QUERY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddRuleGroup = (args: AddArgs) => {
|
const setFilterHandler = (newFilters: QueryBuilderGroup) => {
|
||||||
const { level, groupIndex } = args;
|
setFilters(newFilters);
|
||||||
const filtersCopy = clone(filters);
|
};
|
||||||
|
|
||||||
const getPath = (level: number) => {
|
const handleSave = () => {
|
||||||
if (level === 0) return 'group';
|
onSave(convertQueryGroupToNDQuery(filters), extraFiltersForm.values);
|
||||||
|
};
|
||||||
|
|
||||||
const str = [];
|
const handleSaveAs = () => {
|
||||||
for (const index of groupIndex) {
|
onSaveAs(convertQueryGroupToNDQuery(filters), extraFiltersForm.values);
|
||||||
str.push(`group[${index}]`);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return `${str.join('.')}.group`;
|
const handleAddRuleGroup = (args: AddArgs) => {
|
||||||
};
|
const { level, groupIndex } = args;
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
const path = getPath(level);
|
const getPath = (level: number) => {
|
||||||
const updatedFilters = setWith(
|
if (level === 0) return 'group';
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
[
|
|
||||||
...get(filtersCopy, path),
|
|
||||||
{
|
|
||||||
group: [],
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
field: '',
|
|
||||||
operator: '',
|
|
||||||
uniqueId: nanoid(),
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
type: 'any',
|
|
||||||
uniqueId: nanoid(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
clone,
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilterHandler(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteRuleGroup = (args: DeleteArgs) => {
|
|
||||||
const { uniqueId, level, groupIndex } = args;
|
|
||||||
const filtersCopy = clone(filters);
|
|
||||||
|
|
||||||
const getPath = (level: number) => {
|
|
||||||
if (level === 0) return 'group';
|
|
||||||
|
|
||||||
const str = [];
|
|
||||||
for (let i = 0; i < groupIndex.length; i += 1) {
|
|
||||||
if (i !== groupIndex.length - 1) {
|
|
||||||
str.push(`group[${groupIndex[i]}]`);
|
|
||||||
} else {
|
|
||||||
str.push(`group`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${str.join('.')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const path = getPath(level);
|
|
||||||
|
|
||||||
const updatedFilters = setWith(
|
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
[
|
|
||||||
...get(filtersCopy, path).filter(
|
|
||||||
(group: QueryBuilderGroup) => group.uniqueId !== uniqueId,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
clone,
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilterHandler(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRulePath = (level: number, groupIndex: number[]) => {
|
|
||||||
if (level === 0) return 'rules';
|
|
||||||
|
|
||||||
const str = [];
|
const str = [];
|
||||||
for (const index of groupIndex) {
|
for (const index of groupIndex) {
|
||||||
str.push(`group[${index}]`);
|
str.push(`group[${index}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${str.join('.')}.rules`;
|
return `${str.join('.')}.group`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddRule = (args: AddArgs) => {
|
const path = getPath(level);
|
||||||
const { level, groupIndex } = args;
|
const updatedFilters = setWith(
|
||||||
const filtersCopy = clone(filters);
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
[
|
||||||
|
...get(filtersCopy, path),
|
||||||
|
{
|
||||||
|
group: [],
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
field: '',
|
||||||
|
operator: '',
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'any',
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
clone,
|
||||||
|
);
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
setFilterHandler(updatedFilters);
|
||||||
const updatedFilters = setWith(
|
};
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
[
|
|
||||||
...get(filtersCopy, path),
|
|
||||||
{
|
|
||||||
field: 'title',
|
|
||||||
operator: 'contains',
|
|
||||||
uniqueId: nanoid(),
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
clone,
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilterHandler(updatedFilters);
|
const handleDeleteRuleGroup = (args: DeleteArgs) => {
|
||||||
};
|
const { uniqueId, level, groupIndex } = args;
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
const handleDeleteRule = (args: DeleteArgs) => {
|
const getPath = (level: number) => {
|
||||||
const { uniqueId, level, groupIndex } = args;
|
if (level === 0) return 'group';
|
||||||
const filtersCopy = clone(filters);
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
const str = [];
|
||||||
const updatedFilters = setWith(
|
for (let i = 0; i < groupIndex.length; i += 1) {
|
||||||
filtersCopy,
|
if (i !== groupIndex.length - 1) {
|
||||||
path,
|
str.push(`group[${groupIndex[i]}]`);
|
||||||
get(filtersCopy, path).filter((rule: QueryBuilderRule) => rule.uniqueId !== uniqueId),
|
} else {
|
||||||
clone,
|
str.push(`group`);
|
||||||
);
|
}
|
||||||
|
|
||||||
setFilterHandler(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeField = (args: any) => {
|
|
||||||
const { uniqueId, level, groupIndex, value } = args;
|
|
||||||
const filtersCopy = clone(filters);
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
|
||||||
const updatedFilters = setWith(
|
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
get(filtersCopy, path).map((rule: QueryBuilderGroup) => {
|
|
||||||
if (rule.uniqueId !== uniqueId) return rule;
|
|
||||||
// const defaultOperator = FILTER_OPTIONS_DATA.find(
|
|
||||||
// (option) => option.value === value,
|
|
||||||
// )?.default;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rule,
|
|
||||||
field: value,
|
|
||||||
operator: '',
|
|
||||||
value: '',
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
clone,
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilterHandler(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeType = (args: any) => {
|
|
||||||
const { level, groupIndex, value } = args;
|
|
||||||
|
|
||||||
const filtersCopy = clone(filters);
|
|
||||||
|
|
||||||
if (level === 0) {
|
|
||||||
return setFilterHandler({ ...filtersCopy, type: value });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTypePath = () => {
|
return `${str.join('.')}`;
|
||||||
const str = [];
|
|
||||||
for (let i = 0; i < groupIndex.length; i += 1) {
|
|
||||||
str.push(`group[${groupIndex[i]}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${str.join('.')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const path = getTypePath();
|
|
||||||
const updatedFilters = setWith(
|
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
{
|
|
||||||
...get(filtersCopy, path),
|
|
||||||
type: value,
|
|
||||||
},
|
|
||||||
clone,
|
|
||||||
);
|
|
||||||
|
|
||||||
return setFilterHandler(updatedFilters);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeOperator = (args: any) => {
|
const path = getPath(level);
|
||||||
const { uniqueId, level, groupIndex, value } = args;
|
|
||||||
const filtersCopy = clone(filters);
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
const updatedFilters = setWith(
|
||||||
const updatedFilters = setWith(
|
filtersCopy,
|
||||||
filtersCopy,
|
path,
|
||||||
path,
|
[...get(filtersCopy, path).filter((group: QueryBuilderGroup) => group.uniqueId !== uniqueId)],
|
||||||
get(filtersCopy, path).map((rule: QueryBuilderRule) => {
|
clone,
|
||||||
if (rule.uniqueId !== uniqueId) return rule;
|
|
||||||
return {
|
|
||||||
...rule,
|
|
||||||
operator: value,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
clone,
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilterHandler(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeValue = (args: any) => {
|
|
||||||
const { uniqueId, level, groupIndex, value } = args;
|
|
||||||
const filtersCopy = clone(filters);
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
|
||||||
console.log('path', path);
|
|
||||||
const updatedFilters = setWith(
|
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
get(filtersCopy, path).map((rule: QueryBuilderRule) => {
|
|
||||||
if (rule.uniqueId !== uniqueId) return rule;
|
|
||||||
return {
|
|
||||||
...rule,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
clone,
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilterHandler(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
h="100%"
|
|
||||||
justify="space-between"
|
|
||||||
>
|
|
||||||
<ScrollArea h="100%">
|
|
||||||
<QueryBuilder
|
|
||||||
data={filters}
|
|
||||||
filters={NDSongQueryFields}
|
|
||||||
groupIndex={[]}
|
|
||||||
level={0}
|
|
||||||
uniqueId={filters.uniqueId}
|
|
||||||
onAddRule={handleAddRule}
|
|
||||||
onAddRuleGroup={handleAddRuleGroup}
|
|
||||||
onChangeField={handleChangeField}
|
|
||||||
onChangeOperator={handleChangeOperator}
|
|
||||||
onChangeType={handleChangeType}
|
|
||||||
onChangeValue={handleChangeValue}
|
|
||||||
onDeleteRule={handleDeleteRule}
|
|
||||||
onDeleteRuleGroup={handleDeleteRuleGroup}
|
|
||||||
/>
|
|
||||||
</ScrollArea>
|
|
||||||
<Group
|
|
||||||
align="flex-end"
|
|
||||||
p="1rem 1rem 0"
|
|
||||||
position="apart"
|
|
||||||
>
|
|
||||||
<NumberInput
|
|
||||||
label="Limit to"
|
|
||||||
width={75}
|
|
||||||
/>
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
onClick={handleSave}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<DropdownMenu position="bottom-end">
|
|
||||||
<DropdownMenu.Target>
|
|
||||||
<Button
|
|
||||||
p="0.5em"
|
|
||||||
variant="default"
|
|
||||||
>
|
|
||||||
<RiMore2Fill size={15} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu.Target>
|
|
||||||
<DropdownMenu.Dropdown>
|
|
||||||
<DropdownMenu.Item onClick={handleSaveAs}>Save as</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Flex>
|
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRulePath = (level: number, groupIndex: number[]) => {
|
||||||
|
if (level === 0) return 'rules';
|
||||||
|
|
||||||
|
const str = [];
|
||||||
|
for (const index of groupIndex) {
|
||||||
|
str.push(`group[${index}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str.join('.')}.rules`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddRule = (args: AddArgs) => {
|
||||||
|
const { level, groupIndex } = args;
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
|
const path = getRulePath(level, groupIndex);
|
||||||
|
const updatedFilters = setWith(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
[
|
||||||
|
...get(filtersCopy, path),
|
||||||
|
{
|
||||||
|
field: '',
|
||||||
|
operator: '',
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
clone,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteRule = (args: DeleteArgs) => {
|
||||||
|
const { uniqueId, level, groupIndex } = args;
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
|
const path = getRulePath(level, groupIndex);
|
||||||
|
const updatedFilters = setWith(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
get(filtersCopy, path).filter((rule: QueryBuilderRule) => rule.uniqueId !== uniqueId),
|
||||||
|
clone,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeField = (args: any) => {
|
||||||
|
const { uniqueId, level, groupIndex, value } = args;
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
|
const path = getRulePath(level, groupIndex);
|
||||||
|
const updatedFilters = setWith(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
get(filtersCopy, path).map((rule: QueryBuilderGroup) => {
|
||||||
|
if (rule.uniqueId !== uniqueId) return rule;
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
field: value,
|
||||||
|
operator: '',
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
clone,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeType = (args: any) => {
|
||||||
|
const { level, groupIndex, value } = args;
|
||||||
|
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
|
if (level === 0) {
|
||||||
|
return setFilterHandler({ ...filtersCopy, type: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypePath = () => {
|
||||||
|
const str = [];
|
||||||
|
for (let i = 0; i < groupIndex.length; i += 1) {
|
||||||
|
str.push(`group[${groupIndex[i]}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str.join('.')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = getTypePath();
|
||||||
|
const updatedFilters = setWith(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
{
|
||||||
|
...get(filtersCopy, path),
|
||||||
|
type: value,
|
||||||
|
},
|
||||||
|
clone,
|
||||||
|
);
|
||||||
|
|
||||||
|
return setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeOperator = (args: any) => {
|
||||||
|
const { uniqueId, level, groupIndex, value } = args;
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
|
const path = getRulePath(level, groupIndex);
|
||||||
|
const updatedFilters = setWith(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
get(filtersCopy, path).map((rule: QueryBuilderRule) => {
|
||||||
|
if (rule.uniqueId !== uniqueId) return rule;
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
operator: value,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
clone,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeValue = (args: any) => {
|
||||||
|
const { uniqueId, level, groupIndex, value } = args;
|
||||||
|
const filtersCopy = clone(filters);
|
||||||
|
|
||||||
|
const path = getRulePath(level, groupIndex);
|
||||||
|
const updatedFilters = setWith(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
get(filtersCopy, path).map((rule: QueryBuilderRule) => {
|
||||||
|
if (rule.uniqueId !== uniqueId) return rule;
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
clone,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortOptions = [{ label: 'Random', type: 'string', value: 'random' }, ...NDSongQueryFields];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionFlex
|
||||||
|
direction="column"
|
||||||
|
h="calc(100% - 3rem)"
|
||||||
|
justify="space-between"
|
||||||
|
>
|
||||||
|
<ScrollArea
|
||||||
|
h="100%"
|
||||||
|
p="1rem"
|
||||||
|
>
|
||||||
|
<QueryBuilder
|
||||||
|
data={filters}
|
||||||
|
filters={NDSongQueryFields}
|
||||||
|
groupIndex={[]}
|
||||||
|
level={0}
|
||||||
|
operators={{
|
||||||
|
boolean: NDSongQueryBooleanOperators,
|
||||||
|
date: NDSongQueryDateOperators,
|
||||||
|
number: NDSongQueryNumberOperators,
|
||||||
|
string: NDSongQueryStringOperators,
|
||||||
|
}}
|
||||||
|
uniqueId={filters.uniqueId}
|
||||||
|
onAddRule={handleAddRule}
|
||||||
|
onAddRuleGroup={handleAddRuleGroup}
|
||||||
|
onChangeField={handleChangeField}
|
||||||
|
onChangeOperator={handleChangeOperator}
|
||||||
|
onChangeType={handleChangeType}
|
||||||
|
onChangeValue={handleChangeValue}
|
||||||
|
onClearFilters={handleClearFilters}
|
||||||
|
onDeleteRule={handleDeleteRule}
|
||||||
|
onDeleteRuleGroup={handleDeleteRuleGroup}
|
||||||
|
onResetFilters={handleResetFilters}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
<Group
|
||||||
|
noWrap
|
||||||
|
align="flex-end"
|
||||||
|
p="1rem"
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
noWrap
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
searchable
|
||||||
|
data={sortOptions}
|
||||||
|
label="Sort"
|
||||||
|
maxWidth="20%"
|
||||||
|
width={125}
|
||||||
|
{...extraFiltersForm.getInputProps('sortBy')}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
data={[
|
||||||
|
{
|
||||||
|
label: 'Ascending',
|
||||||
|
value: 'asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Descending',
|
||||||
|
value: 'desc',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
label="Order"
|
||||||
|
maxWidth="20%"
|
||||||
|
width={125}
|
||||||
|
{...extraFiltersForm.getInputProps('sortOrder')}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Limit"
|
||||||
|
maxWidth="20%"
|
||||||
|
width={75}
|
||||||
|
{...extraFiltersForm.getInputProps('limit')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group noWrap>
|
||||||
|
<Button
|
||||||
|
loading={isSaving}
|
||||||
|
variant="filled"
|
||||||
|
onClick={handleSaveAs}
|
||||||
|
>
|
||||||
|
Save as
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu position="bottom-end">
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
disabled={isSaving}
|
||||||
|
p="0.5em"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<RiMore2Fill size={15} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$danger
|
||||||
|
rightSection={
|
||||||
|
<RiSaveLine
|
||||||
|
color="var(--danger-color)"
|
||||||
|
size={15}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Save and replace
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</MotionFlex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Stack } from '@mantine/core';
|
import { Group, Stack } from '@mantine/core';
|
||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
|
import { AnimatePresence, motion, Variants } from 'framer-motion';
|
||||||
|
import { RiArrowDownSLine, RiArrowUpSLine } from 'react-icons/ri';
|
||||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
import { generatePath, useNavigate, useParams } from 'react-router';
|
||||||
import { PlaylistDetailSongListContent } from '../components/playlist-detail-song-list-content';
|
import { PlaylistDetailSongListContent } from '../components/playlist-detail-song-list-content';
|
||||||
import { PlaylistDetailSongListHeader } from '../components/playlist-detail-song-list-header';
|
import { PlaylistDetailSongListHeader } from '../components/playlist-detail-song-list-header';
|
||||||
|
@ -11,23 +13,30 @@ import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playli
|
||||||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||||
import { Paper, toast } from '/@/renderer/components';
|
import { Button, Paper, Text, toast } from '/@/renderer/components';
|
||||||
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
||||||
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { ServerType } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const PlaylistDetailSongListRoute = () => {
|
const PlaylistDetailSongListRoute = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
|
const currentServer = useCurrentServer();
|
||||||
|
|
||||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||||
const createPlaylistMutation = useCreatePlaylist();
|
const createPlaylistMutation = useCreatePlaylist();
|
||||||
const deletePlaylistMutation = useDeletePlaylist();
|
const deletePlaylistMutation = useDeletePlaylist();
|
||||||
|
|
||||||
const handleSave = (filter: Record<string, any>) => {
|
const handleSave = (
|
||||||
|
filter: Record<string, any>,
|
||||||
|
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
|
||||||
|
) => {
|
||||||
const rules = {
|
const rules = {
|
||||||
...filter,
|
...filter,
|
||||||
order: 'desc',
|
limit: extraFilters.limit || undefined,
|
||||||
sort: 'year',
|
order: extraFilters.sortOrder || 'desc',
|
||||||
|
sort: extraFilters.sortBy || 'dateAdded',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!detailQuery?.data) return;
|
if (!detailQuery?.data) return;
|
||||||
|
@ -87,28 +96,105 @@ const PlaylistDetailSongListRoute = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const smartPlaylistVariants: Variants = {
|
||||||
|
animate: (custom: { isQueryBuilderExpanded: boolean }) => {
|
||||||
|
return {
|
||||||
|
maxHeight: custom.isQueryBuilderExpanded ? '35vh' : '3.5em',
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
},
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -25,
|
||||||
|
},
|
||||||
|
initial: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -25,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSmartPlaylist =
|
||||||
|
!detailQuery?.isLoading &&
|
||||||
|
detailQuery?.data?.rules &&
|
||||||
|
currentServer?.type === ServerType.NAVIDROME;
|
||||||
|
|
||||||
|
const [showQueryBuilder, setShowQueryBuilder] = useState(false);
|
||||||
|
const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false);
|
||||||
|
|
||||||
|
const handleToggleExpand = () => {
|
||||||
|
setIsQueryBuilderExpanded((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleShowQueryBuilder = () => {
|
||||||
|
setShowQueryBuilder((prev) => !prev);
|
||||||
|
setIsQueryBuilderExpanded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('detailQuery?.data?.rules', detailQuery?.data?.rules);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||||
<Stack
|
<Stack
|
||||||
h="100%"
|
h="100%"
|
||||||
spacing={0}
|
spacing={0}
|
||||||
>
|
>
|
||||||
<PlaylistDetailSongListHeader tableRef={tableRef} />
|
<PlaylistDetailSongListHeader
|
||||||
<Paper
|
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
|
||||||
sx={{
|
tableRef={tableRef}
|
||||||
maxHeight: '35vh',
|
/>
|
||||||
padding: '1rem',
|
<AnimatePresence
|
||||||
}}
|
custom={{ isQueryBuilderExpanded }}
|
||||||
w="100%"
|
initial={false}
|
||||||
>
|
>
|
||||||
{!detailQuery?.isLoading && (
|
{(isSmartPlaylist || showQueryBuilder) && (
|
||||||
<PlaylistQueryBuilder
|
<motion.div
|
||||||
query={detailQuery?.data?.rules || { all: [] }}
|
animate="animate"
|
||||||
onSave={handleSave}
|
custom={{ isQueryBuilderExpanded }}
|
||||||
onSaveAs={handleSaveAs}
|
exit="exit"
|
||||||
/>
|
initial="initial"
|
||||||
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
||||||
|
variants={smartPlaylistVariants}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
h="100%"
|
||||||
|
pos="relative"
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<Group
|
||||||
|
pt="1rem"
|
||||||
|
px="1rem"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
variant="default"
|
||||||
|
onClick={handleToggleExpand}
|
||||||
|
>
|
||||||
|
{isQueryBuilderExpanded ? (
|
||||||
|
<RiArrowUpSLine size={20} />
|
||||||
|
) : (
|
||||||
|
<RiArrowDownSLine size={20} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Text>Query Editor</Text>
|
||||||
|
</Group>
|
||||||
|
<PlaylistQueryBuilder
|
||||||
|
isSaving={createPlaylistMutation?.isLoading}
|
||||||
|
limit={detailQuery?.data?.rules?.limit}
|
||||||
|
query={detailQuery?.data?.rules}
|
||||||
|
sortBy={detailQuery?.data?.rules?.sort || 'year'}
|
||||||
|
sortOrder={detailQuery?.data?.rules?.order || 'desc'}
|
||||||
|
onSave={handleSave}
|
||||||
|
onSaveAs={handleSaveAs}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</AnimatePresence>
|
||||||
<PlaylistDetailSongListContent tableRef={tableRef} />
|
<PlaylistDetailSongListContent tableRef={tableRef} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
|
|
Reference in a new issue