Add tag deletion to back-end, applying of tag changes to front-end

pull/24/head
Sander Vocke 5 years ago
parent c8a39c9224
commit ff800978d1
  1. 8
      client/src/api.ts
  2. 2
      client/src/components/windows/album/AlbumWindow.tsx
  3. 2
      client/src/components/windows/artist/ArtistWindow.tsx
  4. 20
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  5. 57
      client/src/components/windows/manage_tags/TagChange.tsx
  6. 2
      client/src/components/windows/query/QueryWindow.tsx
  7. 2
      client/src/components/windows/song/SongWindow.tsx
  8. 2
      client/src/components/windows/tag/TagWindow.tsx
  9. 2
      client/src/lib/backend/queries.tsx
  10. 45
      client/src/lib/backend/tags.tsx
  11. 2
      server/app.ts
  12. 64
      server/endpoints/DeleteTagEndpointHandler.ts

@ -302,4 +302,12 @@ export interface TagDetailsResponse {
}
export function checkTagDetailsRequest(req: any): boolean {
return true;
}
// Delete tag (DELETE).
export const DeleteTagEndpoint = '/tag/:id';
export interface DeleteTagRequest { }
export interface DeleteTagResponse { }
export function checkDeleteTagRequest(req: any): boolean {
return true;
}

@ -9,7 +9,7 @@ import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveAlbumChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, querySongs } from '../../../lib/query/Backend';
import { queryAlbums, querySongs } from '../../../lib/backend/queries';
var _ = require('lodash');
export type AlbumMetadata = serverApi.AlbumDetails;

@ -9,7 +9,7 @@ import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveArtistChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, querySongs } from '../../../lib/query/Backend';
import { queryArtists, querySongs } from '../../../lib/backend/queries';
var _ = require('lodash');
export type ArtistMetadata = serverApi.ArtistDetails;

@ -5,8 +5,8 @@ import LoyaltyIcon from '@material-ui/icons/Loyalty';
import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ManageTagMenu from './ManageTagMenu';
import ControlTagChanges, { TagChange, TagChangeType } from './TagChange';
import { queryTags } from '../../../lib/query/Backend';
import ControlTagChanges, { TagChange, TagChangeType, submitTagChanges } from './TagChange';
import { queryTags } from '../../../lib/backend/queries';
import NewTagMenu from './NewTagMenu';
import { v4 as genUuid } from 'uuid';
var _ = require('lodash');
@ -22,6 +22,7 @@ export interface ManageTagsWindowState extends WindowState {
export enum ManageTagsWindowActions {
SetFetchedTags = "SetFetchedTags",
SetPendingChanges = "SetPendingChanges",
Reset = "Reset",
}
export function ManageTagsWindowReducer(state: ManageTagsWindowState, action: any) {
@ -36,6 +37,12 @@ export function ManageTagsWindowReducer(state: ManageTagsWindowState, action: an
...state,
pendingChanges: action.value,
}
case ManageTagsWindowActions.Reset:
return {
...state,
pendingChanges: [],
fetchedTags: null,
}
default:
throw new Error("Unimplemented ManageTagsWindow state update.")
}
@ -344,7 +351,14 @@ export default function ManageTagsWindow(props: {
type: ManageTagsWindowActions.SetPendingChanges,
value: [],
})}
onSave={() => { }}
onSave={() => {
submitTagChanges(props.state.pendingChanges).then(() => {
console.log("reset!")
props.dispatch({
type: ManageTagsWindowActions.Reset
});
})
}}
getTagDetails={(id: string) => tagsWithChanges[id]}
/>
</Box>}

