diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index a63d73e..d600867 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -2,14 +2,14 @@ import React, { useReducer, Reducer } from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import AppBar from './appbar/AppBar'; -import QueryWindow from './windows/QueryWindow'; +import QueryWindow from './windows/query/QueryWindow'; import { NewTabProps } from './appbar/AddTabMenu'; import { newWindowState, newWindowReducer, WindowType } from './windows/Windows'; -import ArtistWindow from './windows/ArtistWindow'; -import AlbumWindow from './windows/AlbumWindow'; -import TagWindow from './windows/TagWindow'; -import SongWindow from './windows/SongWindow'; -import ManageTagsWindow from './windows/ManageTagsWindow'; +import ArtistWindow from './windows/artist/ArtistWindow'; +import AlbumWindow from './windows/album/AlbumWindow'; +import TagWindow from './windows/tag/TagWindow'; +import SongWindow from './windows/song/SongWindow'; +import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow'; var _ = require('lodash'); const darkTheme = createMuiTheme({ diff --git a/client/src/components/querybuilder/QBSelectWithRequest.tsx b/client/src/components/querybuilder/QBSelectWithRequest.tsx index dd1b115..107e982 100644 --- a/client/src/components/querybuilder/QBSelectWithRequest.tsx +++ b/client/src/components/querybuilder/QBSelectWithRequest.tsx @@ -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(() => { startRequest(input); }, [input]); diff --git a/client/src/components/windows/Windows.tsx b/client/src/components/windows/Windows.tsx index 4fc87a7..1411e33 100644 --- a/client/src/components/windows/Windows.tsx +++ b/client/src/components/windows/Windows.tsx @@ -1,17 +1,17 @@ import React from 'react'; -import { QueryWindowReducer } from "./QueryWindow"; -import { ArtistWindowReducer } from "./ArtistWindow"; +import { QueryWindowReducer } from "./query/QueryWindow"; +import { ArtistWindowReducer } from "./artist/ArtistWindow"; import SearchIcon from '@material-ui/icons/Search'; import PersonIcon from '@material-ui/icons/Person'; import AlbumIcon from '@material-ui/icons/Album'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import LoyaltyIcon from '@material-ui/icons/Loyalty'; -import { SongWindowReducer } from './SongWindow'; -import { AlbumWindowReducer } from './AlbumWindow'; -import { TagWindowReducer } from './TagWindow'; +import { SongWindowReducer } from './song/SongWindow'; +import { AlbumWindowReducer } from './album/AlbumWindow'; +import { TagWindowReducer } from './tag/TagWindow'; import { songGetters } from '../../lib/songGetters'; -import { ManageTagsWindowReducer } from './ManageTagsWindow'; +import { ManageTagsWindowReducer } from './manage_tags/ManageTagsWindow'; export enum WindowType { Query = "Query", diff --git a/client/src/components/windows/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx similarity index 94% rename from client/src/components/windows/AlbumWindow.tsx rename to client/src/components/windows/album/AlbumWindow.tsx index 6cefcb8..2fd709c 100644 --- a/client/src/components/windows/AlbumWindow.tsx +++ b/client/src/components/windows/album/AlbumWindow.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState } from 'react'; import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import AlbumIcon from '@material-ui/icons/Album'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import SongTable, { SongGetters } from '../tables/ResultsTable'; -import { saveAlbumChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../../tables/ResultsTable'; +import { saveAlbumChanges } from '../../../lib/saveChanges'; var _ = require('lodash'); export type AlbumMetadata = serverApi.AlbumDetails; diff --git a/client/src/components/windows/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx similarity index 94% rename from client/src/components/windows/ArtistWindow.tsx rename to client/src/components/windows/artist/ArtistWindow.tsx index 7b8ed4c..263f6e3 100644 --- a/client/src/components/windows/ArtistWindow.tsx +++ b/client/src/components/windows/artist/ArtistWindow.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState } from 'react'; import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; import PersonIcon from '@material-ui/icons/Person'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import SongTable, { SongGetters } from '../tables/ResultsTable'; -import { saveArtistChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../../tables/ResultsTable'; +import { saveArtistChanges } from '../../../lib/saveChanges'; var _ = require('lodash'); export type ArtistMetadata = serverApi.ArtistDetails; diff --git a/client/src/components/windows/manage_tags/ManageTagMenu.tsx b/client/src/components/windows/manage_tags/ManageTagMenu.tsx new file mode 100644 index 0000000..08bd55b --- /dev/null +++ b/client/src/components/windows/manage_tags/ManageTagMenu.tsx @@ -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 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 + + { + props.onClose(); + props.onRename(s); + }} + /> + + +} \ No newline at end of file diff --git a/client/src/components/windows/ManageTagsWindow.tsx b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx similarity index 61% rename from client/src/components/windows/ManageTagsWindow.tsx rename to client/src/components/windows/manage_tags/ManageTagsWindow.tsx index db2f00f..8fc2514 100644 --- a/client/src/components/windows/ManageTagsWindow.tsx +++ b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { WindowState } from './Windows'; -import { Box, Typography, Chip, IconButton } from '@material-ui/core'; -import * as serverApi from '../../api'; +import { WindowState } from '../Windows'; +import { Box, Typography, Chip, IconButton, useTheme } from '@material-ui/core'; +import * as serverApi from '../../../api'; 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'; var _ = require('lodash'); export enum TagChangeType { @@ -17,12 +18,13 @@ export enum TagChangeType { export interface TagChange { 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, } export interface ManageTagsWindowState extends WindowState { - fetchedTags: any[] | null, + fetchedTags: Record | null, pendingChanges: TagChange[], } @@ -48,8 +50,8 @@ export function ManageTagsWindowReducer(state: ManageTagsWindowState, action: an } } -export function organiseTags(allTags: any[], fromId: number | null): any[] { - const base = allTags.filter((tag: any) => +export function organiseTags(allTags: Record, fromId: number | null): any[] { + const base = Object.values(allTags).filter((tag: any) => (fromId === null && !tag.parentId) || (tag.parentId && tag.parentId === fromId) ); @@ -87,18 +89,37 @@ export async function getAllTags() { return (async () => { const response = await fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) let json: any = await response.json(); - return json.tags; + var retval: Record = {}; + json.tags.forEach((tag: any) => { + retval[tag.tagId] = tag; + }); + return retval; })(); } export function SingleTag(props: { tag: any, prependElems: any[], + dispatch: (action: any) => void, + state: ManageTagsWindowState, }) { const tag = props.tag; const hasChildren = 'children' in tag && tag.children.length > 0; + const [menuAnchorEl, setMenuAnchorEl] = React.useState(null); const [expanded, setExpanded] = useState(false); + const theme = useTheme(); + + const onOpenMenu = (event: any) => { + setMenuAnchorEl(event.currentTarget); + }; + const onCloseMenu = () => { + setMenuAnchorEl(null); + }; + + const tagLabel = ("proposedName" in tag) ? + <>{tag.name}→{tag.proposedName} : + tag.name; const expandArrow = expanded ? setExpanded(false)}> : @@ -109,7 +130,7 @@ export function SingleTag(props: { > ; @@ -121,7 +142,8 @@ export function SingleTag(props: { {props.prependElems} {hasChildren && expanded && tag.children.map((child: any) => , /]} + dispatch={props.dispatch} + state={props.state} />)} + { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [ + ...props.state.pendingChanges, + { + type: TagChangeType.Rename, + name: s, + id: tag.tagId, + } + ] + }) + }} + tag={tag} + /> } +function addTagChanges(tags: Record, 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 { state: ManageTagsWindowState, dispatch: (action: any) => void, @@ -150,12 +206,13 @@ export default function ManageTagsWindow(props: IProps) { // them hierarchically by giving each tag a "children" prop. props.dispatch({ type: ManageTagsWindowActions.SetFetchedTags, - value: organiseTags(allTags, null), + value: allTags, }); })(); }, [props.state.fetchedTags]); - const tags = props.state.fetchedTags || []; + const tagsWithChanges = addTagChanges(props.state.fetchedTags || {}, props.state.pendingChanges) + const tags = organiseTags(tagsWithChanges, null); return {tags && tags.length && tags.map((tag: any) => { - return ; + return ; })} diff --git a/client/src/components/windows/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx similarity index 92% rename from client/src/components/windows/QueryWindow.tsx rename to client/src/components/windows/query/QueryWindow.tsx index e35d233..1a3fe75 100644 --- a/client/src/components/windows/QueryWindow.tsx +++ b/client/src/components/windows/query/QueryWindow.tsx @@ -1,13 +1,13 @@ import React, { useEffect } from 'react'; import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; -import { QueryElem, toApiQuery } from '../../lib/query/Query'; -import QueryBuilder from '../querybuilder/QueryBuilder'; -import * as serverApi from '../../api'; -import SongTable from '../tables/ResultsTable'; -import { songGetters } from '../../lib/songGetters'; -import { getArtists, getSongTitles, getAlbums, getTags } from '../../lib/query/Getters'; +import { QueryElem, toApiQuery } from '../../../lib/query/Query'; +import QueryBuilder from '../../querybuilder/QueryBuilder'; +import * as serverApi from '../../../api'; +import SongTable from '../../tables/ResultsTable'; +import { songGetters } from '../../../lib/songGetters'; +import { getArtists, getSongTitles, getAlbums, getTags } from '../../../lib/query/Getters'; import { grey } from '@material-ui/core/colors'; -import { WindowState } from './Windows'; +import { WindowState } from '../Windows'; var _ = require('lodash'); const darkTheme = createMuiTheme({ diff --git a/client/src/components/windows/SongWindow.tsx b/client/src/components/windows/song/SongWindow.tsx similarity index 93% rename from client/src/components/windows/SongWindow.tsx rename to client/src/components/windows/song/SongWindow.tsx index bf69c84..cae3a1f 100644 --- a/client/src/components/windows/SongWindow.tsx +++ b/client/src/components/windows/song/SongWindow.tsx @@ -3,14 +3,14 @@ import { Box, Typography, IconButton, Button, CircularProgress } from '@material import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import PersonIcon from '@material-ui/icons/Person'; import AlbumIcon from '@material-ui/icons/Album'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import { ArtistMetadata } from './ArtistWindow'; -import { AlbumMetadata } from './AlbumWindow'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import { saveSongChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import { ArtistMetadata } from '../artist/ArtistWindow'; +import { AlbumMetadata } from '../album/AlbumWindow'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import { saveSongChanges } from '../../../lib/saveChanges'; export type SongMetadata = serverApi.SongDetails; export type SongMetadataChanges = serverApi.ModifySongRequest; diff --git a/client/src/components/windows/TagWindow.tsx b/client/src/components/windows/tag/TagWindow.tsx similarity index 95% rename from client/src/components/windows/TagWindow.tsx rename to client/src/components/windows/tag/TagWindow.tsx index 676aef5..8de6115 100644 --- a/client/src/components/windows/TagWindow.tsx +++ b/client/src/components/windows/tag/TagWindow.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState } from 'react'; import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; -import * as serverApi from '../../api'; -import { WindowState } from './Windows'; -import StoreLinkIcon, { whichStore } from '../common/StoreLinkIcon'; -import EditableText from '../common/EditableText'; -import SubmitChangesButton from '../common/SubmitChangesButton'; -import SongTable, { SongGetters } from '../tables/ResultsTable'; -import { saveTagChanges } from '../../lib/saveChanges'; +import * as serverApi from '../../../api'; +import { WindowState } from '../Windows'; +import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon'; +import EditableText from '../../common/EditableText'; +import SubmitChangesButton from '../../common/SubmitChangesButton'; +import SongTable, { SongGetters } from '../../tables/ResultsTable'; +import { saveTagChanges } from '../../../lib/saveChanges'; var _ = require('lodash'); export interface FullTagMetadata extends serverApi.TagDetails {