Basic tag support.

pull/20/head
Sander Vocke 5 years ago
parent 72a5515167
commit 659c395752
  1. 1
      client/src/api.ts
  2. 3
      client/src/components/Window.tsx
  3. 77
      client/src/components/querybuilder/QBAddElemMenu.tsx
  4. 22
      client/src/components/querybuilder/QBLeafElem.tsx
  5. 7
      client/src/components/querybuilder/QueryBuilder.tsx
  6. 1
      client/src/components/tables/ResultsTable.tsx
  7. 48
      client/src/lib/query/Getters.tsx
  8. 23
      client/src/lib/query/Query.tsx
  9. 2
      server/endpoints/QueryEndpointHandler.ts

@ -81,6 +81,7 @@ export enum QueryElemProperty {
artistName = "artistName",
artistId = "artistId",
albumName = "albumName",
tagId = "tagId",
}
export enum OrderByType {
Name = 0,

@ -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,
}}
/>
</Box>

@ -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 <NestedMenuItem
label={_props.tag.name}
parentMenuOpen={Boolean(anchorEl)}
>
{children.map((child: any) => <TagItem tag={child} allTags={_props.allTags} />)}
</NestedMenuItem>
}
return <MenuItem
onClick={() => {
onClose();
props.onCreateQuery({
a: QueryLeafBy.TagInfo,
leafOp: QueryLeafOp.Equals,
b: createTagInfo(_props.tag, _props.allTags),
});
}}
>
{_props.tag.name}
</MenuItem>
}
const BaseTagsItem = (_props: any) => {
const [tags, setTags] = useState<any[] | null>(null);
useEffect(() => {
(async () => {
setTags(await props.requestFunctions.getTags());
})()
}, []);
return tags ?
<>
{tags.filter((tag: any) => !tag.parentId).map((tag: any) => {
return <TagItem tag={tag} allTags={tags} />
})}
</>
: <>Loading!</>
}
return <Menu
anchorEl={anchorEl}
keepMounted
@ -77,5 +142,11 @@ export function QBAddElemMenu(props: MenuProps) {
style={{ width: 300 }}
/>
</NestedMenuItem>
<NestedMenuItem
label="Tag"
parentMenuOpen={Boolean(anchorEl)}
>
<BaseTagsItem />
</NestedMenuItem>
</Menu >
}

@ -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 <LabeledElemChip
label={"Tag \"" + stringifyList(tagInfo.fullName, undefined,
(idx: number, e: any) => (idx === 0) ? e : " / " + e) + "\""}
extraElements={props.extraElements}
/>
}
export interface DeleteButtonProps {
onClick?: (e: any) => void,
}
@ -139,6 +152,13 @@ export function QBLeafElem(props: IProps) {
{...props}
extraElements={extraElements}
/>
} else if (e.a == QueryLeafBy.TagInfo &&
e.leafOp == QueryLeafOp.Equals &&
isTagQueryInfo(e.b)) {
return <QBQueryElemTagEquals
{...props}
extraElements={extraElements}
/>
}else if (e.leafOp == QueryLeafOp.Placeholder) {
return <QBPlaceholder
onReplace={props.onReplace}

@ -4,11 +4,18 @@ import QBQueryButton from './QBEditButton';
import { QBQueryElem } from './QBQueryElem';
import { QueryElem, addPlaceholders, removePlaceholders, simplify } from '../../lib/query/Query';
export interface TagItem {
name: string,
id: number,
childIds: number[],
parentId?: number,
}
export interface Requests {
getArtists: (filter: string) => Promise<string[]>,
getAlbums: (filter: string) => Promise<string[]>,
getSongTitles: (filter: string) => Promise<string[]>,
getTags: () => Promise<TagItem[]>,
}
export interface IProps {

@ -52,6 +52,7 @@ export function SongTable(props: IProps) {
textTransform: "none",
fontWeight: 400,
paddingLeft: '0',
textAlign: 'left',
}
})();
return <TableCell padding="none" {...props}>

@ -104,3 +104,51 @@ export async function getSongTitles(filter: string) {
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<number, any> = {};
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);
})();
}

@ -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],

@ -19,6 +19,7 @@ const propertyObjects: Record<api.QueryElemProperty, ObjectType> = {
[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.";

Loading…
Cancel
Save