@ -1,9 +1,10 @@
import React, { useState, useEffect } from 'react';
import { Typography, Chip, CircularProgress, Box, Paper } from '@material-ui/core';
import { queryTags } from '../../../lib/query/Backend';
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';
export enum TagChangeType {
Delete = "Delete",
@ -21,6 +22,58 @@ export interface TagChange {
name?: string,
}
export async function submitTagChanges(changes: TagChange[]) {
// Upon entering this function, some tags have a real numeric MuDBase ID (stringified),
// while others have a UUID string which is a placeholder until the tag is created.
// While applying the changes, UUIDs will be replaced by real numeric IDs.
// Therefore we maintain a lookup table for mapping the old to the new.
var id_lookup: Record<string, number> = {}
const getId = (id_string: string) => {
return (Number(id_string) === NaN) ?
id_lookup[id_string] : Number(id_string);
}
for (const change of changes) {
// If string is of form "1", convert to ID number directly.
// Otherwise, look it up in the table.
const parentId = change.parent ? getId(change.parent) : undefined;
const numericId = change.id ? getId(change.id) : undefined;
switch (change.type) {
case TagChangeType.Create:
if (!change.name) { throw new Error("Cannot create tag without name"); }
const { id } = await createTag({
name: change.name,
parentId: parentId,
});
id_lookup[change.id] = id;
break;
case TagChangeType.MoveTo:
if (!numericId) { throw new Error("Cannot modify tag with no numeric ID"); }
await modifyTag(
numericId,
{
parentId: parentId,
})
break;
case TagChangeType.Rename:
if (!numericId) { throw new Error("Cannot modify tag with no numeric ID"); }
await modifyTag(
numericId,
{
name: change.name,
})
break;
case TagChangeType.Delete:
if (!numericId) { throw new Error("Cannot delete tag with no numeric ID"); }
await deleteTag(numericId)
break;
default:
throw new Error("Unimplemented tag change");
}
}
}
export function TagChangeDisplay(props: {
change: TagChange,
getTagDetails: (id: string) => any,
@ -52,7 +105,7 @@ export function TagChangeDisplay(props: {
return <Typography>Move {MainTag} from {OldParent} to {NewParent}</Typography>
case TagChangeType.Create:
return props.change.parent ?
<Typography>Create {MainTag} under <MakeTag name={newParent.name}/></Typography> :
<Typography>Create {MainTag} under <MakeTag name={newParent.name} /></Typography> :
<Typography>Create {MainTag}</Typography>
default:
throw new Error("Unhandled tag change type")

@ -5,7 +5,7 @@ import QueryBuilder from '../../querybuilder/QueryBuilder';
import * as serverApi from '../../../api';
import SongTable from '../../tables/ResultsTable';
import { songGetters } from '../../../lib/songGetters';
import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/query/Backend';
import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { grey } from '@material-ui/core/colors';
import { WindowState } from '../Windows';
var _ = require('lodash');

@ -12,7 +12,7 @@ import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import { saveSongChanges } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { querySongs } from '../../../lib/query/Backend';
import { querySongs } from '../../../lib/backend/queries';
export type SongMetadata = serverApi.SongDetails;
export type SongMetadataChanges = serverApi.ModifySongRequest;

@ -8,7 +8,7 @@ import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveTagChanges } from '../../../lib/saveChanges';
import { queryTags, querySongs } from '../../../lib/query/Backend';
import { queryTags, querySongs } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
var _ = require('lodash');

@ -1,5 +1,5 @@
import * as serverApi from '../../api';
import { QueryElem, toApiQuery } from './Query';
import { QueryElem, toApiQuery } from '../query/Query';
export interface QueryArgs {
query?: QueryElem,

@ -0,0 +1,45 @@
import * as serverApi from '../../api';
export async function createTag(details: serverApi.CreateTagRequest) {
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(details),
};
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.CreateTagEndpoint, requestOpts)
if (!response.ok) {
throw new Error("Response to tag creation not OK: " + JSON.stringify(response));
}
return await response.json();
}
export async function modifyTag(id: number, details: serverApi.ModifyTagRequest) {
const requestOpts = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(details),
};
const response = await fetch(
(process.env.REACT_APP_BACKEND || "") + serverApi.ModifyTagEndpoint.replace(':id', id.toString()),
requestOpts
);
if (!response.ok) {
throw new Error("Response to tag modification not OK: " + JSON.stringify(response));
}
}
export async function deleteTag(id: number) {
const requestOpts = {
method: 'DELETE',
};
const response = await fetch(
(process.env.REACT_APP_BACKEND || "") + serverApi.DeleteTagEndpoint.replace(':id', id.toString()),
requestOpts
);
if (!response.ok) {
throw new Error("Response to tag deletion not OK: " + JSON.stringify(response));
}
}

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

@ -0,0 +1,64 @@
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import Knex from 'knex';
export const DeleteTagEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkDeleteTagRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid DeleteTag request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.DeleteTagRequest = req.body;
console.log("Delete Tag:", reqObject);
await knex.transaction(async (trx) => {
try {
// Start retrieving any child tags.
const childTagsPromise = trx.select('id')
.from('tags')
.where({ 'parentId': req.params.id });
// Start retrieving the tag itself.
const tagPromise = trx.select('id')
.from('tags')
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Wait for the requests to finish.
var [tag, children] = await Promise.all([tagPromise, childTagsPromise]);
// If there are any children, refuse the deletion
if(children && children.length > 0) {
const e: EndpointError = {
internalMessage: 'Invalid DeleteTag request: tag ' + req.params.id.toString() + " has children",
httpStatus: 400
};
throw e;
}
// Check that we found all objects we need.
if (!tag) {
const e: EndpointError = {
internalMessage: 'Tag or parent does not exist for DeleteTag request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
// Delete the tag.
await trx('tags')
.where({ 'id': req.params.id })
.del();
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
}
Loading…
Cancel
Save