From cda62f0a800f002449a105bbdfd17f0e669df031 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Thu, 21 Oct 2021 11:28:34 +0200 Subject: [PATCH] Add edit dialogs for all item types. Names and store links can be edited. --- .../src/components/common/EditItemDialog.tsx | 118 ++++++++++++++++ client/src/components/common/EditableText.tsx | 93 ------------- .../ExternalLinksEditor.tsx} | 126 ++++++++---------- .../components/windows/album/AlbumWindow.tsx | 111 +++++++-------- .../windows/artist/ArtistWindow.tsx | 110 +++++++-------- .../src/components/windows/tag/TagWindow.tsx | 97 +++++++------- .../components/windows/track/TrackWindow.tsx | 14 +- client/src/lib/backend/tags.tsx | 2 +- 8 files changed, 347 insertions(+), 324 deletions(-) create mode 100644 client/src/components/common/EditItemDialog.tsx delete mode 100644 client/src/components/common/EditableText.tsx rename client/src/components/{windows/track/EditTrackDialog.tsx => common/ExternalLinksEditor.tsx} (68%) 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 + Properties + + {props.editStoreLinks && <> + External Links + setEditingMetadata(v)} + defaultQuery={props.defaultExternalLinksQuery} + resourceType={props.resourceType} + />} + + {!_.isEqual(editingMetadata, props.metadata) && + + + } + + +} \ 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 - Properties - Under construction - - External Links - setEditingMetadata(v)} - /> - - {!_.isEqual(editingMetadata, props.metadata) && - - - } - - } \ 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), };