Bit of refactoring. A rename change is possible (without committing)

pull/24/head
Sander Vocke 5 years ago
parent 0e1138cba1
commit e19ebeffaa
  1. 12
      client/src/components/MainWindow.tsx
  2. 12
      client/src/components/querybuilder/QBSelectWithRequest.tsx
  3. 12
      client/src/components/windows/Windows.tsx
  4. 14
      client/src/components/windows/album/AlbumWindow.tsx
  5. 14
      client/src/components/windows/artist/ArtistWindow.tsx
  6. 61
      client/src/components/windows/manage_tags/ManageTagMenu.tsx
  7. 88
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  8. 14
      client/src/components/windows/query/QueryWindow.tsx
  9. 16
      client/src/components/windows/song/SongWindow.tsx
  10. 14
      client/src/components/windows/tag/TagWindow.tsx

@ -2,14 +2,14 @@ import React, { useReducer, Reducer } from 'react';
import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core';
import { grey } from '@material-ui/core/colors'; import { grey } from '@material-ui/core/colors';
import AppBar from './appbar/AppBar'; import AppBar from './appbar/AppBar';
import QueryWindow from './windows/QueryWindow'; import QueryWindow from './windows/query/QueryWindow';
import { NewTabProps } from './appbar/AddTabMenu'; import { NewTabProps } from './appbar/AddTabMenu';
import { newWindowState, newWindowReducer, WindowType } from './windows/Windows'; import { newWindowState, newWindowReducer, WindowType } from './windows/Windows';
import ArtistWindow from './windows/ArtistWindow'; import ArtistWindow from './windows/artist/ArtistWindow';
import AlbumWindow from './windows/AlbumWindow'; import AlbumWindow from './windows/album/AlbumWindow';
import TagWindow from './windows/TagWindow'; import TagWindow from './windows/tag/TagWindow';
import SongWindow from './windows/SongWindow'; import SongWindow from './windows/song/SongWindow';
import ManageTagsWindow from './windows/ManageTagsWindow'; import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow';
var _ = require('lodash'); var _ = require('lodash');
const darkTheme = createMuiTheme({ const darkTheme = createMuiTheme({

@ -43,18 +43,6 @@ export default function QBSelectWithRequest(props: IProps & any) {
})(); })();
}; };
// // Ensure a new request is made whenever the loading option is enabled.
// useEffect(() => {
// startRequest(input);
// }, []);
// Ensure options are cleared whenever the element is closed.
// useEffect(() => {
// if (!open) {
// setOptions(null);
// }
// }, [open]);
useEffect(() => { useEffect(() => {
startRequest(input); startRequest(input);
}, [input]); }, [input]);

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import { QueryWindowReducer } from "./QueryWindow"; import { QueryWindowReducer } from "./query/QueryWindow";
import { ArtistWindowReducer } from "./ArtistWindow"; import { ArtistWindowReducer } from "./artist/ArtistWindow";
import SearchIcon from '@material-ui/icons/Search'; import SearchIcon from '@material-ui/icons/Search';
import PersonIcon from '@material-ui/icons/Person'; import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album'; import AlbumIcon from '@material-ui/icons/Album';
import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import LoyaltyIcon from '@material-ui/icons/Loyalty'; import LoyaltyIcon from '@material-ui/icons/Loyalty';
import { SongWindowReducer } from './SongWindow'; import { SongWindowReducer } from './song/SongWindow';
import { AlbumWindowReducer } from './AlbumWindow'; import { AlbumWindowReducer } from './album/AlbumWindow';
import { TagWindowReducer } from './TagWindow'; import { TagWindowReducer } from './tag/TagWindow';
import { songGetters } from '../../lib/songGetters'; import { songGetters } from '../../lib/songGetters';
import { ManageTagsWindowReducer } from './ManageTagsWindow'; import { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow';
export enum WindowType { export enum WindowType {
Query = "Query", Query = "Query",

@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core';
import AlbumIcon from '@material-ui/icons/Album'; import AlbumIcon from '@material-ui/icons/Album';
import * as serverApi from '../../api'; import * as serverApi from '../../../api';
import { WindowState } from './Windows'; import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../tables/ResultsTable'; import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveAlbumChanges } from '../../lib/saveChanges'; import { saveAlbumChanges } from '../../../lib/saveChanges';
var _ = require('lodash'); var _ = require('lodash');
export type AlbumMetadata = serverApi.AlbumDetails; export type AlbumMetadata = serverApi.AlbumDetails;

@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core';
import PersonIcon from '@material-ui/icons/Person'; import PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../../api'; import * as serverApi from '../../../api';
import { WindowState } from './Windows'; import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../tables/ResultsTable'; import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveArtistChanges } from '../../lib/saveChanges'; import { saveArtistChanges } from '../../../lib/saveChanges';
var _ = require('lodash'); var _ = require('lodash');
export type ArtistMetadata = serverApi.ArtistDetails; export type ArtistMetadata = serverApi.ArtistDetails;

@ -0,0 +1,61 @@
import React, { useState } from 'react';
import { Menu, MenuItem, TextField, Input } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item";
export function MenuEditText(props: {
label: string,
onSubmit: (s: string) => void,
}) {
const [input, setInput] = useState("");
return <TextField
label={props.label}
variant="outlined"
value={input}
onChange={(e: any) => setInput(e.target.value)}
onKeyDown={(e: any) => {
// Prevent the event from propagating, because
// that would trigger keyboard navigation of the menu.
e.stopPropagation();
if (e.key === 'Enter') {
// User submitted free-form value.
props.onSubmit(input);
}
}}
/>
}
export interface IProps {
anchorEl: null | HTMLElement,
onClose: () => void,
onRename: (s: string) => void,
tag: any,
}
export default function ManageTagMenu(props: IProps) {
const anchorEl = props.anchorEl;
const onRename = (name: string) => {
}
return <Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(props.anchorEl)}
onClose={props.onClose}
>
<NestedMenuItem
parentMenuOpen={Boolean(anchorEl)}
label="Rename"
>
<MenuEditText
label="New name"
onSubmit={(s: string) => {
props.onClose();
props.onRename(s);
}}
/>
</NestedMenuItem>
</Menu>
}

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { WindowState } from './Windows'; import { WindowState } from '../Windows';
import { Box, Typography, Chip, IconButton } from '@material-ui/core'; import { Box, Typography, Chip, IconButton, useTheme } from '@material-ui/core';
import * as serverApi from '../../api'; import * as serverApi from '../../../api';
import LoyaltyIcon from '@material-ui/icons/Loyalty'; import LoyaltyIcon from '@material-ui/icons/Loyalty';
import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ManageTagMenu from './ManageTagMenu';
var _ = require('lodash'); var _ = require('lodash');
export enum TagChangeType { export enum TagChangeType {
@ -17,12 +18,13 @@ export enum TagChangeType {
export interface TagChange { export interface TagChange {
type: TagChangeType, type: TagChangeType,
parent?: number | string, // Number if MuDBase ID, string if UUID for new tag not in DB yet. id: number, // MuDBase ID. If not in database yet, negative IDs will be used until submitted.
parent?: number, // MuDBase ID. If not in database yet, negative IDs will be used until submitted.
name?: string, name?: string,
} }
export interface ManageTagsWindowState extends WindowState { export interface ManageTagsWindowState extends WindowState {
fetchedTags: any[] | null, fetchedTags: Record<number, any> | null,
pendingChanges: TagChange[], pendingChanges: TagChange[],
} }
@ -48,8 +50,8 @@ export function ManageTagsWindowReducer(state: ManageTagsWindowState, action: an
} }
} }
export function organiseTags(allTags: any[], fromId: number | null): any[] { export function organiseTags(allTags: Record<number, any>, fromId: number | null): any[] {
const base = allTags.filter((tag: any) => const base = Object.values(allTags).filter((tag: any) =>
(fromId === null && !tag.parentId) || (fromId === null && !tag.parentId) ||
(tag.parentId && tag.parentId === fromId) (tag.parentId && tag.parentId === fromId)
); );
@ -87,18 +89,37 @@ export async function getAllTags() {
return (async () => { return (async () => {
const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json(); let json: any = await response.json();
return json.tags; var retval: Record<number, any> = {};
json.tags.forEach((tag: any) => {
retval[tag.tagId] = tag;
});
return retval;
})(); })();
} }
export function SingleTag(props: { export function SingleTag(props: {
tag: any, tag: any,
prependElems: any[], prependElems: any[],
dispatch: (action: any) => void,
state: ManageTagsWindowState,
}) { }) {
const tag = props.tag; const tag = props.tag;
const hasChildren = 'children' in tag && tag.children.length > 0; const hasChildren = 'children' in tag && tag.children.length > 0;
const [menuAnchorEl, setMenuAnchorEl] = React.useState<null | HTMLElement>(null);
const [expanded, setExpanded] = useState<Boolean>(false); const [expanded, setExpanded] = useState<Boolean>(false);
const theme = useTheme();
const onOpenMenu = (event: any) => {
setMenuAnchorEl(event.currentTarget);
};
const onCloseMenu = () => {
setMenuAnchorEl(null);
};
const tagLabel = ("proposedName" in tag) ?
<><del style={{ color: theme.palette.text.secondary }}>{tag.name}</del>{tag.proposedName}</> :
tag.name;
const expandArrow = expanded ? const expandArrow = expanded ?
<IconButton size="small" onClick={() => setExpanded(false)}><ArrowDropDownIcon /></IconButton> : <IconButton size="small" onClick={() => setExpanded(false)}><ArrowDropDownIcon /></IconButton> :
@ -109,7 +130,7 @@ export function SingleTag(props: {
> >
<Chip <Chip
size="small" size="small"
label={props.tag.name} label={tagLabel}
/> />
</Box>; </Box>;
@ -121,7 +142,8 @@ export function SingleTag(props: {
{props.prependElems} {props.prependElems}
<Chip <Chip
size="small" size="small"
label={tag.name} label={tagLabel}
onClick={onOpenMenu}
/> />
</Box> </Box>
{hasChildren && expanded && tag.children.map((child: any) => <SingleTag {hasChildren && expanded && tag.children.map((child: any) => <SingleTag
@ -129,10 +151,44 @@ export function SingleTag(props: {
prependElems={[...props.prependElems, prependElems={[...props.prependElems,
<GreyedTag tag={tag} />, <GreyedTag tag={tag} />,
<Typography variant="h5">/</Typography>]} <Typography variant="h5">/</Typography>]}
dispatch={props.dispatch}
state={props.state}
/>)} />)}
<ManageTagMenu
anchorEl={menuAnchorEl}
onClose={onCloseMenu}
onRename={(s: string) => {
props.dispatch({
type: ManageTagsWindowActions.SetPendingChanges,
value: [
...props.state.pendingChanges,
{
type: TagChangeType.Rename,
name: s,
id: tag.tagId,
}
]
})
}}
tag={tag}
/>
</> </>
} }
function addTagChanges(tags: Record<number, any>, changes: TagChange[]) {
var retval = tags;
changes.forEach((change: TagChange) => {
switch (change.type) {
case TagChangeType.Rename:
retval[change.id].proposedName = change.name;
break;
default:
throw new Error("Unimplemented tag change")
}
})
return retval;
}
export interface IProps { export interface IProps {
state: ManageTagsWindowState, state: ManageTagsWindowState,
dispatch: (action: any) => void, dispatch: (action: any) => void,
@ -150,12 +206,13 @@ export default function ManageTagsWindow(props: IProps) {
// them hierarchically by giving each tag a "children" prop. // them hierarchically by giving each tag a "children" prop.
props.dispatch({ props.dispatch({
type: ManageTagsWindowActions.SetFetchedTags, type: ManageTagsWindowActions.SetFetchedTags,
value: organiseTags(allTags, null), value: allTags,
}); });
})(); })();
}, [props.state.fetchedTags]); }, [props.state.fetchedTags]);
const tags = props.state.fetchedTags || []; const tagsWithChanges = addTagChanges(props.state.fetchedTags || {}, props.state.pendingChanges)
const tags = organiseTags(tagsWithChanges, null);
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box <Box
@ -178,7 +235,12 @@ export default function ManageTagsWindow(props: IProps) {
width="80%" width="80%"
> >
{tags && tags.length && tags.map((tag: any) => { {tags && tags.length && tags.map((tag: any) => {
return <SingleTag tag={tag} prependElems={[]} />; return <SingleTag
tag={tag}
prependElems={[]}
dispatch={props.dispatch}
state={props.state}
/>;
})} })}
</Box> </Box>
</Box> </Box>

@ -1,13 +1,13 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; import { createMuiTheme, Box, LinearProgress } from '@material-ui/core';
import { QueryElem, toApiQuery } from '../../lib/query/Query'; import { QueryElem, toApiQuery } from '../../../lib/query/Query';
import QueryBuilder from '../querybuilder/QueryBuilder'; import QueryBuilder from '../../querybuilder/QueryBuilder';
import * as serverApi from '../../api'; import * as serverApi from '../../../api';
import SongTable from '../tables/ResultsTable'; import SongTable from '../../tables/ResultsTable';
import { songGetters } from '../../lib/songGetters'; import { songGetters } from '../../../lib/songGetters';
import { getArtists, getSongTitles, getAlbums, getTags } from '../../lib/query/Getters'; import { getArtists, getSongTitles, getAlbums, getTags } from '../../../lib/query/Getters';
import { grey } from '@material-ui/core/colors'; import { grey } from '@material-ui/core/colors';
import { WindowState } from './Windows'; import { WindowState } from '../Windows';
var _ = require('lodash'); var _ = require('lodash');
const darkTheme = createMuiTheme({ const darkTheme = createMuiTheme({

@ -3,14 +3,14 @@ import { Box, Typography, IconButton, Button, CircularProgress } from '@material
import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import AudiotrackIcon from '@material-ui/icons/Audiotrack';
import PersonIcon from '@material-ui/icons/Person'; import PersonIcon from '@material-ui/icons/Person';
import AlbumIcon from '@material-ui/icons/Album'; import AlbumIcon from '@material-ui/icons/Album';
import * as serverApi from '../../api'; import * as serverApi from '../../../api';
import { WindowState } from './Windows'; import { WindowState } from '../Windows';
import { ArtistMetadata } from './ArtistWindow'; import { ArtistMetadata } from '../artist/ArtistWindow';
import { AlbumMetadata } from './AlbumWindow'; import { AlbumMetadata } from '../album/AlbumWindow';
import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import { saveSongChanges } from '../../lib/saveChanges'; import { saveSongChanges } from '../../../lib/saveChanges';
export type SongMetadata = serverApi.SongDetails; export type SongMetadata = serverApi.SongDetails;
export type SongMetadataChanges = serverApi.ModifySongRequest; export type SongMetadataChanges = serverApi.ModifySongRequest;

@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core';
import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import * as serverApi from '../../api'; import * as serverApi from '../../../api';
import { WindowState } from './Windows'; import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../common/EditableText'; import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../common/SubmitChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton';
import SongTable, { SongGetters } from '../tables/ResultsTable'; import SongTable, { SongGetters } from '../../tables/ResultsTable';
import { saveTagChanges } from '../../lib/saveChanges'; import { saveTagChanges } from '../../../lib/saveChanges';
var _ = require('lodash'); var _ = require('lodash');
export interface FullTagMetadata extends serverApi.TagDetails { export interface FullTagMetadata extends serverApi.TagDetails {
Loading…
Cancel
Save