Add edit dialogs for all item types. Names and store links can be edited.

master
Sander Vocke 4 years ago
parent df60e91bf3
commit cda62f0a80
  1. 118
      client/src/components/common/EditItemDialog.tsx
  2. 93
      client/src/components/common/EditableText.tsx
  3. 126
      client/src/components/common/ExternalLinksEditor.tsx
  4. 111
      client/src/components/windows/album/AlbumWindow.tsx
  5. 110
      client/src/components/windows/artist/ArtistWindow.tsx
  6. 97
      client/src/components/windows/tag/TagWindow.tsx
  7. 14
      client/src/components/windows/track/TrackWindow.tsx
  8. 2
      client/src/lib/backend/tags.tsx

@ -0,0 +1,118 @@
import React, { useState } from 'react';
import { Button, Dialog, DialogActions, Divider, Typography, Box, TextField, IconButton } from "@material-ui/core";
import { ExternalLinksEditor } from './ExternalLinksEditor';
import UndoIcon from '@material-ui/icons/Undo';
import { ResourceType } from '../../api/api';
let _ = require('lodash')
export enum EditablePropertyType {
Text = 0,
}
export interface EditableProperty {
metadataKey: string,
title: string,
type: EditablePropertyType
}
function EditTextProperty(props: {
title: string,
originalValue: string,
currentValue: string,
onChange: (v: string) => void
}) {
return <Box display="flex" alignItems="center" width="100%">
<TextField
// Here we "abuse" the label to show the original title.
// emptying the text box means going back to the original.
variant="outlined"
value={props.currentValue}
label={props.title}
helperText={(props.currentValue != props.originalValue) &&
"Current: " + props.originalValue || undefined}
error={(props.currentValue != props.originalValue)}
onChange={(e: any) => {
props.onChange((e.target.value == "") ?
props.originalValue : e.target.value)
}}
fullWidth={true}
/>
{props.currentValue != props.originalValue && <IconButton
onClick={() => {
props.onChange(props.originalValue)
}}
><UndoIcon /></IconButton>}
</Box>
}
function PropertyEditor(props: {
originalMetadata: any,
currentMetadata: any,
onChange: (metadata: any) => void,
editableProperties: EditableProperty[]
}) {
return <Box display="flex" width="100%">
{props.editableProperties.map(
(p: EditableProperty) => {
if (p.type == EditablePropertyType.Text) {
return <EditTextProperty
title={p.title}
originalValue={props.originalMetadata[p.metadataKey]}
currentValue={props.currentMetadata[p.metadataKey]}
onChange={(v: string) => props.onChange({ ...props.currentMetadata, [p.metadataKey]: v })}
/>
}
return undefined;
}
)}
</Box >
}
export default function EditItemDialog(props: {
open: boolean,
onClose: () => void,
onSubmit: (v: any) => void,
id: number,
metadata: any,
defaultExternalLinksQuery: string,
editableProperties: EditableProperty[],
resourceType: ResourceType,
editStoreLinks: boolean,
}) {
let [editingMetadata, setEditingMetadata] = useState<any>(props.metadata);
return <Dialog
maxWidth="lg"
fullWidth
open={props.open}
onClose={props.onClose}
disableBackdropClick={true}>
<Typography variant="h5">Properties</Typography>
<PropertyEditor
originalMetadata={props.metadata}
currentMetadata={editingMetadata}
onChange={setEditingMetadata}
editableProperties={props.editableProperties}
/>
{props.editStoreLinks && <><Divider />
<Typography variant="h5">External Links</Typography>
<ExternalLinksEditor
metadata={editingMetadata}
original={props.metadata}
onChange={(v: any) => setEditingMetadata(v)}
defaultQuery={props.defaultExternalLinksQuery}
resourceType={props.resourceType}
/></>}
<Divider />
{!_.isEqual(editingMetadata, props.metadata) && <DialogActions>
<Button variant="contained" color="secondary"
onClick={() => {
props.onSubmit(editingMetadata);
props.onClose();
}}>Save all changes</Button>
<Button variant="outlined"
onClick={() => setEditingMetadata(props.metadata)}>Discard changes</Button>
</DialogActions>}
</Dialog>
}

