From 65974dbf2847188f23d77d6a68d97f7ca6f361de Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 4 Jan 2023 04:09:24 -0800 Subject: [PATCH] temp --- src/renderer/components/index.ts | 1 + .../components/query-builder/index.tsx | 241 +++++++++++ .../query-builder/query-builder-option.tsx | 380 ++++++++++++++++++ .../components/playlist-query-builder.tsx | 336 ++++++++++++++++ 4 files changed, 958 insertions(+) create mode 100644 src/renderer/components/query-builder/index.tsx create mode 100644 src/renderer/components/query-builder/query-builder-option.tsx create mode 100644 src/renderer/features/playlists/components/playlist-query-builder.tsx diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts index 682217ae..273f7ed1 100644 --- a/src/renderer/components/index.ts +++ b/src/renderer/components/index.ts @@ -31,3 +31,4 @@ export * from './virtual-grid'; export * from './virtual-table'; export * from './motion'; export * from './context-menu'; +export * from './query-builder'; diff --git a/src/renderer/components/query-builder/index.tsx b/src/renderer/components/query-builder/index.tsx new file mode 100644 index 00000000..41d9febe --- /dev/null +++ b/src/renderer/components/query-builder/index.tsx @@ -0,0 +1,241 @@ +import { Group, Stack } from '@mantine/core'; +import { Select } from '/@/renderer/components/select'; +import { FilterGroupType } from '/@/renderer/types'; +import { AnimatePresence, motion } from 'framer-motion'; +import { RiAddLine, RiMore2Line } from 'react-icons/ri'; +import { Button } from '/@/renderer/components/button'; +import { DropdownMenu } from '/@/renderer/components/dropdown-menu'; +import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option'; + +export type AdvancedFilterGroup = { + children: AdvancedFilterGroup[]; + rules: AdvancedFilterRule[]; + type: FilterGroupType; + uniqueId: string; +}; + +export type AdvancedFilterRule = { + field?: string | null; + operator?: string | null; + uniqueId: string; + value?: string | number | Date | undefined | null | any; +}; + +const FILTER_GROUP_OPTIONS_DATA = [ + { + label: 'Match all', + value: 'all', + }, + { + label: 'Match any', + value: 'any', + }, +]; + +// const queryJson = [ +// { +// any: [{ is: { loved: true } }, { gt: { rating: 3 } }], +// }, +// { inTheRange: { year: [1981, 1990] } }, +// ]; + +// const parseQuery = (query: Record[]) => { +// // for (const ruleset in query) { +// // // console.log('key', key); +// // // console.log('query[key]', query[key]); +// // // console.log('Object.keys(query[key])', Object.keys(query[key])); +// // // console.log('Object.values(query[key])', Object.values(query[key])); +// // // console.log('Object.entries(query[key])', Object.entries(query[key])); + +// // const keys = Object.keys(query[ruleset]); +// // } + +// const res = flatMapDeep(query, flatten); +// console.log('res', res); + +// return res; +// }; + +// const OperatorSelect = ({ value, onChange }: any) => { +// const handleChange = (e: any) => { +// onChange(e); +// }; + +// return ( +// + + + + + + + Add rule group + {level > 0 && ( + + Remove rule group + + )} + + + + + {rules.map((rule: AdvancedFilterRule, i: number) => ( + + + + ))} + + {group && ( + + {group.map((group: AdvancedFilterGroup, index: number) => ( + + + + ))} + + )} + + ); +}; diff --git a/src/renderer/components/query-builder/query-builder-option.tsx b/src/renderer/components/query-builder/query-builder-option.tsx new file mode 100644 index 00000000..0098ae3d --- /dev/null +++ b/src/renderer/components/query-builder/query-builder-option.tsx @@ -0,0 +1,380 @@ +import { Group } from '@mantine/core'; +import dayjs from 'dayjs'; +import { RiSubtractLine } from 'react-icons/ri'; +import { Button } from '/@/renderer/components/button'; +import { TextInput } from '/@/renderer/components/input'; +import { Select } from '/@/renderer/components/select'; +import { AdvancedFilterRule } 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 = { + groupIndex: number[]; + groupValue: string; + level: number; + uniqueId: string; +}; + +interface QueryOptionProps { + data: AdvancedFilterRule; + filters: { label: string; value: string }[]; + groupIndex: number[]; + groupValue: string; + level: number; + noRemove: boolean; + onChangeField: (args: any) => void; + onChangeOperator: (args: any) => void; + onChangeValue: (args: any) => void; + onDeleteRule: (args: DeleteArgs) => void; +} + +export const QueryBuilderOption = ({ + data, + filters, + level, + onDeleteRule, + groupIndex, + groupValue, + noRemove, + onChangeField, + onChangeOperator, + onChangeValue, +}: QueryOptionProps) => { + const { field, operator, uniqueId, value } = data; + + const handleDeleteRule = () => { + onDeleteRule({ groupIndex, groupValue, level, uniqueId }); + }; + + const handleChangeField = (e: any) => { + onChangeField({ groupIndex, level, uniqueId, value: e }); + }; + + const handleChangeOperator = (e: any) => { + onChangeOperator({ groupIndex, level, uniqueId, value: e }); + }; + + const handleChangeValue = (e: any) => { + const isDirectValue = + typeof e === 'string' || + typeof e === 'number' || + typeof e === 'undefined' || + typeof e === null; + + if (isDirectValue) { + return onChangeValue({ + groupIndex, + level, + uniqueId, + value: e, + }); + } + + const isDate = e instanceof Date; + + if (isDate) { + return onChangeValue({ + groupIndex, + level, + uniqueId, + value: dayjs(e).format('YYYY-MM-DD'), + }); + } + + return onChangeValue({ + groupIndex, + level, + uniqueId, + value: e.currentTarget.value, + }); + }; + + // const filterOperatorMap = { + // date: ( + // + // ), + // number: ( + // + // ), + // }; + + // const filterInputValueMap = { + // 'albumArtists.genres.id': ( + // + // ), + // 'albums.name': ( + // + // ), + // 'albums.playCount': ( + // handleChangeValue(e)} + // /> + // ), + // 'albums.ratings.value': ( + // + // ), + // 'albums.releaseDate': ( + // + // ), + // 'albums.releaseYear': ( + // + // ), + // 'artists.genres.id': ( + // + // ), + // 'songs.name': ( + // + // ), + // 'songs.playCount': ( + // + // ), + // 'songs.ratings.value': ( + // + // ), + // }; + + const ml = (level + 1) * 10 - level * 5; + + return ( + + + {field ? ( + <> + ) : ( + + )} + {/* // filterOperatorMap[ // OPTIONS_MAP[field as keyof typeof OPTIONS_MAP].type as keyof typeof + filterOperatorMap // ] */} + + + ); +}; diff --git a/src/renderer/features/playlists/components/playlist-query-builder.tsx b/src/renderer/features/playlists/components/playlist-query-builder.tsx new file mode 100644 index 00000000..126aa237 --- /dev/null +++ b/src/renderer/features/playlists/components/playlist-query-builder.tsx @@ -0,0 +1,336 @@ +import { useState, useImperativeHandle, forwardRef } from 'react'; +import { uniqueId } from 'lodash'; +import clone from 'lodash/clone'; +import setWith from 'lodash/setWith'; +import get from 'lodash/get'; +import sortBy from 'lodash/sortBy'; +import { nanoid } from 'nanoid'; +import { NDSongQueryFields } from '/@/renderer/api/navidrome.types'; +import { AdvancedFilterGroup, AdvancedFilterRule, QueryBuilder } from '/@/renderer/components'; +import { FilterGroupType } from '/@/renderer/types'; + +type AddArgs = { + groupIndex: number[]; + groupValue: string; + level: number; +}; + +type DeleteArgs = { + groupIndex: number[]; + groupValue: string; + level: number; + uniqueId: string; +}; + +const sortQuery = (query: any) => { + let b; + + if (query.all) { + b = sortBy(query.all, (item) => { + const key = Object.keys(item)[0]; + return key === 'all' || key === 'any' ? 0 : 1; + }); + } else { + b = sortBy(query.any, (item) => { + const key = Object.keys(item)[0]; + return key === 'all' || key === 'any' ? 0 : 1; + }); + } + + return { all: b }; +}; + +const addUniqueId = (query: any) => { + const queryCopy = clone(query); + const addId = (item: any) => { + const key = Object.keys(item)[0]; + if (key === 'all' || key === 'any') { + item[key].forEach(addId); + } else { + item[key].uniqueId = nanoid(); + } + }; + + addId(queryCopy); + return queryCopy; +}; + +export const PlaylistQueryBuilder = forwardRef(({ query, onChange }: any, ref) => { + const [filters, setFilters] = useState( + sortQuery(addUniqueId(query)) || { + all: [], + }, + ); + + console.log('filters :>> ', JSON.stringify(filters)); + + useImperativeHandle(ref, () => ({ + reset() { + setFilters({ + all: [], + }); + }, + })); + + const setFilterHandler = (newFilters: AdvancedFilterGroup) => { + setFilters(newFilters); + onChange(newFilters); + }; + + const handleAddRuleGroup = (args: AddArgs) => { + const { level, groupIndex, groupValue } = args; + const filtersCopy = clone(filters); + + const getPath = (level: number) => { + const rootKey = Object.keys(filters)[0]; + if (level === 0) return rootKey; + + const str = [rootKey]; + for (const index of groupIndex) { + str.push(`[${index}].${groupValue}`); + } + + return `${str.join('.')}`; + }; + + const path = getPath(level); + console.log('path', filtersCopy, path); + const updatedFilters = setWith( + filtersCopy, + path, + sortQuery([...get(filtersCopy, path), { any: [{ contains: { title: '' } }] }]), + clone, + ); + + setFilterHandler(updatedFilters); + }; + + const handleDeleteRuleGroup = (args: DeleteArgs) => { + const { uniqueId, level, groupIndex, groupValue } = args; + const filtersCopy = clone(filters); + + const getPath = (level: number) => { + const rootKey = Object.keys(filters)[0]; + if (level === 0) return rootKey; + + const str = []; + for (let i = 0; i < groupIndex.length; i += 1) { + if (groupIndex.length === 1) { + str.push(rootKey); + break; + } + + if (i === 0) { + str.push(`${rootKey}[${groupIndex[i]}]`); + } else if (i !== groupIndex.length - 1) { + str.push(`${groupValue}[${groupIndex[i]}]`); + } else { + str.push(`${groupValue}`); + } + } + + return `${str.join('.')}`; + }; + + const path = getPath(level); + + const dataAtPath = get(filtersCopy, path); + const lv = groupIndex[level - 1]; + const removed = [...dataAtPath.slice(0, lv), ...dataAtPath.slice(lv + 1)]; + const updatedFilters = setWith(filtersCopy, path, sortQuery(removed), clone); + + setFilterHandler(updatedFilters); + }; + + const getRulePath = (level: number, groupIndex: number[], groupValue: string) => { + if (level === 0) return Object.keys(filters)[0]; + + const str = []; + for (const index of groupIndex) { + str.push(`${Object.keys(filters)[0]}[${index}].${groupValue}`); + } + + return `${str.join('.')}`; + }; + + // const getRulePath = ( + // level: number, + // groupIndex: number[], + // groupValue: string, + // uniqueId?: string, + // ) => { + // const rootKey = Object.keys(filters)[0]; + // if (level === 0) return rootKey; + + // const str = []; + // for (const index of groupIndex) { + // if (uniqueId) { + // str.push(`${rootKey}[${index}].${groupValue}.${uniqueId}`); + // } else { + // str.push(`${rootKey}[${index}].${groupValue}`); + // } + // } + + // return `${str.join('.')}`; + // }; + + const handleAddRule = (args: AddArgs) => { + const { level, groupValue, groupIndex } = args; + const filtersCopy = clone(filters); + + const path = getRulePath(level, groupIndex, groupValue); + + const updatedFilters = setWith( + filtersCopy, + path, + [...get(filtersCopy, path), { contains: { title: '', uniqueId: nanoid() } }], + clone, + ); + + setFilterHandler(updatedFilters); + }; + + const handleDeleteRule = (args: DeleteArgs) => { + const { uniqueId, level, groupIndex, groupValue } = args; + const filtersCopy = clone(filters); + + const path = getRulePath(level, groupIndex, groupValue); + + const dataAtPath = get(filtersCopy, path); + const lv = groupIndex[level - 1]; + const removed = [...dataAtPath.slice(0, lv), ...dataAtPath.slice(lv + 1)]; + + console.log('removed :>> ', removed); + + const updatedFilters = setWith(filtersCopy, path, removed, clone); + + setFilterHandler(updatedFilters); + }; + + const handleChangeField = (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: AdvancedFilterRule) => { + if (rule.uniqueId !== uniqueId) return rule; + // const defaultOperator = FILTER_OPTIONS_DATA.find( + // (option) => option.value === value, + // )?.default; + + return { + ...rule, + field: value, + // operator: defaultOperator || '', + operator: '', + value: '', + }; + }), + clone, + ); + + console.log('updatedFilters', updatedFilters); + + // 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: AdvancedFilterRule) => { + 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: AdvancedFilterRule) => { + if (rule.uniqueId !== uniqueId) return rule; + return { + ...rule, + value, + }; + }), + clone, + ); + + setFilterHandler(updatedFilters); + }; + + return ( + <> + + + ); +});