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
+ }
+
+ 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.";