@ -1,93 +0,0 @@
import React, { useState } from 'react';
import { Box, IconButton, TextField } from '@material-ui/core';
import EditIcon from '@material-ui/icons/Edit';
import CheckIcon from '@material-ui/icons/Check';
import UndoIcon from '@material-ui/icons/Undo';
import { useTheme } from '@material-ui/core/styles';
// This component is an editable text. It shows up as normal text,
// but will display an edit icon on hover. When clicked, this
// enables a text input to make a new suggestion.
// The text can show a striked-through version of the old text,
// with the new value next to it and an undo button.
export interface IProps {
defaultValue: string,
changedValue: string | null, // Null == not changed
editingValue: string | null, // Null == not editing
editingLabel: string,
onChangeEditingValue: (v: string | null) => void,
onChangeChangedValue: (v: string | null) => void,
}
export default function EditableText(props: IProps) {
let editingValue = props.editingValue;
let defaultValue = props.defaultValue;
let changedValue = props.changedValue;
let onChangeEditingValue = props.onChangeEditingValue;
let onChangeChangedValue = props.onChangeChangedValue;
let editing = editingValue !== null;
let editingLabel = props.editingLabel;
const theme = useTheme();
const [hovering, setHovering] = useState<Boolean>(false);
const editButton = <Box
visibility={(hovering && !editing) ? "visible" : "hidden"}>
<IconButton
onClick={() => onChangeEditingValue(changedValue || defaultValue)}
>
<EditIcon />
</IconButton>
</Box>
const discardChangesButton = <Box
visibility={(hovering && !editing) ? "visible" : "hidden"}>
<IconButton
onClick={() => {
onChangeChangedValue(null);
onChangeEditingValue(null);
}}
>
<UndoIcon />
</IconButton>
</Box>
if (editing) {
return <Box display="flex" alignItems="center">
<TextField
variant="outlined"
value={editingValue || ""}
label={editingLabel}
inputProps={{ style: { fontSize: '2rem' } }}
onChange={(e: any) => onChangeEditingValue(e.target.value)}
/>
<IconButton
onClick={() => {
onChangeChangedValue(editingValue === defaultValue ? null : editingValue);
onChangeEditingValue(null);
}}
><CheckIcon /></IconButton>
</Box>
} else if (changedValue) {
return <Box
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
display="flex"
alignItems="center"
>
<del style={{ color: theme.palette.text.secondary }}>{defaultValue}</del>
{changedValue}
{editButton}
{discardChangesButton}
</Box>
}
return <Box
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
display="flex"
alignItems="center"
>{defaultValue}{editButton}</Box>;
}

