Added tag merging support, browsing tag from management window

pull/24/head
Sander Vocke 5 years ago
parent 045cdc51e2
commit 06044c5a51
  1. 8
      client/src/api.ts
  2. 7
      client/src/components/windows/manage_tags/ManageTagMenu.tsx
  3. 22
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  4. 8
      client/src/components/windows/manage_tags/TagChange.tsx
  5. 16
      client/src/lib/backend/tags.tsx
  6. 2
      server/app.ts
  7. 73
      server/endpoints/MergeTagEndpointHandler.ts

@ -311,3 +311,11 @@ export interface DeleteTagResponse { }
export function checkDeleteTagRequest(req: any): boolean {
return true;
}
// Merge tag (POST).
export const MergeTagEndpoint = '/tag/:id/merge/:toId';
export interface MergeTagRequest { }
export interface MergeTagResponse { }
export function checkMergeTagRequest(req: any): boolean {
return true;
}

@ -35,6 +35,7 @@ export default function ManageTagMenu(props: {
onDelete: () => void,
onMove: (to: string | null) => void,
onMergeInto: (to: string) => void,
onOpenInTab: () => void,
tag: any,
changedTags: any[], // Tags organized hierarchically with "children" fields
}) {
@ -49,6 +50,12 @@ export default function ManageTagMenu(props: {
keepMounted
onClose={props.onClose}
>
<MenuItem
onClick={() => {
props.onClose();
props.onOpenInTab();
}}
>Browse</MenuItem>
<MenuItem
onClick={() => {
props.onClose();

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { WindowState } from '../Windows';
import { WindowState, newWindowReducer, WindowType } from '../Windows';
import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core';
import LoyaltyIcon from '@material-ui/icons/Loyalty';
import ArrowRightIcon from '@material-ui/icons/ArrowRight';
@ -9,6 +9,9 @@ import ControlTagChanges, { TagChange, TagChangeType, submitTagChanges } from '.
import { queryTags } from '../../../lib/backend/queries';
import NewTagMenu from './NewTagMenu';
import { v4 as genUuid } from 'uuid';
import { MainWindowStateActions } from '../../MainWindow';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import { songGetters } from '../../../lib/songGetters';
var _ = require('lodash');
export interface ManageTagsWindowState extends WindowState {
@ -108,6 +111,7 @@ export function SingleTag(props: {
tag: any,
prependElems: any[],
dispatch: (action: any) => void,
mainDispatch: (action: any) => void,
state: ManageTagsWindowState,
changedTags: any[],
}) {
@ -156,6 +160,7 @@ export function SingleTag(props: {
<TagChip transparent={true} label={tagLabel} />,
<Typography variant="h5">/</Typography>]}
dispatch={props.dispatch}
mainDispatch={props.mainDispatch}
state={props.state}
changedTags={props.changedTags}
/>)}
@ -163,6 +168,20 @@ export function SingleTag(props: {
position={menuPos}
open={menuPos !== null}
onClose={onCloseMenu}
onOpenInTab={() => {
props.mainDispatch({
type: MainWindowStateActions.AddTab,
tabState: {
tabLabel: <><LocalOfferIcon />{tag.name}</>,
tagId: tag.tagId,
metadata: null,
songGetters: songGetters,
songsWithTag: null,
},
tabReducer: newWindowReducer[WindowType.Tag],
tabType: WindowType.Tag,
})
}}
onRename={(s: string) => {
props.dispatch({
type: ManageTagsWindowActions.SetPendingChanges,
@ -388,6 +407,7 @@ export default function ManageTagsWindow(props: {
tag={tag}
prependElems={[]}
dispatch={props.dispatch}
mainDispatch={props.mainDispatch}
state={props.state}
changedTags={changedTags}
/>;

@ -4,7 +4,7 @@ import { queryTags } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import DiscardChangesButton from '../../common/DiscardChangesButton';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import { createTag, modifyTag, deleteTag } from '../../../lib/backend/tags';
import { createTag, modifyTag, deleteTag, mergeTag } from '../../../lib/backend/tags';
export enum TagChangeType {
Delete = "Delete",
@ -40,6 +40,7 @@ export async function submitTagChanges(changes: TagChange[]) {
// Otherwise, look it up in the table.
const parentId = change.parent ? getId(change.parent) : undefined;
const numericId = change.id ? getId(change.id) : undefined;
const intoId = change.into ? getId(change.into) : undefined;
switch (change.type) {
case TagChangeType.Create:
if (!change.name) { throw new Error("Cannot create tag without name"); }
@ -69,6 +70,11 @@ export async function submitTagChanges(changes: TagChange[]) {
if (!numericId) { throw new Error("Cannot delete tag with no numeric ID"); }
await deleteTag(numericId)
break;
case TagChangeType.MergeTo:
if (!numericId) { throw new Error("Cannot merge tag with no numeric ID"); }
if (!intoId) { throw new Error("Cannot merge tag into tag with no numeric ID"); }
await mergeTag(numericId, intoId);
break;
default:
throw new Error("Unimplemented tag change");
}

@ -43,3 +43,19 @@ export async function deleteTag(id: number) {
throw new Error("Response to tag deletion not OK: " + JSON.stringify(response));
}
}
export async function mergeTag(fromId: number, toId: number) {
const requestOpts = {
method: 'POST',
};
const response = await fetch(
(process.env.REACT_APP_BACKEND || "") + serverApi.MergeTagEndpoint
.replace(':id', fromId.toString())
.replace(':toId', toId.toString()),
requestOpts
);
if (!response.ok) {
throw new Error("Response to tag merge not OK: " + JSON.stringify(response));
}
}

@ -16,6 +16,7 @@ import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbumEndpointHandl
import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbumEndpointHandler';
import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler';
import { DeleteTagEndpointHandler } from './endpoints/DeleteTagEndpointHandler';
import { MergeTagEndpointHandler } from './endpoints/MergeTagEndpointHandler';
import * as endpointTypes from './endpoints/types';
const invokeHandler = (handler:endpointTypes.EndpointHandler, knex: Knex) => {
@ -55,6 +56,7 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
app.put(apiBaseUrl + api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler));
app.get(apiBaseUrl + api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler));
app.delete(apiBaseUrl + api.DeleteTagEndpoint, invokeWithKnex(DeleteTagEndpointHandler));
app.post(apiBaseUrl + api.MergeTagEndpoint, invokeWithKnex(MergeTagEndpointHandler));
}
export { SetupApp }

@ -0,0 +1,73 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const MergeTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkMergeTagRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid MergeTag request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.DeleteTagRequest = req.body;
console.log("Merge Tag:", reqObject);
const fromId = req.params.id;
const toId = req.params.toId;
await knex.transaction(async (trx) => {
try {
// Start retrieving the "from" tag.
const fromTagPromise = trx.select('id')
.from('tags')
.where({ id: fromId })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Start retrieving the "to" tag.
const toTagPromise = trx.select('id')
.from('tags')
.where({ id: toId })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Wait for the requests to finish.
var [fromTag, toTag] = await Promise.all([fromTagPromise, toTagPromise]);
// Check that we found all objects we need.
if (!fromTag || !toTag) {
const e: EndpointError = {
internalMessage: 'Source or target tag does not exist for MergeTag request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
// Assign new tag ID to any objects referencing the to-be-merged tag.
const cPromise = trx('tags')
.where({ 'parentId': fromId })
.update({ 'parentId': toId });
const sPromise = trx('songs_tags')
.where({ 'tagId': fromId })
.update({ 'tagId': toId });
const arPromise = trx('artists_tags')
.where({ 'tagId': fromId })
.update({ 'tagId': toId });
const alPromise = trx('albums_tags')
.where({ 'tagId': fromId })
.update({ 'tagId': toId });
await Promise.all([sPromise, arPromise, alPromise, cPromise]);
// Delete the original tag.
await trx('tags')
.where({ 'id': fromId })
.del();
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
}
Loading…
Cancel
Save