From b78f84ffd00348851efeb2b30df796f7579817f4 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Tue, 15 Sep 2020 16:25:57 +0200 Subject: [PATCH] Add tags display. --- client/src/components/Window.tsx | 10 +++ .../querybuilder/QBSelectWithRequest.tsx | 5 -- client/src/components/tables/ResultsTable.tsx | 13 +++- client/src/lib/stringifyList.tsx | 16 +++- server/endpoints/QueryEndpointHandler.ts | 74 ++++++++----------- server/lib/dbToApi.ts | 44 +++++++++++ 6 files changed, 109 insertions(+), 53 deletions(-) create mode 100644 server/lib/dbToApi.ts diff --git a/client/src/components/Window.tsx b/client/src/components/Window.tsx index 3b13f01..7ce375c 100644 --- a/client/src/components/Window.tsx +++ b/client/src/components/Window.tsx @@ -30,6 +30,16 @@ export default function Window(props: any) { getTitle: (song: any) => song.title, getArtist: (song: any) => stringifyList(song.artists, (a: any) => a.name), getAlbum: (song: any) => stringifyList(song.albums, (a: any) => a.name), + getTags: (song: any) => { + // Recursively resolve the name. + const resolveTag = (tag: any) => { + var r = [tag.name]; + if(tag.parent) { r.unshift(resolveTag(tag.parent)); } + return r; + } + + return song.tags.map((tag: any) => resolveTag(tag)); + } } const doQuery = async (_query: QueryElem) => { diff --git a/client/src/components/querybuilder/QBSelectWithRequest.tsx b/client/src/components/querybuilder/QBSelectWithRequest.tsx index b4caac1..dd1b115 100644 --- a/client/src/components/querybuilder/QBSelectWithRequest.tsx +++ b/client/src/components/querybuilder/QBSelectWithRequest.tsx @@ -28,7 +28,6 @@ export default function QBSelectWithRequest(props: IProps & any) { const updateOptions = (forInput: string, options: any[]) => { if (forInput === input) { - console.log("setting options."); setOptions({ forInput: forInput, options: options, @@ -37,11 +36,9 @@ export default function QBSelectWithRequest(props: IProps & any) { } const startRequest = (_input: string) => { - console.log('starting req', _input); setInput(_input); (async () => { const newOptions = await getNewOptions(_input); - console.log('new options', newOptions); updateOptions(_input, newOptions); })(); }; @@ -72,8 +69,6 @@ export default function QBSelectWithRequest(props: IProps & any) { } } - console.log("Render props:", props); - return ( string, getArtist: (song: any) => string, getAlbum: (song: any) => string, + getTags: (song: any) => string[][], // Each tag is represented as a series of strings. } export interface IProps { @@ -28,18 +30,25 @@ export function SongTable(props: IProps) { Title Artist Album + Tags - {props.songs.map((song:any) => { + {props.songs.map((song: any) => { const title = props.songGetters.getTitle(song); const artist = props.songGetters.getArtist(song); const album = props.songGetters.getAlbum(song); + const tags = props.songGetters.getTags(song).map((tag: string[]) => { + return { + return (idx === 0) ? e : " / " + e; + })} /> + }); return {title} {artist} {album} + {tags} })} diff --git a/client/src/lib/stringifyList.tsx b/client/src/lib/stringifyList.tsx index dc883ba..eee9c21 100644 --- a/client/src/lib/stringifyList.tsx +++ b/client/src/lib/stringifyList.tsx @@ -1,12 +1,20 @@ export default function stringifyList( s: any[], stringifyElem?: (e: any) => string, + stringifyConnect?: (idx: number, e: any) => string, ) { - const stringify = stringifyElem || ((e: any) => e); + if(!stringifyElem) { + stringifyElem = (e: any) => e; + } + if(!stringifyConnect) { + stringifyConnect = (idx: number, e: string) => { + return (idx === 0) ? e : ", " + e; + } + } + var r = ""; - if (s.length > 0) { r += stringify(s[0]) } - for (let i = 1; i < s.length; i++) { - r += ", " + stringify(s[i]); + for (let i = 0; i < s.length; i++) { + r += stringifyConnect(i, stringifyElem(s[i])); } return r; diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/QueryEndpointHandler.ts index 6b37ea3..ac37749 100644 --- a/server/endpoints/QueryEndpointHandler.ts +++ b/server/endpoints/QueryEndpointHandler.ts @@ -2,6 +2,7 @@ import * as api from '../../client/src/api'; import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; import Knex from 'knex'; import asJson from '../lib/asJson'; +import { toApiArtist, toApiTag, toApiAlbum, toApiSong } from '../lib/dbToApi'; enum ObjectType { Song = 0, @@ -230,6 +231,21 @@ async function getLinkedObjects(knex: Knex, base: ObjectType, linked: ObjectType return result; } +// Resolve a tag into the full nested structure of its ancestors. +async function getFullTag(knex: Knex, tag: any): Promise { + const resolveTag = async (t: any) => { + if (t['tags.parentId']) { + const parent = (await knex.select(objectColumns[ObjectType.Tag]) + .from('tags') + .where({ [objectTables[ObjectType.Tag] + '.id']: t['tags.parentId'] }))[0]; + t.parent = await resolveTag(parent); + } + return t; + } + + return await resolveTag(tag); +} + export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { if (!api.checkQueryRequest(req.body)) { const e: EndpointError = { @@ -295,7 +311,6 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, // For that we need to do further queries. const songIdsPromise = (async () => { const songs = await songsPromise; - console.log("Found songs:", songs); const ids = songs.map((song: any) => song['songs.id']); return ids; })(); @@ -306,7 +321,17 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, (async () => { return {}; })(); const songsTagsPromise: Promise> = (songLimit && songLimit > 0) ? (async () => { - return await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); + const tagsPerSong: Record = await getLinkedObjects(knex, ObjectType.Song, ObjectType.Tag, await songIdsPromise); + var result: Record = {}; + for (var key in tagsPerSong) { + const tags = tagsPerSong[key]; + var fullTags: any[] = []; + for (var idx in tags) { + fullTags.push(await getFullTag(knex, tags[idx])); + } + result[key] = fullTags; + } + return result; })() : (async () => { return {}; })(); const songsAlbumsPromise: Promise> = (songLimit && songLimit > 0) ? @@ -336,52 +361,17 @@ export const QueryEndpointHandler: EndpointHandler = async (req: any, res: any, const response: api.QueryResponse = { songs: songs.map((song: any) => { - return { - songId: song['songs.id'], - title: song['songs.title'], - storeLinks: asJson(song['songs.storeLinks']), - artists: songsArtists[song['songs.id']].map((artist: any) => { - return { - artistId: artist['artists.id'], - name: artist['artists.name'], - storeLinks: asJson(artist['artists.storeLinks']), - }; - }), - tags: songsTags[song['songs.id']].map((tag: any) => { - return { - tagId: tag['tags.id'], - name: tag['tags.name'], - }; - }), - albums: songsAlbums[song['songs.id']].map((album: any) => { - return { - albumId: album['albums.id'], - name: album['albums.name'], - storeLinks: asJson(album['albums.storeLinks']), - }; - }), - } + const id = song['songs.id']; + return toApiSong(song, songsArtists[id], songsTags[id], songsAlbums[id]); }), artists: artists.map((artist: any) => { - return { - artistId: artist['artists.id'], - name: artist['artists.name'], - storeLinks: asJson(artist['artists.storeLinks']), - } + return toApiArtist(artist); }), albums: albums.map((album: any) => { - return { - albumId: album['albums.id'], - name: album['albums.name'], - storeLinks: asJson(album['albums.storeLinks']), - } + return toApiAlbum(album); }), tags: tags.map((tag: any) => { - return { - tagId: tag['tags.id'], - name: tag['tags.name'], - parentId: tag['tags.parentId'], - } + return toApiTag(tag); }), } diff --git a/server/lib/dbToApi.ts b/server/lib/dbToApi.ts new file mode 100644 index 0000000..3495058 --- /dev/null +++ b/server/lib/dbToApi.ts @@ -0,0 +1,44 @@ +import * as api from '../../client/src/api'; +import asJson from './asJson'; + +export function toApiTag(dbObj: any): api.TagDetails { + return { + tagId: dbObj['tags.id'], + name: dbObj['tags.name'], + parentId: dbObj['tags.parentId'], + parent: dbObj.parent ? toApiTag(dbObj.parent) : undefined, + }; +} + +export function toApiArtist(dbObj: any) { + return { + artistId: dbObj['artists.id'], + name: dbObj['artists.name'], + storeLinks: asJson(dbObj['artists.storeLinks']), + }; +} + +export function toApiSong(dbObj: any, artists: any[], tags: any[], albums: any[]) { + return { + songId: dbObj['songs.id'], + title: dbObj['songs.title'], + storeLinks: asJson(dbObj['songs.storeLinks']), + artists: artists.map((artist: any) => { + return toApiArtist(artist); + }), + tags: tags.map((tag: any) => { + return toApiTag(tag); + }), + albums: albums.map((album: any) => { + return toApiAlbum(album); + }), + } +} + +export function toApiAlbum(dbObj: any) { + return { + albumId: dbObj['albums.id'], + name: dbObj['albums.name'], + storeLinks: asJson(dbObj['albums.storeLinks']), + }; +} \ No newline at end of file