@ -1,33 +1,33 @@
import { IntegrationWith, Name, ResourceType, StoreLinks } from '../../api/api';
import { IntegrationState, useIntegrations } from '../../lib/integration/useIntegrations';
import StoreLinkIcon, { whichStore } from './StoreLinkIcon';
import { $enum } from "ts-enum-util";
import React, { useEffect, useState } from 'react';
import { AppBar, Box, Button, Dialog, DialogActions, Divider, FormControl, FormControlLabel, IconButton, Link, List, ListItem, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, Select, Tab, Tabs, TextField, Typography } from "@material-ui/core";
import { TrackMetadata } from "./TrackWindow";
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import { IntegrationAlbum, IntegrationArtist, IntegrationFeature, IntegrationTrack } from '../../lib/integration/Integration';
import { Box, List, ListItem, ListItemIcon, ListItemText, IconButton, Typography, FormControl, FormControlLabel, MenuItem, Radio, RadioGroup, Select, TextField } from '@material-ui/core';
import CheckIcon from '@material-ui/icons/Check';
import SearchIcon from '@material-ui/icons/Search';
import CancelIcon from '@material-ui/icons/Cancel';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import DeleteIcon from '@material-ui/icons/Delete';
import { $enum } from "ts-enum-util";
import { useIntegrations, IntegrationsState, IntegrationState } from '../../../lib/integration/useIntegrations';
import { IntegrationFeature, IntegrationTrack } from '../../../lib/integration/Integration';
import { TabPanel } from '@material-ui/lab';
import { v1 } from 'uuid';
import { IntegrationWith } from '../../../api/api';
let _ = require('lodash')
export type ItemWithExternalLinksProperties = StoreLinks & Name;
export function ProvideLinksWidget(props: {
providers: IntegrationState[],
metadata: TrackMetadata,
metadata: ItemWithExternalLinksProperties,
store: IntegrationWith,
onChange: (link: string | undefined) => void,
defaultQuery: string,
resourceType: ResourceType,
}) {
let defaultQuery = `${props.metadata.name}${props.metadata.artists && ` ${props.metadata.artists[0].name}`}${props.metadata.album && ` ${props.metadata.album.name}`}`;
let [selectedProviderIdx, setSelectedProviderIdx] = useState<number | undefined>(
props.providers.length > 0 ? 0 : undefined
);
let [query, setQuery] = useState<string>(defaultQuery)
let [results, setResults] = useState<IntegrationTrack[] | undefined>(undefined);
let [query, setQuery] = useState<string>(props.defaultQuery)
let [results, setResults] = useState<
IntegrationTrack[] | IntegrationAlbum[] | IntegrationArtist[] | undefined>(undefined);
let selectedProvider: IntegrationState | undefined = selectedProviderIdx !== undefined ?
props.providers[selectedProviderIdx] : undefined;
@ -39,7 +39,7 @@ export function ProvideLinksWidget(props: {
// Ensure results are cleared when input state changes.
useEffect(() => {
setResults(undefined);
setQuery(defaultQuery);
setQuery(props.defaultQuery);
}, [props.store, props.providers, props.metadata])
return <Box display="flex" flexDirection="column" alignItems="left">
@ -63,17 +63,44 @@ export function ProvideLinksWidget(props: {
/>
<IconButton
onClick={() => {
selectedProvider?.integration.searchTrack(query, 10)
.then((tracks: IntegrationTrack[]) => setResults(tracks))
switch (props.resourceType) {
case ResourceType.Track:
selectedProvider?.integration.searchTrack(query, 10)
.then((tracks: IntegrationTrack[]) => setResults(tracks))
break;
case ResourceType.Album:
selectedProvider?.integration.searchAlbum(query, 10)
.then((albums: IntegrationAlbum[]) => setResults(albums))
break;
case ResourceType.Artist:
selectedProvider?.integration.searchArtist(query, 10)
.then((artists: IntegrationArtist[]) => setResults(artists))
break;
}
}}
><SearchIcon /></IconButton>
{results && results.length > 0 && <Typography>Suggestions:</Typography>}
<FormControl>
<RadioGroup value={currentLink} onChange={(e: any) => props.onChange(e.target.value)}>
{results && results.map((result: IntegrationTrack, idx: number) => {
let pretty = `"${result.title}"
${result.artist && ` by ${result.artist.name}`}
${result.album && ` (${result.album.name})`}`;
{results && (results as any).map((result: IntegrationTrack | IntegrationAlbum | IntegrationArtist, idx: number) => {
var pretty = "";
switch (props.resourceType) {
case ResourceType.Track:
let rt = result as IntegrationTrack;
pretty = `"${rt.title}"
${rt.artist && ` by ${rt.artist.name}`}
${rt.album && ` (${rt.album.name})`}`;
break;
case ResourceType.Album:
let ral = result as IntegrationAlbum;
pretty = `"${ral.name}"
${ral.artist && ` by ${ral.artist.name}`}`;
break;
case ResourceType.Artist:
let rar = result as IntegrationArtist;
pretty = rar.name || "(Unknown Artist)";
break;
}
return <FormControlLabel
value={result.url || idx}
control={<Radio checked={(result.url || idx) === currentLink} />}
@ -92,14 +119,16 @@ export function ProvideLinksWidget(props: {
}
export function ExternalLinksEditor(props: {
metadata: TrackMetadata,
original: TrackMetadata,
onChange: (v: TrackMetadata) => void,
metadata: ItemWithExternalLinksProperties,
original: ItemWithExternalLinksProperties,
onChange: (v: any) => void,
defaultQuery: string,
resourceType: ResourceType,
}) {
let [selectedIdx, setSelectedIdx] = useState<number>(0);
let integrations = useIntegrations();
let getLinksSet = (metadata: TrackMetadata) => {
let getLinksSet = (metadata: ItemWithExternalLinksProperties) => {
return $enum(IntegrationWith).getValues().reduce((prev: any, store: string) => {
var maybeLink: string | null = null;
metadata.storeLinks && metadata.storeLinks.forEach((link: string) => {
@ -145,7 +174,7 @@ export function ExternalLinksEditor(props: {
>
<ListItemIcon>{linksSet[store] !== null ? <CheckIcon style={{ color: color }} /> : <CancelIcon style={{ color: color }} />}</ListItemIcon>
<ListItemIcon><StoreLinkIcon whichStore={store} /></ListItemIcon>
<ListItemText style={{ color: color }} primary={store} />
<ListItemText style={{ color: color }} primary={store} />
{maybeLink && <a href={maybeLink} target="_blank">
<ListItemIcon><IconButton><OpenInNewIcon style={{ color: color }} /></IconButton></ListItemIcon>
</a>}
@ -184,51 +213,10 @@ export function ExternalLinksEditor(props: {
})
}
}}
defaultQuery={props.defaultQuery}
resourceType={props.resourceType}
/>
}
</Box>
</Box >
}
export default function EditTrackDialog(props: {
open: boolean,
onClose: () => void,
onSubmit: (v: TrackMetadata) => void,
id: number,
metadata: TrackMetadata,
}) {
enum EditTrackTabs {
Details = 0,
ExternalLinks,
}
let [editingMetadata, setEditingMetadata] = useState<TrackMetadata>(props.metadata);
return <Dialog
maxWidth="lg"
fullWidth
open={props.open}
onClose={props.onClose}
disableBackdropClick={true}>
<Typography variant="h5">Properties</Typography>
<Typography>Under construction</Typography>
<Divider />
<Typography variant="h5">External Links</Typography>
<ExternalLinksEditor
metadata={editingMetadata}
original={props.metadata}
onChange={(v: TrackMetadata) => setEditingMetadata(v)}
/>
<Divider />
{!_.isEqual(editingMetadata, props.metadata) && <DialogActions>
<Button variant="contained" color="secondary"
onClick={() => {
props.onSubmit(editingMetadata);
props.onClose();
}}>Save all changes</Button>
<Button variant="outlined"
onClick={() => setEditingMetadata(props.metadata)}>Discard changes</Button>
</DialogActions>}
</Dialog>
}

@ -4,16 +4,16 @@ import AlbumIcon from '@material-ui/icons/Album';
import * as serverApi from '../../../api/api';
import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import TrackTable from '../../tables/ResultsTable';
import { modifyAlbum } from '../../../lib/saveChanges';
import { modifyAlbum, modifyTrack } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryAlbums, queryTracks } from '../../../lib/backend/queries';
import { useParams } from 'react-router';
import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request';
import { useAuth } from '../../../lib/useAuth';
import { Album, Name, Id, StoreLinks, AlbumRefs } from '../../../api/api';
import { Album, Name, Id, StoreLinks, AlbumRefs, Artist, Tag, Track, ResourceType } from '../../../api/api';
import EditItemDialog, { EditablePropertyType } from '../../common/EditItemDialog';
import EditIcon from '@material-ui/icons/Edit';
export type AlbumMetadata = serverApi.QueryResponseAlbumDetails;
export type AlbumMetadataChanges = serverApi.PatchAlbumRequest;
@ -47,7 +47,7 @@ export function AlbumWindowReducer(state: AlbumWindowState, action: any) {
}
}
export async function getAlbumMetadata(id: number) : Promise<AlbumMetadata> {
export async function getAlbumMetadata(id: number): Promise<AlbumMetadata> {
let result: any = await queryAlbums(
{
a: QueryLeafBy.AlbumId,
@ -77,18 +77,21 @@ export function AlbumWindowControlled(props: {
let { id: albumId, metadata, pendingChanges, tracksOnAlbum } = props.state;
let { dispatch } = props;
let auth = useAuth();
let [editing, setEditing] = useState<boolean>(false);
// Effect to get the album's metadata.
useEffect(() => {
getAlbumMetadata(albumId)
.then((m: AlbumMetadata) => {
dispatch({
type: AlbumWindowStateActions.SetMetadata,
value: m
});
})
.catch((e: any) => { handleNotLoggedIn(auth, e) })
}, [albumId, dispatch]);
if (metadata === null) {
getAlbumMetadata(albumId)
.then((m: AlbumMetadata) => {
dispatch({
type: AlbumWindowStateActions.SetMetadata,
value: m
});
})
.catch((e: any) => { handleNotLoggedIn(auth, e) })
}
}, [albumId, dispatch, metadata]);
// Effect to get the album's tracks.
useEffect(() => {
@ -110,23 +113,7 @@ export function AlbumWindowControlled(props: {
})();
}, [tracksOnAlbum, albumId, dispatch]);
const [editingName, setEditingName] = useState<string | null>(null);
const name = <Typography variant="h4"><EditableText
defaultValue={metadata?.name || "(Unknown name)"}
changedValue={pendingChanges?.name || null}
editingValue={editingName}
editingLabel="Name"
onChangeEditingValue={(v: string | null) => setEditingName(v)}
onChangeChangedValue={(v: string | null) => {
let newVal: any = { ...pendingChanges };
if (v) { newVal.name = v }
else { delete newVal.name }
props.dispatch({
type: AlbumWindowStateActions.SetPendingChanges,
value: newVal,
})
}}
/></Typography>
const name = <Typography variant="h4">{metadata?.name || "(Unknown album name)"}</Typography>
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
@ -141,23 +128,6 @@ export function AlbumWindowControlled(props: {
</a>
});
const [applying, setApplying] = useState(false);
const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 &&
<Box>
<SubmitChangesButton onClick={() => {
setApplying(true);
modifyAlbum(props.state.id, pendingChanges || { mbApi_typename: 'album' })
.then(() => {
setApplying(false);
props.dispatch({
type: AlbumWindowStateActions.Reload
})
})
.catch((e: any) => { handleNotLoggedIn(auth, e) })
}} />
{applying && <CircularProgress />}
</Box>
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
@ -179,14 +149,13 @@ export function AlbumWindowControlled(props: {
{storeLinks}
</Box>
</Box>
<Box m={1}>
<IconButton
onClick={() => { setEditing(true); }}
><EditIcon /></IconButton>
</Box>
</Box>}
</Box>
<Box
m={1}
width="80%"
>
{maybeSubmitButton}
</Box>
<Box
m={1}
width="80%"
@ -199,5 +168,39 @@ export function AlbumWindowControlled(props: {
/>}
{!props.state.tracksOnAlbum && <CircularProgress />}
</Box>
{metadata && <EditItemDialog
open={editing}
onClose={() => { setEditing(false); }}
onSubmit={(v: serverApi.PatchAlbumRequest) => {
// Remove any details about linked resources and leave only their IDs.
let v_modified = {
...v,
tracks: undefined,
artists: undefined,
tags: undefined,
trackIds: v.trackIds || v.tracks?.map(
(a: (Track & Id)) => { return a.id }
) || undefined,
artistIds: v.artistIds || v.artists?.map(
(a: (Artist & Id)) => { return a.id }
) || undefined,
tagIds: v.tagIds || v.tags?.map(
(t: (Tag & Id)) => { return t.id }
) || undefined,
};
modifyAlbum(albumId, v_modified)
.then(() => dispatch({
type: AlbumWindowStateActions.Reload
}))
}}
id={albumId}
metadata={metadata}
editableProperties={[
{ metadataKey: 'name', title: 'Name', type: EditablePropertyType.Text },
]}
defaultExternalLinksQuery={metadata.name}
resourceType={ResourceType.Album}
editStoreLinks={true}
/>}
</Box>
}

@ -4,15 +4,16 @@ import PersonIcon from '@material-ui/icons/Person';
import * as serverApi from '../../../api/api';
import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import TrackTable from '../../tables/ResultsTable';
import { modifyArtist } from '../../../lib/saveChanges';
import { modifyAlbum, modifyArtist } from '../../../lib/saveChanges';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryArtists, queryTracks } from '../../../lib/backend/queries';
import { useParams } from 'react-router';
import { handleNotLoggedIn, NotLoggedInError } from '../../../lib/backend/request';
import { useAuth } from '../../../lib/useAuth';
import { Track, Id, Artist, Tag, ResourceType, Album } from '../../../api/api';
import EditItemDialog, { EditablePropertyType } from '../../common/EditItemDialog';
import EditIcon from '@material-ui/icons/Edit';
export type ArtistMetadata = serverApi.QueryResponseArtistDetails;
export type ArtistMetadataChanges = serverApi.PatchArtistRequest;
@ -51,7 +52,7 @@ export interface IProps {
dispatch: (action: any) => void,
}
export async function getArtistMetadata(id: number) : Promise<ArtistMetadata> {
export async function getArtistMetadata(id: number): Promise<ArtistMetadata> {
let response: any = await queryArtists(
{
a: QueryLeafBy.ArtistId,
@ -81,18 +82,21 @@ export function ArtistWindowControlled(props: {
let { metadata, id: artistId, pendingChanges, tracksByArtist } = props.state;
let { dispatch } = props;
let auth = useAuth();
let [editing, setEditing] = useState<boolean>(false);
// Effect to get the artist's metadata.
useEffect(() => {
getArtistMetadata(artistId)
.then((m: ArtistMetadata) => {
dispatch({
type: ArtistWindowStateActions.SetMetadata,
value: m
});
})
.catch((e: any) => { handleNotLoggedIn(auth, e) })
}, [artistId, dispatch]);
if (metadata === null) {
getArtistMetadata(artistId)
.then((m: ArtistMetadata) => {
dispatch({
type: ArtistWindowStateActions.SetMetadata,
value: m
});
})
.catch((e: any) => { handleNotLoggedIn(auth, e) })
}
}, [artistId, dispatch, metadata]);
// Effect to get the artist's tracks.
useEffect(() => {
@ -114,23 +118,7 @@ export function ArtistWindowControlled(props: {
})();
}, [tracksByArtist, dispatch, artistId]);
const [editingName, setEditingName] = useState<string | null>(null);
const name = <Typography variant="h4"><EditableText
defaultValue={metadata?.name || "(Unknown name)"}
changedValue={pendingChanges?.name || null}
editingValue={editingName}
editingLabel="Name"
onChangeEditingValue={(v: string | null) => setEditingName(v)}
onChangeChangedValue={(v: string | null) => {
let newVal: any = { ...pendingChanges };
if (v) { newVal.name = v }
else { delete newVal.name }
props.dispatch({
type: ArtistWindowStateActions.SetPendingChanges,
value: newVal,
})
}}
/></Typography>
const name = <Typography variant="h4">{metadata?.name || "(Unknown artist)"}</Typography>
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
@ -145,23 +133,6 @@ export function ArtistWindowControlled(props: {
</a>
});
const [applying, setApplying] = useState(false);
const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 &&
<Box>
<SubmitChangesButton onClick={() => {
setApplying(true);
modifyArtist(props.state.id, pendingChanges || { mbApi_typename: 'artist' })
.then(() => {
setApplying(false);
props.dispatch({
type: ArtistWindowStateActions.Reload
})
})
.catch((e: any) => { handleNotLoggedIn(auth, e) })
}} />
{applying && <CircularProgress />}
</Box>
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
@ -183,14 +154,13 @@ export function ArtistWindowControlled(props: {
{storeLinks}
</Box>
</Box>
<Box m={1}>
<IconButton
onClick={() => { setEditing(true); }}
><EditIcon /></IconButton>
</Box>
</Box>}
</Box>
<Box
m={1}
width="80%"
>
{maybeSubmitButton}
</Box>
<Box
m={1}
width="80%"
@ -203,5 +173,39 @@ export function ArtistWindowControlled(props: {
/>}
{!props.state.tracksByArtist && <CircularProgress />}
</Box>
{metadata && <EditItemDialog
open={editing}
onClose={() => { setEditing(false); }}
onSubmit={(v: serverApi.PatchArtistRequest) => {
// Remove any details about linked resources and leave only their IDs.
let v_modified = {
...v,
tracks: undefined,
albums: undefined,
tags: undefined,
albumIds: v.albumIds || v.albums?.map(
(a: (Album & Id)) => { return a.id }
) || undefined,
trackIds: v.trackIds || v.tracks?.map(
(t: (Track & Id)) => { return t.id }
) || undefined,
tagIds: v.tagIds || v.tags?.map(
(t: (Tag & Id)) => { return t.id }
) || undefined,
};
modifyArtist(artistId, v_modified)
.then(() => dispatch({
type: ArtistWindowStateActions.Reload
}))
}}
id={artistId}
metadata={metadata}
editableProperties={[
{ metadataKey: 'name', title: 'Name', type: EditablePropertyType.Text },
]}
defaultExternalLinksQuery={metadata.name}
resourceType={ResourceType.Artist}
editStoreLinks={true}
/>}
</Box>
}

@ -4,13 +4,14 @@ import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import * as serverApi from '../../../api/api';
import { WindowState } from '../Windows';
import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import EditableText from '../../common/EditableText';
import SubmitChangesButton from '../../common/SubmitChangesButton';
import TrackTable from '../../tables/ResultsTable';
import { modifyTag } from '../../../lib/backend/tags';
import { queryTags, queryTracks } from '../../../lib/backend/queries';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { useParams } from 'react-router';
import { Id, Track, Tag, ResourceType, Album } from '../../../api/api';
import EditItemDialog, { EditablePropertyType } from '../../common/EditItemDialog';
import EditIcon from '@material-ui/icons/Edit';
export interface FullTagMetadata extends serverApi.QueryResponseTagDetails {
fullName: string[],
@ -49,7 +50,7 @@ export function TagWindowReducer(state: TagWindowState, action: any) {
}
}
export async function getTagMetadata(id: number) : Promise<FullTagMetadata> {
export async function getTagMetadata(id: number): Promise<FullTagMetadata> {
let tags: any = await queryTags(
{
a: QueryLeafBy.TagId,
@ -93,17 +94,20 @@ export function TagWindowControlled(props: {
let pendingChanges = props.state.pendingChanges;
let { id: tagId, tracksWithTag } = props.state;
let dispatch = props.dispatch;
let [editing, setEditing] = useState<boolean>(false);
// Effect to get the tag's metadata.
useEffect(() => {
getTagMetadata(tagId)
.then((m: TagMetadata) => {
dispatch({
type: TagWindowStateActions.SetMetadata,
value: m
});
})
}, [tagId, dispatch]);
if (metadata === null) {
getTagMetadata(tagId)
.then((m: TagMetadata) => {
dispatch({
type: TagWindowStateActions.SetMetadata,
value: m
});
})
}
}, [tagId, dispatch, metadata]);
// Effect to get the tag's tracks.
useEffect(() => {
@ -124,23 +128,8 @@ export function TagWindowControlled(props: {
})();
}, [tracksWithTag, tagId, dispatch]);
const [editingName, setEditingName] = useState<string | null>(null);
const name = <Typography variant="h4"><EditableText
defaultValue={metadata?.name || "(Unknown name)"}
changedValue={pendingChanges?.name || null}
editingValue={editingName}
editingLabel="Name"
onChangeEditingValue={(v: string | null) => setEditingName(v)}
onChangeChangedValue={(v: string | null) => {
let newVal: any = { ...pendingChanges };
if (v) { newVal.name = v }
else { delete newVal.name }
props.dispatch({
type: TagWindowStateActions.SetPendingChanges,
value: newVal,
})
}}
/></Typography>
const name = <Typography variant="h4">{metadata?.name || "(Unknown tag name)"}</Typography>
const fullName = <Box display="flex" alignItems="center">
{metadata?.fullName.map((n: string, i: number) => {
if (metadata?.fullName && i === metadata?.fullName.length - 1) {
@ -153,22 +142,6 @@ export function TagWindowControlled(props: {
})}
</Box>
const [applying, setApplying] = useState(false);
const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 &&
<Box>
<SubmitChangesButton onClick={() => {
setApplying(true);
modifyTag(props.state.id, pendingChanges || { mbApi_typename: 'tag' })
.then(() => {
setApplying(false);
props.dispatch({
type: TagWindowStateActions.Reload
})
})
}} />
{applying && <CircularProgress />}
</Box>
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
@ -186,12 +159,11 @@ export function TagWindowControlled(props: {
{fullName}
</Box>
</Box>}
</Box>
<Box
m={1}
width="80%"
>
{maybeSubmitButton}
<Box m={1}>
<IconButton
onClick={() => { setEditing(true); }}
><EditIcon /></IconButton>
</Box>
</Box>
<Box
m={1}
@ -205,5 +177,30 @@ export function TagWindowControlled(props: {
/>}
{!props.state.tracksWithTag && <CircularProgress />}
</Box>
{metadata && <EditItemDialog
open={editing}
onClose={() => { setEditing(false); }}
onSubmit={(v: serverApi.PatchTagRequest) => {
// Remove any details about linked resources and leave only their IDs.
let v_modified: serverApi.PatchTagRequest = {
mbApi_typename: 'tag',
name: v.name,
parent: undefined,
parentId: v.parentId || v.parent?.id || undefined,
};
modifyTag(tagId, v_modified)
.then(() => dispatch({
type: TagWindowStateActions.Reload
}))
}}
id={tagId}
metadata={metadata}
editableProperties={[
{ metadataKey: 'name', title: 'Name', type: EditablePropertyType.Text },
]}
defaultExternalLinksQuery={metadata.name}
resourceType={ResourceType.Artist}
editStoreLinks={false}
/>}
</Box>
}

@ -11,11 +11,11 @@ import StoreLinkIcon, { whichStore } from '../../common/StoreLinkIcon';
import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
import { queryTracks } from '../../../lib/backend/queries';
import { useParams } from 'react-router';
import EditTrackDialog from './EditTrackDialog';
import EditIcon from '@material-ui/icons/Edit';
import { modifyTrack } from '../../../lib/saveChanges';
import { getTrack } from '../../../lib/backend/tracks';
import { Artist, Id, Tag } from '../../../api/api';
import { Artist, Id, ResourceType, Tag } from '../../../api/api';
import EditItemDialog, { EditablePropertyType } from '../../common/EditItemDialog';
export type TrackMetadata = serverApi.QueryResponseTrackDetails;
@ -70,7 +70,7 @@ export function TrackWindowControlled(props: {
}
}, [trackId, dispatch, metadata]);
const title = <Typography variant="h4">{metadata?.name || "(Unknown title)"}</Typography>
const title = <Typography variant="h4">{metadata?.name || "(Unknown track title)"}</Typography>
const artists = metadata?.artists && metadata?.artists.map((artist: (serverApi.Artist & serverApi.Name)) => {
return <Typography>
@ -139,7 +139,7 @@ export function TrackWindowControlled(props: {
</Box>
</Box>}
</Box>
{metadata && <EditTrackDialog
{metadata && <EditItemDialog
open={editing}
onClose={() => { setEditing(false); }}
onSubmit={(v: serverApi.PatchTrackRequest) => {
@ -164,6 +164,12 @@ export function TrackWindowControlled(props: {
}}
id={trackId}
metadata={metadata}
editableProperties={[
{ metadataKey: 'name', title: 'Title', type: EditablePropertyType.Text },
]}
resourceType={ResourceType.Track}
editStoreLinks={true}
defaultExternalLinksQuery={`${metadata.name}${metadata.artists && ` ${metadata.artists[0].name}`}${metadata.album && ` ${metadata.album.name}`}`}
/>}
</Box>
}

@ -17,7 +17,7 @@ export async function createTag(details: serverApi.PostTagRequest) {
export async function modifyTag(id: number, details: serverApi.PatchTagRequest) {
const requestOpts = {
method: 'PUT',
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(details),
};

Loading…
Cancel
Save