diff --git a/client/src/components/common/EditItemDialog.tsx b/client/src/components/common/EditItemDialog.tsx
new file mode 100644
index 0000000..5143e3f
--- /dev/null
+++ b/client/src/components/common/EditItemDialog.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
+ {
+ props.onChange((e.target.value == "") ?
+ props.originalValue : e.target.value)
+ }}
+ fullWidth={true}
+ />
+ {props.currentValue != props.originalValue && {
+ props.onChange(props.originalValue)
+ }}
+ >}
+
+}
+
+function PropertyEditor(props: {
+ originalMetadata: any,
+ currentMetadata: any,
+ onChange: (metadata: any) => void,
+ editableProperties: EditableProperty[]
+}) {
+ return
+ {props.editableProperties.map(
+ (p: EditableProperty) => {
+ if (p.type == EditablePropertyType.Text) {
+ return props.onChange({ ...props.currentMetadata, [p.metadataKey]: v })}
+ />
+ }
+ return undefined;
+ }
+ )}
+
+}
+
+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(props.metadata);
+
+ return
+
+}
\ No newline at end of file
diff --git a/client/src/components/common/EditableText.tsx b/client/src/components/common/EditableText.tsx
deleted file mode 100644
index e67dca4..0000000
--- a/client/src/components/common/EditableText.tsx
+++ /dev/null
@@ -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(false);
-
- const editButton =
- onChangeEditingValue(changedValue || defaultValue)}
- >
-
-
-
-
- const discardChangesButton =
- {
- onChangeChangedValue(null);
- onChangeEditingValue(null);
- }}
- >
-
-
-
-
- if (editing) {
- return
- onChangeEditingValue(e.target.value)}
- />
- {
- onChangeChangedValue(editingValue === defaultValue ? null : editingValue);
- onChangeEditingValue(null);
- }}
- >
-
- } else if (changedValue) {
- return setHovering(true)}
- onMouseLeave={() => setHovering(false)}
- display="flex"
- alignItems="center"
- >
- {defaultValue}→
- {changedValue}
- {editButton}
- {discardChangesButton}
-
- }
-
- return setHovering(true)}
- onMouseLeave={() => setHovering(false)}
- display="flex"
- alignItems="center"
- >{defaultValue}{editButton};
-}
\ No newline at end of file
diff --git a/client/src/components/windows/track/EditTrackDialog.tsx b/client/src/components/common/ExternalLinksEditor.tsx
similarity index 68%
rename from client/src/components/windows/track/EditTrackDialog.tsx
rename to client/src/components/common/ExternalLinksEditor.tsx
index 4a1488d..815cb4e 100644
--- a/client/src/components/windows/track/EditTrackDialog.tsx
+++ b/client/src/components/common/ExternalLinksEditor.tsx
@@ -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(
props.providers.length > 0 ? 0 : undefined
);
- let [query, setQuery] = useState(defaultQuery)
- let [results, setResults] = useState(undefined);
+ let [query, setQuery] = useState(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
@@ -63,17 +63,44 @@ export function ProvideLinksWidget(props: {
/>
{
- 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;
+ }
}}
>
{results && results.length > 0 && Suggestions:}
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 }
@@ -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(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: {
>
{linksSet[store] !== null ? : }
-
+
{maybeLink &&
}
@@ -184,51 +213,10 @@ export function ExternalLinksEditor(props: {
})
}
}}
+ defaultQuery={props.defaultQuery}
+ resourceType={props.resourceType}
/>
}
-}
-
-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(props.metadata);
-
- return
-
}
\ No newline at end of file
diff --git a/client/src/components/windows/album/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx
index 27e90a8..2b78ed5 100644
--- a/client/src/components/windows/album/AlbumWindow.tsx
+++ b/client/src/components/windows/album/AlbumWindow.tsx
@@ -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 {
+export async function getAlbumMetadata(id: number): Promise {
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(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(null);
- const name = 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,
- })
- }}
- />
+ const name = {metadata?.name || "(Unknown album name)"}
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
@@ -141,23 +128,6 @@ export function AlbumWindowControlled(props: {
});
- const [applying, setApplying] = useState(false);
- const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 &&
-
- {
- 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 && }
-
-
return
+
+ { setEditing(true); }}
+ >
+
}
-
- {maybeSubmitButton}
-
}
{!props.state.tracksOnAlbum && }
+ {metadata && { 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}
+ />}
}
\ No newline at end of file
diff --git a/client/src/components/windows/artist/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx
index f5b23f5..b1a1e90 100644
--- a/client/src/components/windows/artist/ArtistWindow.tsx
+++ b/client/src/components/windows/artist/ArtistWindow.tsx
@@ -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 {
+export async function getArtistMetadata(id: number): Promise {
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(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(null);
- const name = 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,
- })
- }}
- />
+ const name = {metadata?.name || "(Unknown artist)"}
const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => {
const store = whichStore(link);
@@ -145,23 +133,6 @@ export function ArtistWindowControlled(props: {
});
- const [applying, setApplying] = useState(false);
- const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 &&
-
- {
- 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 && }
-
-
return
+
+ { setEditing(true); }}
+ >
+
}
-
- {maybeSubmitButton}
-
}
{!props.state.tracksByArtist && }
+ {metadata && { 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}
+ />}
}
\ No newline at end of file
diff --git a/client/src/components/windows/tag/TagWindow.tsx b/client/src/components/windows/tag/TagWindow.tsx
index 213a3ac..aadb6c2 100644
--- a/client/src/components/windows/tag/TagWindow.tsx
+++ b/client/src/components/windows/tag/TagWindow.tsx
@@ -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 {
+export async function getTagMetadata(id: number): Promise {
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(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(null);
- const name = 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,
- })
- }}
- />
+ const name = {metadata?.name || "(Unknown tag name)"}
+
const fullName =
{metadata?.fullName.map((n: string, i: number) => {
if (metadata?.fullName && i === metadata?.fullName.length - 1) {
@@ -153,22 +142,6 @@ export function TagWindowControlled(props: {
})}
- const [applying, setApplying] = useState(false);
- const maybeSubmitButton = pendingChanges && Object.keys(pendingChanges).length > 0 &&
-
- {
- setApplying(true);
- modifyTag(props.state.id, pendingChanges || { mbApi_typename: 'tag' })
- .then(() => {
- setApplying(false);
- props.dispatch({
- type: TagWindowStateActions.Reload
- })
- })
- }} />
- {applying && }
-
-
return
}
-
-
- {maybeSubmitButton}
+
+ { setEditing(true); }}
+ >
+
}
{!props.state.tracksWithTag && }
+ {metadata && { 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}
+ />}
}
\ No newline at end of file
diff --git a/client/src/components/windows/track/TrackWindow.tsx b/client/src/components/windows/track/TrackWindow.tsx
index abfafc9..ea4fd1a 100644
--- a/client/src/components/windows/track/TrackWindow.tsx
+++ b/client/src/components/windows/track/TrackWindow.tsx
@@ -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 = {metadata?.name || "(Unknown title)"}
+ const title = {metadata?.name || "(Unknown track title)"}
const artists = metadata?.artists && metadata?.artists.map((artist: (serverApi.Artist & serverApi.Name)) => {
return
@@ -139,7 +139,7 @@ export function TrackWindowControlled(props: {
}
- {metadata && { 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}`}`}
/>}
}
\ No newline at end of file
diff --git a/client/src/lib/backend/tags.tsx b/client/src/lib/backend/tags.tsx
index 838ba97..7680afe 100644
--- a/client/src/lib/backend/tags.tsx
+++ b/client/src/lib/backend/tags.tsx
@@ -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),
};