Forward playlist query filters

This commit is contained in:
jeffvli 2023-01-15 21:57:44 -08:00
parent 9a809a61dd
commit e5f478218e

View file

@ -1,4 +1,4 @@
import { useState } from 'react'; import { forwardRef, Ref, useImperativeHandle, useState } from 'react';
import { Group } from '@mantine/core'; import { Group } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import clone from 'lodash/clone'; import clone from 'lodash/clone';
@ -20,7 +20,7 @@ import {
} from '/@/renderer/features/playlists/utils'; } from '/@/renderer/features/playlists/utils';
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types'; import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
import { RiMore2Fill, RiSaveLine } from 'react-icons/ri'; import { RiMore2Fill, RiSaveLine } from 'react-icons/ri';
import { SongListSort, SortOrder } from '/@/renderer/api/types'; import { SongListSort } from '/@/renderer/api/types';
import { import {
NDSongQueryBooleanOperators, NDSongQueryBooleanOperators,
NDSongQueryDateOperators, NDSongQueryDateOperators,
@ -43,17 +43,17 @@ type DeleteArgs = {
interface PlaylistQueryBuilderProps { interface PlaylistQueryBuilderProps {
isSaving?: boolean; isSaving?: boolean;
limit?: number; limit?: number;
onSave: ( onSave?: (
parsedFilter: any, parsedFilter: any,
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string }, extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
) => void; ) => void;
onSaveAs: ( onSaveAs?: (
parsedFilter: any, parsedFilter: any,
extraFilters: { limit?: number; sortBy?: string; sortOrder?: string }, extraFilters: { limit?: number; sortBy?: string; sortOrder?: string },
) => void; ) => void;
query: any; query: any;
sortBy: SongListSort; sortBy: SongListSort;
sortOrder: SortOrder; sortOrder: 'asc' | 'desc';
} }
const DEFAULT_QUERY = { const DEFAULT_QUERY = {
@ -70,381 +70,405 @@ const DEFAULT_QUERY = {
uniqueId: nanoid(), uniqueId: nanoid(),
}; };
export const PlaylistQueryBuilder = ({ export type PlaylistQueryBuilderRef = {
sortOrder, getFilters: () => {
sortBy, extraFilters: {
limit, limit?: number;
isSaving, sortBy?: string;
query, sortOrder?: string;
onSave, };
onSaveAs, filters: QueryBuilderGroup;
}: PlaylistQueryBuilderProps) => { };
const [filters, setFilters] = useState<QueryBuilderGroup>( };
query ? convertNDQueryToQueryGroup(query) : DEFAULT_QUERY,
);
const extraFiltersForm = useForm({ export const PlaylistQueryBuilder = forwardRef(
initialValues: { (
limit, { sortOrder, sortBy, limit, isSaving, query, onSave, onSaveAs }: PlaylistQueryBuilderProps,
sortBy, ref: Ref<PlaylistQueryBuilderRef>,
sortOrder, ) => {
}, const [filters, setFilters] = useState<QueryBuilderGroup>(
}); query ? convertNDQueryToQueryGroup(query) : DEFAULT_QUERY,
);
const handleResetFilters = () => { const extraFiltersForm = useForm({
if (query) { initialValues: {
setFilters(convertNDQueryToQueryGroup(query)); limit,
} else { sortBy,
sortOrder,
},
});
useImperativeHandle(ref, () => ({
getFilters: () => ({
extraFilters: extraFiltersForm.values,
filters,
}),
}));
const handleResetFilters = () => {
if (query) {
setFilters(convertNDQueryToQueryGroup(query));
} else {
setFilters(DEFAULT_QUERY);
}
};
const handleClearFilters = () => {
setFilters(DEFAULT_QUERY); setFilters(DEFAULT_QUERY);
} };
};
const handleClearFilters = () => { const setFilterHandler = (newFilters: QueryBuilderGroup) => {
setFilters(DEFAULT_QUERY); setFilters(newFilters);
}; };
const setFilterHandler = (newFilters: QueryBuilderGroup) => { const handleSave = () => {
setFilters(newFilters); onSave?.(convertQueryGroupToNDQuery(filters), extraFiltersForm.values);
}; };
const handleSave = () => { const handleSaveAs = () => {
onSave(convertQueryGroupToNDQuery(filters), extraFiltersForm.values); onSaveAs?.(convertQueryGroupToNDQuery(filters), extraFiltersForm.values);
}; };
const handleSaveAs = () => { const handleAddRuleGroup = (args: AddArgs) => {
onSaveAs(convertQueryGroupToNDQuery(filters), extraFiltersForm.values); const { level, groupIndex } = args;
}; const filtersCopy = clone(filters);
const handleAddRuleGroup = (args: AddArgs) => { const getPath = (level: number) => {
const { level, groupIndex } = args; if (level === 0) return 'group';
const filtersCopy = clone(filters);
const getPath = (level: number) => { const str = [];
if (level === 0) return 'group'; for (const index of groupIndex) {
str.push(`group[${index}]`);
}
return `${str.join('.')}.group`;
};
const path = getPath(level);
const updatedFilters = setWith(
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('.')}.group`; return `${str.join('.')}.rules`;
}; };
const path = getPath(level); const handleAddRule = (args: AddArgs) => {
const updatedFilters = setWith( const { level, groupIndex } = args;
filtersCopy, const filtersCopy = clone(filters);
path,
[
...get(filtersCopy, path),
{
group: [],
rules: [
{
field: '',
operator: '',
uniqueId: nanoid(),
value: '',
},
],
type: 'any',
uniqueId: nanoid(),
},
],
clone,
);
setFilterHandler(updatedFilters); const path = getRulePath(level, groupIndex);
}; const updatedFilters = setWith(
filtersCopy,
path,
[
...get(filtersCopy, path),
{
field: '',
operator: '',
uniqueId: nanoid(),
value: null,
},
],
clone,
);
const handleDeleteRuleGroup = (args: DeleteArgs) => { setFilterHandler(updatedFilters);
const { uniqueId, level, groupIndex } = args; };
const filtersCopy = clone(filters);
const getPath = (level: number) => { const handleDeleteRule = (args: DeleteArgs) => {
if (level === 0) return 'group'; const { uniqueId, level, groupIndex } = args;
const filtersCopy = clone(filters);
const str = []; const path = getRulePath(level, groupIndex);
for (let i = 0; i < groupIndex.length; i += 1) { const updatedFilters = setWith(
if (i !== groupIndex.length - 1) { 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]}]`); str.push(`group[${groupIndex[i]}]`);
} else {
str.push(`group`);
} }
}
return `${str.join('.')}`; return `${str.join('.')}`;
}; };
const path = getPath(level); const path = getTypePath();
const updatedFilters = setWith(
const updatedFilters = setWith( filtersCopy,
filtersCopy, path,
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 = [];
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: '', ...get(filtersCopy, path),
operator: '', type: value,
uniqueId: nanoid(),
value: null,
}, },
], clone,
clone, );
);
setFilterHandler(updatedFilters); return 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 handleChangeOperator = (args: any) => {
const updatedFilters = setWith( const { uniqueId, level, groupIndex, value } = args;
filtersCopy, const filtersCopy = clone(filters);
path,
{
...get(filtersCopy, path),
type: value,
},
clone,
);
return setFilterHandler(updatedFilters); 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,
);
const handleChangeOperator = (args: any) => { setFilterHandler(updatedFilters);
const { uniqueId, level, groupIndex, value } = args; };
const filtersCopy = clone(filters);
const path = getRulePath(level, groupIndex); const handleChangeValue = (args: any) => {
const updatedFilters = setWith( const { uniqueId, level, groupIndex, value } = args;
filtersCopy, const filtersCopy = clone(filters);
path,
get(filtersCopy, path).map((rule: QueryBuilderRule) => {
if (rule.uniqueId !== uniqueId) return rule;
return {
...rule,
operator: value,
};
}),
clone,
);
setFilterHandler(updatedFilters); 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,
);
const handleChangeValue = (args: any) => { setFilterHandler(updatedFilters);
const { uniqueId, level, groupIndex, value } = args; };
const filtersCopy = clone(filters);
const path = getRulePath(level, groupIndex); const sortOptions = [
const updatedFilters = setWith( { label: 'Random', type: 'string', value: 'random' },
filtersCopy, ...NDSongQueryFields,
path, ];
get(filtersCopy, path).map((rule: QueryBuilderRule) => {
if (rule.uniqueId !== uniqueId) return rule;
return {
...rule,
value,
};
}),
clone,
);
setFilterHandler(updatedFilters); return (
}; <MotionFlex
direction="column"
const sortOptions = [{ label: 'Random', type: 'string', value: 'random' }, ...NDSongQueryFields]; h="calc(100% - 3rem)"
justify="space-between"
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"
> >
<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 <Group
noWrap noWrap
w="100%" align="flex-end"
p="1rem"
position="apart"
> >
<Select <Group
searchable noWrap
data={sortOptions} w="100%"
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 <Select
</Button> searchable
<DropdownMenu position="bottom-end"> data={sortOptions}
<DropdownMenu.Target> 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>
{onSave && onSaveAs && (
<Group noWrap>
<Button <Button
disabled={isSaving} loading={isSaving}
p="0.5em" variant="filled"
variant="default" onClick={handleSaveAs}
> >
<RiMore2Fill size={15} /> Save as
</Button> </Button>
</DropdownMenu.Target> <DropdownMenu position="bottom-end">
<DropdownMenu.Dropdown> <DropdownMenu.Target>
<DropdownMenu.Item <Button
$danger disabled={isSaving}
rightSection={ p="0.5em"
<RiSaveLine variant="default"
color="var(--danger-color)" >
size={15} <RiMore2Fill size={15} />
/> </Button>
} </DropdownMenu.Target>
onClick={handleSave} <DropdownMenu.Dropdown>
> <DropdownMenu.Item
Save and replace $danger
</DropdownMenu.Item> rightSection={
</DropdownMenu.Dropdown> <RiSaveLine
</DropdownMenu> color="var(--danger-color)"
size={15}
/>
}
onClick={handleSave}
>
Save and replace
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
)}
</Group> </Group>
</Group> </MotionFlex>
</MotionFlex> );
); },
}; );