diff --git a/client/src/api.ts b/client/src/api.ts index 3ef33f7..b162fff 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -81,6 +81,7 @@ export enum QueryElemProperty { artistName = "artistName", artistId = "artistId", albumName = "albumName", + tagId = "tagId", } export enum OrderByType { Name = 0, diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index 7ce375c..8e87931 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -5,7 +5,7 @@ import QueryBuilder from './querybuilder/QueryBuilder'; import * as serverApi from '../api'; import { SongTable } from './tables/ResultsTable'; import stringifyList from '../lib/stringifyList'; -import { getArtists, getSongTitles, getAlbums } from '../lib/query/Getters'; +import { getArtists, getSongTitles, getAlbums, getTags } from '../lib/query/Getters'; var _ = require('lodash'); const darkTheme = createMuiTheme({ @@ -102,6 +102,7 @@ export default function Window(props: any) { getArtists: getArtists, getSongTitles: getSongTitles, getAlbums: getAlbums, + getTags: getTags, }} /> diff --git a/client/src/components/querybuilder/QBAddElemMenu.tsx b/client/src/components/querybuilder/QBAddElemMenu.tsx index acce42d..61af908 100644 --- a/client/src/components/querybuilder/QBAddElemMenu.tsx +++ b/client/src/components/querybuilder/QBAddElemMenu.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Menu, MenuItem } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; -import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../lib/query/Query'; +import { QueryElem, QueryLeafBy, QueryLeafOp, TagQueryInfo } from '../../lib/query/Query'; import QBSelectWithRequest from './QBSelectWithRequest'; -import { Requests } from './QueryBuilder'; +import { Requests, TagItem } from './QueryBuilder'; export interface MenuProps { anchorEl: null | HTMLElement, @@ -12,10 +12,75 @@ export interface MenuProps { requestFunctions: Requests, } +export function createTagInfo(tag: any, allTags: any[]): TagQueryInfo { + const resolveName: (t: any) => string[] = (t: any) => { + if (t.parentId) { + const parent = allTags.filter((o: any) => o.tagId === t.parentId)[0]; + return [resolveName(parent), t.name]; + } + return [t.name]; + } + + return { + id: tag.tagId, + fullName: resolveName(tag), + } +} + export function QBAddElemMenu(props: MenuProps) { let anchorEl = props.anchorEl; let onClose = props.onClose; + interface TagItemProps { + tag: any, + allTags: any[], + } + const TagItem = (_props: TagItemProps) => { + if (_props.tag.childIds.length > 0) { + const children = _props.allTags.filter( + (tag: any) => _props.tag.childIds.includes(tag.tagId) + ); + + return + {children.map((child: any) => )} + + } + + return { + onClose(); + props.onCreateQuery({ + a: QueryLeafBy.TagInfo, + leafOp: QueryLeafOp.Equals, + b: createTagInfo(_props.tag, _props.allTags), + }); + }} + > + {_props.tag.name} + + } + + const BaseTagsItem = (_props: any) => { + const [tags, setTags] = useState(null); + + useEffect(() => { + (async () => { + setTags(await props.requestFunctions.getTags()); + })() + }, []); + + return tags ? + <> + {tags.filter((tag: any) => !tag.parentId).map((tag: any) => { + return + })} + + : <>Loading! + } + return + + + } diff --git a/client/src/components/querybuilder/QBLeafElem.tsx b/client/src/components/querybuilder/QBLeafElem.tsx index d772b7e..566f9be 100644 --- a/client/src/components/querybuilder/QBLeafElem.tsx +++ b/client/src/components/querybuilder/QBLeafElem.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem } from '../../lib/query/Query'; +import { QueryLeafElem, QueryLeafBy, QueryLeafOp, QueryElem, TagQueryInfo, isTagQueryInfo } from '../../lib/query/Query'; import { Chip, Typography, IconButton, Box } from '@material-ui/core'; import { QBPlaceholder } from './QBPlaceholder'; import DeleteIcon from '@material-ui/icons/Delete'; import { Requests } from './QueryBuilder'; +import stringifyList from '../../lib/stringifyList'; export interface ElemChipProps { label: any, @@ -65,6 +66,18 @@ export function QBQueryElemAlbumLike(props: LeafProps) { /> } +export function QBQueryElemTagEquals(props: LeafProps) { + if (!isTagQueryInfo(props.elem.b)) { + throw new Error("Tag equals should have a TagQueryInfo operand"); + } + const tagInfo: TagQueryInfo = props.elem.b; + return (idx === 0) ? e : " / " + e) + "\""} + extraElements={props.extraElements} + /> +} + export interface DeleteButtonProps { onClick?: (e: any) => void, } @@ -139,7 +152,14 @@ export function QBLeafElem(props: IProps) { {...props} extraElements={extraElements} /> - } else if (e.leafOp == QueryLeafOp.Placeholder) { + } else if (e.a == QueryLeafBy.TagInfo && + e.leafOp == QueryLeafOp.Equals && + isTagQueryInfo(e.b)) { + return + }else if (e.leafOp == QueryLeafOp.Placeholder) { return Promise, getAlbums: (filter: string) => Promise, getSongTitles: (filter: string) => Promise, + getTags: () => Promise, } export interface IProps { diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 4e41e5d..3c6fb5e 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -52,6 +52,7 @@ export function SongTable(props: IProps) { textTransform: "none", fontWeight: 400, paddingLeft: '0', + textAlign: 'left', } })(); return diff --git a/client/src/lib/query/Getters.tsx b/client/src/lib/query/Getters.tsx index 56c4f33..8d16129 100644 --- a/client/src/lib/query/Getters.tsx +++ b/client/src/lib/query/Getters.tsx @@ -103,4 +103,52 @@ export async function getSongTitles(filter: string) { const titles: string[] = json.songs.map((elem: any) => { return elem.title; }); return [...new Set(titles)]; })(); +} + +export async function getTags() { + var q: serverApi.QueryRequest = { + query: {}, + offsetsLimits: { + tagOffset: 0, + tagLimit: 100, + }, + ordering: { + orderBy: { + type: serverApi.OrderByType.Name, + }, + ascending: true, + }, + }; + + const requestOpts = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(q), + }; + + return (async () => { + const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) + let json: any = await response.json(); + const tags = json.tags; + + // Organise the tags into a tree structure. + // First, we put them in an indexed dict. + const idxTags: Record = {}; + tags.forEach((tag: any) => { + idxTags[tag.tagId] = { + ...tag, + childIds: [], + } + }) + + // Resolve children. + tags.forEach((tag: any) => { + if(tag.parentId && tag.parentId in idxTags) { + idxTags[tag.parentId].childIds.push(tag.tagId); + } + }) + + // Return the loose objects again. + return Object.values(idxTags); + })(); } \ No newline at end of file diff --git a/client/src/lib/query/Query.tsx b/client/src/lib/query/Query.tsx index 75f86e8..714ce4e 100644 --- a/client/src/lib/query/Query.tsx +++ b/client/src/lib/query/Query.tsx @@ -3,7 +3,7 @@ import * as serverApi from '../../api'; export enum QueryLeafBy { ArtistName = 0, AlbumName, - TagName, + TagInfo, SongTitle } @@ -13,7 +13,15 @@ export enum QueryLeafOp { Placeholder, // Special op which indicates that this leaf is not filled in yet. } -export type QueryLeafOperand = string | number; +export interface TagQueryInfo { + fullName: string[], + id: number, +} +export function isTagQueryInfo(e: any): e is TagQueryInfo { + return 'fullName' in e && 'id' in e; + } + +export type QueryLeafOperand = string | number | TagQueryInfo; export interface QueryLeafElem { a: QueryLeafBy; @@ -168,7 +176,16 @@ export function toApiQuery(q: QueryElem) : serverApi.Query { [QueryNodeOp.Or]: serverApi.QueryElemOp.Or, } - if(isLeafElem(q)) { + if(isLeafElem(q) && isTagQueryInfo(q.b)) { + // Special case for tag queries by ID + const r: serverApi.QueryElem = { + prop: serverApi.QueryElemProperty.tagId, + propOperator: serverApi.QueryFilterOp.Eq, + propOperand: q.b.id, + } + return r; + } else if(isLeafElem(q)) { + // "Regular" queries const r: serverApi.QueryElem = { prop: propsMapping[q.a], propOperator: leafOpsMapping[q.leafOp], diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index ac37749..0aed145 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -19,6 +19,7 @@ const propertyObjects: Record = { [api.QueryElemProperty.artistName]: ObjectType.Artist, [api.QueryElemProperty.songId]: ObjectType.Song, [api.QueryElemProperty.songTitle]: ObjectType.Song, + [api.QueryElemProperty.tagId]: ObjectType.Tag, } // To keep track of the tables in which objects are stored. @@ -100,6 +101,7 @@ function addLeafWhere(knexQuery: any, queryElem: api.QueryElem, type: WhereType) [api.QueryElemProperty.artistName]: 'artists.name', [api.QueryElemProperty.artistId]: 'artists.id', [api.QueryElemProperty.albumName]: 'albums.name', + [api.QueryElemProperty.tagId]: 'tags.id', } if (!queryElem.propOperator) throw "Cannot create where clause without an operator.";