From 34613a627ee4afd2496fd48731cc759538b04670 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 18 Jul 2022 15:56:24 +0200 Subject: [PATCH 01/44] Listening now: Refactor util to get recording MBID --- .../js/src/metadata-viewer/MetadataViewer.tsx | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index c531138d8e..91e422e04d 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -8,6 +8,7 @@ import TagsComponent from "./TagsComponent"; import { getArtistName, getAverageRGBOfImage, + getRecordingMBID, getTrackName, } from "../utils/utils"; import GlobalAppContext from "../utils/GlobalAppContext"; @@ -54,6 +55,18 @@ function OpenInMusicBrainzButton(props: { ); } +function getNowPlayingRecordingMBID( + recordingData?: MetadataLookup, + playingNow?: Listen +) { + if (!recordingData && !playingNow) { + return undefined; + } + return ( + recordingData?.recording_mbid ?? getRecordingMBID(playingNow as Listen) + ); +} + export default function MetadataViewer(props: MetadataViewerProps) { const { recordingData, playingNow } = props; const { APIService, currentUser } = React.useContext(GlobalAppContext); @@ -84,9 +97,10 @@ export default function MetadataViewer(props: MetadataViewerProps) { React.useEffect(() => { const getFeedbackPromise = async () => { - const recordingMBID = - recordingData?.recording_mbid || - get(playingNow, "track_metadata.additional_info.recording_mbid"); + const recordingMBID = getNowPlayingRecordingMBID( + recordingData, + playingNow + ); if (!recordingMBID) { return; } @@ -114,9 +128,10 @@ export default function MetadataViewer(props: MetadataViewerProps) { const submitFeedback = React.useCallback( async (score: ListenFeedBack) => { if (currentUser?.auth_token) { - const recordingMBID = - recordingData?.recording_mbid || - get(playingNow, "track_metadata.additional_info.recording_mbid"); + const recordingMBID = getNowPlayingRecordingMBID( + recordingData, + playingNow + ); if (!recordingMBID) { return; } @@ -153,7 +168,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { // Default to empty object const { metadata } = recordingData ?? {}; - + const recordingMBID = getNowPlayingRecordingMBID(recordingData, playingNow); const artistMBID = first(recordingData?.artist_mbids); const userSubmittedReleaseMBID = playingNow?.track_metadata?.additional_info?.release_mbid; @@ -331,7 +346,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { )} @@ -452,8 +467,8 @@ export default function MetadataViewer(props: MetadataViewerProps) { > Date: Mon, 18 Jul 2022 15:57:50 +0200 Subject: [PATCH 02/44] Listening now: Refactor tag component --- frontend/css/metadata-viewer.less | 39 ++++- .../js/src/metadata-viewer/TagComponent.tsx | 144 ++++++++++++++++++ 2 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 frontend/js/src/metadata-viewer/TagComponent.tsx diff --git a/frontend/css/metadata-viewer.less b/frontend/css/metadata-viewer.less index dc4c34bbfd..3318555f16 100644 --- a/frontend/css/metadata-viewer.less +++ b/frontend/css/metadata-viewer.less @@ -229,12 +229,45 @@ // hidden scrollbar display: none; } - .badge { + .tag { flex: none; color: currentcolor; - background-color: lightgrey; + > * { + background-color: lightgrey; + display: inline-block; + padding: 3px 7px; + &:first-child { + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + } + &:last-child { + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; + } + } + .tag-vote-button { + border-left: 1px dashed @boxes-background-color; + // Cancelling some .btn styling; we want the .btn class applied + // for interactivity reasons, but not all the CSS styling + margin: 0; + font-size: inherit; + line-height: inherit; + vertical-align: initial; + &.upvote.selected { + color: @btn-success-color; + background-color: @btn-success-bg; + } + &.downvote.selected { + color: @btn-danger-color; + background-color: @btn-danger-bg; + } + &.selected { + pointer-events: none; + cursor: not-allowed; + } + } } - .badge + .badge { + .tag + .tag { margin-left: 0.3em; } } diff --git a/frontend/js/src/metadata-viewer/TagComponent.tsx b/frontend/js/src/metadata-viewer/TagComponent.tsx new file mode 100644 index 0000000000..33680f5f40 --- /dev/null +++ b/frontend/js/src/metadata-viewer/TagComponent.tsx @@ -0,0 +1,144 @@ +import { noop, upperFirst } from "lodash"; +import * as React from "react"; + +type UserTagScore = 0 | 1 | -1; + +enum TagActionType { + UPVOTE = "upvote", + DOWNVOTE = "downvote", + WITHDRAW = "withdraw", +} + +function TagVoteButton(props: { + action: TagActionType; + actionFunction: React.MouseEventHandler; + userScore: UserTagScore; + withdrawMethod: React.MouseEventHandler; +}) { + const { action, actionFunction, userScore, withdrawMethod } = props; + let title = upperFirst(action); + let isActive = false; + let onClick = actionFunction; + // Logic to return the right action depending on the current user score for that tag. + // We mimic the logic of the MusicBrainz tag vote button component + // If the score is 0 upvoting and downvoting should do what we expect. + if (action === TagActionType.UPVOTE) { + switch (userScore) { + case -1: + // If score is -1 and action is "upvote", we withdraw the downvote. + title = "Withdraw my vote"; + onClick = withdrawMethod; + break; + case 1: + // Already upvoted, do nothing + isActive = true; + title = "You’ve upvoted this tag"; + onClick = noop; + break; + case 0: + default: + } + } + if (action === TagActionType.DOWNVOTE) { + switch (userScore) { + case -1: + // Already downvoted + isActive = true; + title = "You’ve downvoted this tag"; + onClick = noop; + break; + case 1: + // If score is +1 and action is "downvote", we withdraw the upvote. + title = "Withdraw my vote"; + onClick = withdrawMethod; + break; + case 0: + default: + } + } + return ( + + ); +} + +export default function TagComponent(props: { + tag: ArtistTag | RecordingTag | ReleaseGroupTag; + entityType: "artist" | "release-group" | "recording"; + entityMBID?: string; +}) { + const { tag, entityType, entityMBID } = props; + const [userScore, setUserScore] = React.useState(0); + + const [upvote, downvote, withdraw] = Object.values(TagActionType).map( + (actionVerb: TagActionType) => { + return React.useCallback(async () => { + let MBID = entityMBID; + if (entityType === "artist") { + MBID = (tag as ArtistTag).artist_mbid; + } + if (entityType === "release-group") { + MBID = (tag as ReleaseGroupTag).release_group_mbid; + } + const requestURL = `https://musicbrainz.org/${entityType}/${MBID}/tags/${actionVerb}?tags=${tag.tag}&client=brainzplayer`; + try { + const request = await fetch(requestURL, { + method: "GET", + mode: "cors", + credentials: "include", + headers: { + Accept: "application/json", + }, + }); + if (request.ok) { + switch (actionVerb) { + case TagActionType.UPVOTE: + setUserScore(1); + break; + case TagActionType.DOWNVOTE: + setUserScore(-1); + break; + case TagActionType.WITHDRAW: + default: + setUserScore(0); + break; + } + } + } catch (err) { + console.error(err); + } + }, [tag, entityType, entityMBID]); + } + ); + + return ( + + + {tag.tag} + + + + + ); +} From 2d6fee82abe7d199019149f39c8a1107336da219 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 18 Jul 2022 15:58:24 +0200 Subject: [PATCH 03/44] Listening now: Refactor tags container + use new TagComponent --- .../js/src/metadata-viewer/MetadataViewer.tsx | 14 ++++-- .../js/src/metadata-viewer/TagsComponent.tsx | 50 +++++++++++++------ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index 91e422e04d..60bee3448b 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -174,6 +174,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { playingNow?.track_metadata?.additional_info?.release_mbid; const CAAReleaseMBID = metadata?.release?.caa_release_mbid; const CAAID = metadata?.release?.caa_id; + let coverArtSrc = "/static/img/cover-art-placeholder.jpg"; // try fetching cover art using user submitted release mbid first @@ -301,7 +302,11 @@ export default function MetadataViewer(props: MetadataViewerProps) { aria-labelledby="headingOne" >
- + {/*
*/} {Boolean(flattenedRecRels?.length) && (
@@ -388,7 +393,10 @@ export default function MetadataViewer(props: MetadataViewerProps) { aria-labelledby="headingTwo" >
- +
- + {/*
*/} {(artist?.begin_year || artist?.area) && (
diff --git a/frontend/js/src/metadata-viewer/TagsComponent.tsx b/frontend/js/src/metadata-viewer/TagsComponent.tsx index 9e53fbb4d5..fe456fde41 100644 --- a/frontend/js/src/metadata-viewer/TagsComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagsComponent.tsx @@ -1,9 +1,35 @@ import * as React from "react"; +import TagComponent from "./TagComponent"; + +function AddTagComponent() { + const [expanded, setExpanded] = React.useState(false); + return ( +
+ + {expanded && ( +
+ + +
+ )} +
+ ); +} export default function TagsComponent(props: { - tags?: Array; + tags?: Array; + entityType: "artist" | "release-group" | "recording"; + entityMBID?: string; }) { - const { tags } = props; + const { tags, entityType, entityMBID } = props; return (
@@ -12,25 +38,17 @@ export default function TagsComponent(props: { .filter((tag) => tag.genre_mbid) .sort((t1, t2) => t2.count - t1.count) .map((tag) => ( - - - {tag.tag} - - + )) ) : ( Be the first to add a tag )}
- {/*
- -
*/} +
); } From 46b69f203c25aba666b5d502d2173a23f8a8fac8 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 18 Jul 2022 18:10:13 +0200 Subject: [PATCH 04/44] Listening now: Small refactor always pass an entity MBID, which we need for the add tag component --- .../js/src/metadata-viewer/MetadataViewer.tsx | 12 +++- .../js/src/metadata-viewer/TagsComponent.tsx | 2 +- .../static/js/src/metadata-viewer/types.d.ts | 61 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 listenbrainz/webserver/static/js/src/metadata-viewer/types.d.ts diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index 60bee3448b..f9096cbbe6 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -396,6 +396,12 @@ export default function MetadataViewer(props: MetadataViewerProps) {
- + {/*
*/} {(artist?.begin_year || artist?.area) && (
diff --git a/frontend/js/src/metadata-viewer/TagsComponent.tsx b/frontend/js/src/metadata-viewer/TagsComponent.tsx index fe456fde41..df82ddacc5 100644 --- a/frontend/js/src/metadata-viewer/TagsComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagsComponent.tsx @@ -27,7 +27,7 @@ function AddTagComponent() { export default function TagsComponent(props: { tags?: Array; entityType: "artist" | "release-group" | "recording"; - entityMBID?: string; + entityMBID: string; }) { const { tags, entityType, entityMBID } = props; return ( diff --git a/listenbrainz/webserver/static/js/src/metadata-viewer/types.d.ts b/listenbrainz/webserver/static/js/src/metadata-viewer/types.d.ts new file mode 100644 index 0000000000..29ece1a26f --- /dev/null +++ b/listenbrainz/webserver/static/js/src/metadata-viewer/types.d.ts @@ -0,0 +1,61 @@ +/** Main entities */ +declare type MusicBrainzArtist = { + area: string; + begin_year: number; + rels: { [key: string]: string }; + // rels: Record; + type: string; +}; + +declare type MusicBrainzRecordingRel = { + artist_mbid: string; + artist_name: string; + instrument?: string; + type: string; +}; + +/** Tags / Genres / Moods */ +declare type EntityTag = { + count: number; + genre_mbid?: string; + tag: string; +}; + +declare type RecordingTag = EntityTag; + +declare type ArtistTag = EntityTag & { + artist_mbid: string; +}; + +declare type ReleaseGroupTag = EntityTag & { + release_group_mbid: string; +}; + +declare type ListenMetadata = { + artist?: Array; + recording?: { + rels?: Array; + duration?: number; + }; + release?: { + caa_id: ?number; + mbid?: string; + year?: number; + release_group_mbid?: string; + }; + tag?: { + artist?: Array; + recording?: Array; + release_group?: Array; + }; +}; + +declare type MetadataLookup = { + metadata: ListenMetadata; + artist_credit_name: string; + artist_mbids: string[]; + recording_mbid: string; + recording_name: string; + release_mbid: string; + release_name: string; +}; From 2c92bf4ae197bdbb8fe810784f4c98813b794cb0 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 18 Jul 2022 18:15:54 +0200 Subject: [PATCH 05/44] Listening now: Improve "add tag" component Actually enable users to write a new tag, and stub implementation of submission mechanism --- frontend/css/metadata-viewer.less | 11 ++- .../js/src/metadata-viewer/TagsComponent.tsx | 82 +++++++++++++++---- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/frontend/css/metadata-viewer.less b/frontend/css/metadata-viewer.less index 3318555f16..d4a74bc9e6 100644 --- a/frontend/css/metadata-viewer.less +++ b/frontend/css/metadata-viewer.less @@ -271,16 +271,21 @@ margin-left: 0.3em; } } + .add-tag { flex: none; + position: relative; // For the overlay + form input { + height: 2em; // same height as the buttons + max-width: 13em; + } // Faded overlay to hide extra tags in .tags &:before { content: ""; position: absolute; width: 60px; - height: 80%; - right: 75px; - top: 10%; + left: -60px; + height: 100%; background: linear-gradient( to right, transparent, diff --git a/frontend/js/src/metadata-viewer/TagsComponent.tsx b/frontend/js/src/metadata-viewer/TagsComponent.tsx index df82ddacc5..16dbd0db7e 100644 --- a/frontend/js/src/metadata-viewer/TagsComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagsComponent.tsx @@ -1,24 +1,74 @@ import * as React from "react"; import TagComponent from "./TagComponent"; -function AddTagComponent() { +function AddTagComponent(props: { + entityType: "artist" | "release-group" | "recording"; + entityMBID: string; + callback?: Function; +}) { const [expanded, setExpanded] = React.useState(false); + const inputRef = React.useRef(null); + const { entityMBID, entityType, callback } = props; + + const submitNewTag = React.useCallback( + async (event: React.FormEvent) => { + event.preventDefault(); + const newTag = inputRef?.current?.value; + const requestURL = `https://musicbrainz.org/${entityType}/${entityMBID}/tags/upvote?tags=${newTag}&client=brainzplayer`; + try { + const request = await fetch(requestURL, { + method: "GET", + mode: "cors", + credentials: "include", + headers: { + Accept: "application/json", + }, + }); + if (request.ok && callback) { + callback(newTag); + } + } catch (err) { + console.error(err); + } + }, + [entityType, entityMBID] + ); return ( -
- - {expanded && ( -
- - +
+ {expanded ? ( + + +
+ + +
+ ) : ( + )}
); @@ -48,7 +98,7 @@ export default function TagsComponent(props: { Be the first to add a tag )}
- +
); } From 1f0fb7e0af2e8353eaca95fd0e489d7550a17f51 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 18 Jul 2022 18:50:42 +0200 Subject: [PATCH 06/44] Listening now: minor UI tweaks --- frontend/css/metadata-viewer.less | 8 +++++--- frontend/js/src/metadata-viewer/MetadataViewer.tsx | 11 +++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/css/metadata-viewer.less b/frontend/css/metadata-viewer.less index d4a74bc9e6..efe385c295 100644 --- a/frontend/css/metadata-viewer.less +++ b/frontend/css/metadata-viewer.less @@ -148,6 +148,8 @@ border-radius: @border-radius; box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1) inset; background-color: @boxes-background-color; + min-height: 2.8em; // adapt to add-tag component height + align-items: center; &.white { background-color: white; @@ -255,11 +257,11 @@ vertical-align: initial; &.upvote.selected { color: @btn-success-color; - background-color: @btn-success-bg; + background-color: lighten(@btn-success-bg, 20%); } &.downvote.selected { color: @btn-danger-color; - background-color: @btn-danger-bg; + background-color: lighten(@btn-danger-bg, 20%); } &.selected { pointer-events: none; @@ -277,7 +279,7 @@ position: relative; // For the overlay form input { height: 2em; // same height as the buttons - max-width: 13em; + max-width: 11em; } // Faded overlay to hide extra tags in .tags &:before { diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index f9096cbbe6..35c4a75aff 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -24,6 +24,10 @@ const supportLinkTypes = [ "official homepage", "purchase for download", "purchase for mail-order", + "social network", + "patronage", + "crowdfunding", + "blog", ]; function OpenInMusicBrainzButton(props: { @@ -286,6 +290,7 @@ export default function MetadataViewer(props: MetadataViewerProps) {

{trackName}
+  Track
{isNumber(duration) && millisecondsToStr(duration)}
@@ -305,7 +310,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { {/*
*/} {Boolean(flattenedRecRels?.length) && ( @@ -379,6 +384,7 @@ export default function MetadataViewer(props: MetadataViewerProps) {
{recordingData?.release_name}
+  Album
{metadata?.release?.year}
@@ -430,6 +436,7 @@ export default function MetadataViewer(props: MetadataViewerProps) {

{artistName}
+  Artist
{artist?.begin_year}
@@ -447,7 +454,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { {/*
*/} {(artist?.begin_year || artist?.area) && ( From c55bdf355b23fafd058a9c11c82c9302010c706a Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Tue, 19 Jul 2022 18:23:30 +0200 Subject: [PATCH 07/44] Listening now: implement tag posting properly Yada yada XML yada yada validation yada yada auth token. Auth token is currently missing, will need to wait for LB-1120 to be implemented. --- .../js/src/metadata-viewer/TagComponent.tsx | 50 ++++++------ .../js/src/metadata-viewer/TagsComponent.tsx | 31 ++++---- frontend/js/src/utils/APIService.ts | 76 ++++++++++++++++++- 3 files changed, 114 insertions(+), 43 deletions(-) diff --git a/frontend/js/src/metadata-viewer/TagComponent.tsx b/frontend/js/src/metadata-viewer/TagComponent.tsx index 33680f5f40..73d6605268 100644 --- a/frontend/js/src/metadata-viewer/TagComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagComponent.tsx @@ -1,9 +1,10 @@ import { noop, upperFirst } from "lodash"; import * as React from "react"; +import GlobalAppContext from "../utils/GlobalAppContext"; type UserTagScore = 0 | 1 | -1; -enum TagActionType { +export enum TagActionType { UPVOTE = "upvote", DOWNVOTE = "downvote", WITHDRAW = "withdraw", @@ -75,6 +76,7 @@ export default function TagComponent(props: { }) { const { tag, entityType, entityMBID } = props; const [userScore, setUserScore] = React.useState(0); + const { APIService } = React.useContext(GlobalAppContext); const [upvote, downvote, withdraw] = Object.values(TagActionType).map( (actionVerb: TagActionType) => { @@ -86,32 +88,28 @@ export default function TagComponent(props: { if (entityType === "release-group") { MBID = (tag as ReleaseGroupTag).release_group_mbid; } - const requestURL = `https://musicbrainz.org/${entityType}/${MBID}/tags/${actionVerb}?tags=${tag.tag}&client=brainzplayer`; - try { - const request = await fetch(requestURL, { - method: "GET", - mode: "cors", - credentials: "include", - headers: { - Accept: "application/json", - }, - }); - if (request.ok) { - switch (actionVerb) { - case TagActionType.UPVOTE: - setUserScore(1); - break; - case TagActionType.DOWNVOTE: - setUserScore(-1); - break; - case TagActionType.WITHDRAW: - default: - setUserScore(0); - break; - } + if (!MBID) { + return; + } + const success = await APIService.submitTagToMusicBrainz( + entityType, + MBID, + tag.tag, + actionVerb + ); + if (success) { + switch (actionVerb) { + case TagActionType.UPVOTE: + setUserScore(1); + break; + case TagActionType.DOWNVOTE: + setUserScore(-1); + break; + case TagActionType.WITHDRAW: + default: + setUserScore(0); + break; } - } catch (err) { - console.error(err); } }, [tag, entityType, entityMBID]); } diff --git a/frontend/js/src/metadata-viewer/TagsComponent.tsx b/frontend/js/src/metadata-viewer/TagsComponent.tsx index 16dbd0db7e..07975c093b 100644 --- a/frontend/js/src/metadata-viewer/TagsComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagsComponent.tsx @@ -1,5 +1,7 @@ import * as React from "react"; -import TagComponent from "./TagComponent"; +import { isFunction } from "lodash"; +import TagComponent, { TagActionType } from "./TagComponent"; +import GlobalAppContext from "../utils/GlobalAppContext"; function AddTagComponent(props: { entityType: "artist" | "release-group" | "recording"; @@ -8,27 +10,24 @@ function AddTagComponent(props: { }) { const [expanded, setExpanded] = React.useState(false); const inputRef = React.useRef(null); + const { APIService } = React.useContext(GlobalAppContext); const { entityMBID, entityType, callback } = props; const submitNewTag = React.useCallback( async (event: React.FormEvent) => { event.preventDefault(); const newTag = inputRef?.current?.value; - const requestURL = `https://musicbrainz.org/${entityType}/${entityMBID}/tags/upvote?tags=${newTag}&client=brainzplayer`; - try { - const request = await fetch(requestURL, { - method: "GET", - mode: "cors", - credentials: "include", - headers: { - Accept: "application/json", - }, - }); - if (request.ok && callback) { - callback(newTag); - } - } catch (err) { - console.error(err); + if (!newTag || !entityMBID) { + return; + } + const success = await APIService.submitTagToMusicBrainz( + entityType, + entityMBID, + newTag, + TagActionType.UPVOTE + ); + if (success && isFunction(callback)) { + callback(newTag); } }, [entityType, entityMBID] diff --git a/frontend/js/src/utils/APIService.ts b/frontend/js/src/utils/APIService.ts index bab432212c..4ec66f682d 100644 --- a/frontend/js/src/utils/APIService.ts +++ b/frontend/js/src/utils/APIService.ts @@ -1,4 +1,6 @@ -import { isNil, isUndefined, omit } from "lodash"; +import { isNil, isUndefined, kebabCase, lowerCase, omit } from "lodash"; +import async from "react-select/async"; +import { TagActionType } from "../metadata-viewer/TagComponent"; import APIError from "./APIError"; export default class APIService { @@ -1303,4 +1305,76 @@ export default class APIService { await this.checkStatus(response); return response.json(); }; + /** MusicBrainz */ + + getMusicBrainzAccessToken = async (): Promise => { + const response = await fetch(`${this.APIBaseURI}/profile/mbtoken`); + await this.checkStatus(response); + return response.text(); + }; + + submitTagToMusicBrainz = async ( + entityType: string, + entityMBID: string, + tagName: string, + action: TagActionType + ): Promise => { + const formattedEntityName = kebabCase(entityType); + // encode reserved characters + const safeTagName = tagName + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + try { + const parser = new DOMParser(); + const xmlDocument = parser.parseFromString( + ` + + <${formattedEntityName}-list> + <${formattedEntityName} id="${entityMBID}"> + + ${safeTagName} + + + + + `, + "application/xml" + ); + + // Check if the XML parsing threw an error; if so the first element will be a + const isInvalid = + xmlDocument.documentElement?.childNodes?.[0]?.nodeName === + "parsererror"; + if (isInvalid) { + // Get the error text content from the element + const errorText = + xmlDocument.documentElement.childNodes[0].childNodes?.[1] + ?.textContent; + throw SyntaxError(`Invalid XML: ${errorText}`); + } + + const url = `${this.MBBaseURI}/tag?client=listenbrainz-listening-now`; + const serializer = new XMLSerializer(); + const body = serializer.serializeToString(xmlDocument); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/xml", + }, + body, + }); + if (response.ok) { + return true; + } + return false; + } catch (err) { + console.error(err); + return false; + } + }; } From d02aafdfd05bc1a1cee820f5b03f09ad23d77455 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 4 May 2023 14:47:12 +0200 Subject: [PATCH 08/44] Add MusicBrainz auth token to front-end global context --- frontend/js/src/utils/GlobalAppContext.tsx | 4 +++- frontend/js/src/utils/types.d.ts | 2 +- frontend/js/src/utils/utils.tsx | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/js/src/utils/GlobalAppContext.tsx b/frontend/js/src/utils/GlobalAppContext.tsx index fe93ad06d6..bf816fcd9c 100644 --- a/frontend/js/src/utils/GlobalAppContext.tsx +++ b/frontend/js/src/utils/GlobalAppContext.tsx @@ -6,7 +6,8 @@ export type GlobalAppContextT = { currentUser: ListenBrainzUser; spotifyAuth?: SpotifyUser; youtubeAuth?: YoutubeUser; - critiquebrainzAuth?: CritiqueBrainzUser; + critiquebrainzAuth?: MetaBrainzProjectUser; + musicbrainzAuth?: MetaBrainzProjectUser; userPreferences?: UserPreferences; }; @@ -16,6 +17,7 @@ const GlobalAppContext = createContext({ spotifyAuth: {}, youtubeAuth: {}, critiquebrainzAuth: {}, + musicbrainzAuth: {}, userPreferences: {}, }); diff --git a/frontend/js/src/utils/types.d.ts b/frontend/js/src/utils/types.d.ts index 473372eb49..cac9fc1246 100644 --- a/frontend/js/src/utils/types.d.ts +++ b/frontend/js/src/utils/types.d.ts @@ -102,7 +102,7 @@ declare type YoutubeUser = { api_key?: string; }; -declare type CritiqueBrainzUser = { +declare type MetaBrainzProjectUser = { access_token?: string; }; diff --git a/frontend/js/src/utils/utils.tsx b/frontend/js/src/utils/utils.tsx index 1164bfe886..f658527ff4 100644 --- a/frontend/js/src/utils/utils.tsx +++ b/frontend/js/src/utils/utils.tsx @@ -436,7 +436,8 @@ type GlobalAppProps = { current_user: ListenBrainzUser; spotify?: SpotifyUser; youtube?: YoutubeUser; - critiquebrainz?: CritiqueBrainzUser; + critiquebrainz?: MetaBrainzProjectUser; + musicbrainz?: MetaBrainzProjectUser; user_preferences?: UserPreferences; }; type GlobalProps = GlobalAppProps & SentryProps; @@ -482,6 +483,7 @@ const getPageProps = (): { spotify, youtube, critiquebrainz, + musicbrainz, sentry_traces_sample_rate, sentry_dsn, } = globalReactProps; @@ -506,6 +508,7 @@ const getPageProps = (): { spotifyAuth: spotify, youtubeAuth: youtube, critiquebrainzAuth: critiquebrainz, + musicbrainzAuth: musicbrainz, userPreferences: user_preferences, }; sentryProps = { From c9c00cff29c9f8b422ef2e0e8d834a32529886d3 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 4 May 2023 15:09:47 +0200 Subject: [PATCH 09/44] Use MusicBrainz auth token in tag voting methods --- .../js/src/metadata-viewer/TagComponent.tsx | 19 ++++++++++++++----- .../js/src/metadata-viewer/TagsComponent.tsx | 19 ++++++++++++++----- frontend/js/src/utils/APIService.ts | 6 ++++-- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/frontend/js/src/metadata-viewer/TagComponent.tsx b/frontend/js/src/metadata-viewer/TagComponent.tsx index 73d6605268..dad14d6ae9 100644 --- a/frontend/js/src/metadata-viewer/TagComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagComponent.tsx @@ -76,7 +76,9 @@ export default function TagComponent(props: { }) { const { tag, entityType, entityMBID } = props; const [userScore, setUserScore] = React.useState(0); - const { APIService } = React.useContext(GlobalAppContext); + const { APIService, musicbrainzAuth } = React.useContext(GlobalAppContext); + const { access_token: musicbrainzAuthToken } = musicbrainzAuth ?? {}; + const { submitTagToMusicBrainz } = APIService; const [upvote, downvote, withdraw] = Object.values(TagActionType).map( (actionVerb: TagActionType) => { @@ -88,14 +90,15 @@ export default function TagComponent(props: { if (entityType === "release-group") { MBID = (tag as ReleaseGroupTag).release_group_mbid; } - if (!MBID) { + if (!MBID || !musicbrainzAuthToken) { return; } - const success = await APIService.submitTagToMusicBrainz( + const success = await submitTagToMusicBrainz( entityType, MBID, tag.tag, - actionVerb + actionVerb, + musicbrainzAuthToken ); if (success) { switch (actionVerb) { @@ -111,7 +114,13 @@ export default function TagComponent(props: { break; } } - }, [tag, entityType, entityMBID]); + }, [ + tag.tag, + entityType, + entityMBID, + musicbrainzAuthToken, + submitTagToMusicBrainz, + ]); } ); diff --git a/frontend/js/src/metadata-viewer/TagsComponent.tsx b/frontend/js/src/metadata-viewer/TagsComponent.tsx index 07975c093b..dc40fc1225 100644 --- a/frontend/js/src/metadata-viewer/TagsComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagsComponent.tsx @@ -10,27 +10,36 @@ function AddTagComponent(props: { }) { const [expanded, setExpanded] = React.useState(false); const inputRef = React.useRef(null); - const { APIService } = React.useContext(GlobalAppContext); + const { APIService, musicbrainzAuth } = React.useContext(GlobalAppContext); + const { access_token: musicbrainzAuthToken } = musicbrainzAuth ?? {}; + const { submitTagToMusicBrainz } = APIService; const { entityMBID, entityType, callback } = props; const submitNewTag = React.useCallback( async (event: React.FormEvent) => { event.preventDefault(); const newTag = inputRef?.current?.value; - if (!newTag || !entityMBID) { + if (!newTag || !entityMBID || !musicbrainzAuthToken) { return; } - const success = await APIService.submitTagToMusicBrainz( + const success = await submitTagToMusicBrainz( entityType, entityMBID, newTag, - TagActionType.UPVOTE + TagActionType.UPVOTE, + musicbrainzAuthToken ); if (success && isFunction(callback)) { callback(newTag); } }, - [entityType, entityMBID] + [ + entityType, + entityMBID, + musicbrainzAuthToken, + submitTagToMusicBrainz, + callback, + ] ); return (
diff --git a/frontend/js/src/utils/APIService.ts b/frontend/js/src/utils/APIService.ts index 4ec66f682d..eec2731ac5 100644 --- a/frontend/js/src/utils/APIService.ts +++ b/frontend/js/src/utils/APIService.ts @@ -1317,7 +1317,8 @@ export default class APIService { entityType: string, entityMBID: string, tagName: string, - action: TagActionType + action: TagActionType, + musicBrainzAuthToken: string ): Promise => { const formattedEntityName = kebabCase(entityType); // encode reserved characters @@ -1364,7 +1365,8 @@ export default class APIService { const response = await fetch(url, { method: "POST", headers: { - "Content-Type": "application/xml", + "Content-Type": "application/xml; charset=utf-8", + Authorization: `Token ${musicBrainzAuthToken}`, }, body, }); From 96ad3234ca35605b19bd761fc9638f642dd69030 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 4 May 2023 17:18:31 +0200 Subject: [PATCH 10/44] Refactor React useCallback in TagComponent We can't generate the callback frunction from a TagActionType array.map to refactor, the callback functions have to be defined separately, duplicating some code. See https://react.dev/reference/react/useCallback#i-need-to-call-usememo-for-each-list-item-in-a-loop-but-its-not-allowed --- .../js/src/metadata-viewer/TagComponent.tsx | 116 +++++++++++------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/frontend/js/src/metadata-viewer/TagComponent.tsx b/frontend/js/src/metadata-viewer/TagComponent.tsx index dad14d6ae9..5265e330ca 100644 --- a/frontend/js/src/metadata-viewer/TagComponent.tsx +++ b/frontend/js/src/metadata-viewer/TagComponent.tsx @@ -80,49 +80,81 @@ export default function TagComponent(props: { const { access_token: musicbrainzAuthToken } = musicbrainzAuth ?? {}; const { submitTagToMusicBrainz } = APIService; - const [upvote, downvote, withdraw] = Object.values(TagActionType).map( - (actionVerb: TagActionType) => { - return React.useCallback(async () => { - let MBID = entityMBID; - if (entityType === "artist") { - MBID = (tag as ArtistTag).artist_mbid; - } - if (entityType === "release-group") { - MBID = (tag as ReleaseGroupTag).release_group_mbid; - } - if (!MBID || !musicbrainzAuthToken) { - return; - } - const success = await submitTagToMusicBrainz( - entityType, - MBID, - tag.tag, - actionVerb, - musicbrainzAuthToken - ); - if (success) { - switch (actionVerb) { - case TagActionType.UPVOTE: - setUserScore(1); - break; - case TagActionType.DOWNVOTE: - setUserScore(-1); - break; - case TagActionType.WITHDRAW: - default: - setUserScore(0); - break; - } - } - }, [ - tag.tag, - entityType, - entityMBID, - musicbrainzAuthToken, - submitTagToMusicBrainz, - ]); + let tagEntityMBID = entityMBID; + if (entityType === "artist") { + tagEntityMBID = (tag as ArtistTag).artist_mbid; + } + if (entityType === "release-group") { + tagEntityMBID = (tag as ReleaseGroupTag).release_group_mbid; + } + /** We can't generate the callback frunction from an array.map to refactor this, + * the callback functions have to be defined separately, duplicating some code. + */ + const upvote = React.useCallback(async () => { + if (!tagEntityMBID || !musicbrainzAuthToken) { + return; } - ); + const success = await submitTagToMusicBrainz( + entityType, + tagEntityMBID, + tag.tag, + TagActionType.UPVOTE, + musicbrainzAuthToken + ); + if (success) { + setUserScore(1); + } + }, [ + tag.tag, + entityType, + tagEntityMBID, + musicbrainzAuthToken, + submitTagToMusicBrainz, + ]); + + const downvote = React.useCallback(async () => { + if (!tagEntityMBID || !musicbrainzAuthToken) { + return; + } + const success = await submitTagToMusicBrainz( + entityType, + tagEntityMBID, + tag.tag, + TagActionType.DOWNVOTE, + musicbrainzAuthToken + ); + if (success) { + setUserScore(-1); + } + }, [ + tag.tag, + entityType, + tagEntityMBID, + musicbrainzAuthToken, + submitTagToMusicBrainz, + ]); + + const withdraw = React.useCallback(async () => { + if (!tagEntityMBID || !musicbrainzAuthToken) { + return; + } + const success = await submitTagToMusicBrainz( + entityType, + tagEntityMBID, + tag.tag, + TagActionType.WITHDRAW, + musicbrainzAuthToken + ); + if (success) { + setUserScore(0); + } + }, [ + tag.tag, + entityType, + tagEntityMBID, + musicbrainzAuthToken, + submitTagToMusicBrainz, + ]); return ( From 5990a967bd2c4fad6cde9292efa63e6af7a4ec51 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 4 May 2023 17:18:46 +0200 Subject: [PATCH 11/44] Resolve React hooks linting warnings --- .../js/src/metadata-viewer/MetadataViewer.tsx | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index 35c4a75aff..22a9426761 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -74,6 +74,8 @@ function getNowPlayingRecordingMBID( export default function MetadataViewer(props: MetadataViewerProps) { const { recordingData, playingNow } = props; const { APIService, currentUser } = React.useContext(GlobalAppContext); + const { getFeedbackForUserForMBIDs, submitFeedback } = APIService; + const { auth_token, name: username } = currentUser; const [currentListenFeedback, setCurrentListenFeedback] = React.useState(0); const [expandedAccordion, setExpandedAccordion] = React.useState(1); @@ -89,15 +91,16 @@ export default function MetadataViewer(props: MetadataViewerProps) { const averageColor = getAverageRGBOfImage(albumArtRef?.current); setAlbumArtColor(averageColor); }; - if (albumArtRef?.current) { - albumArtRef.current.addEventListener("load", setAverageColor); + const currentAlbumArtRef = albumArtRef.current; + if (currentAlbumArtRef) { + currentAlbumArtRef.addEventListener("load", setAverageColor); } return () => { - if (albumArtRef?.current) { - albumArtRef.current.removeEventListener("load", setAverageColor); + if (currentAlbumArtRef) { + currentAlbumArtRef.removeEventListener("load", setAverageColor); } }; - }, [albumArtRef?.current, setAlbumArtColor]); + }, [setAlbumArtColor]); React.useEffect(() => { const getFeedbackPromise = async () => { @@ -109,8 +112,8 @@ export default function MetadataViewer(props: MetadataViewerProps) { return; } try { - const feedbackObject = await APIService.getFeedbackForUserForMBIDs( - currentUser.name, + const feedbackObject = await getFeedbackForUserForMBIDs( + username, recordingMBID ); if (feedbackObject?.feedback?.length) { @@ -127,11 +130,11 @@ export default function MetadataViewer(props: MetadataViewerProps) { } }; getFeedbackPromise(); - }, [recordingData, playingNow]); + }, [recordingData, playingNow, getFeedbackForUserForMBIDs, username]); - const submitFeedback = React.useCallback( + const submitFeedbackCallback = React.useCallback( async (score: ListenFeedBack) => { - if (currentUser?.auth_token) { + if (auth_token) { const recordingMBID = getNowPlayingRecordingMBID( recordingData, playingNow @@ -141,12 +144,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { } try { setCurrentListenFeedback(score); - await APIService.submitFeedback( - currentUser.auth_token, - score, - undefined, - recordingMBID - ); + await submitFeedback(auth_token, score, undefined, recordingMBID); } catch (error) { // Revert the feedback UI in case of failure setCurrentListenFeedback(0); @@ -155,7 +153,13 @@ export default function MetadataViewer(props: MetadataViewerProps) { } } }, - [recordingData, playingNow, setCurrentListenFeedback] + [ + recordingData, + playingNow, + setCurrentListenFeedback, + submitFeedback, + auth_token, + ] ); const adjustedAlbumColor = tinycolor.fromRatio(albumArtColor); @@ -520,7 +524,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { - -
- + ({ - value: genre, - label: genre, - }))} - isSearchable - onChange={submitNewTag} - onMenuClose={() => setExpanded(false)} - /> - ) : ( - - )} -
- ); -} -export default function TagsComponent(props: { - tags?: Array; - entityType: "artist" | "release-group" | "recording"; - entityMBID: string; -}) { - const { tags, entityType, entityMBID } = props; - const [newTags, setNewTags] = React.useState([]); - const onAddNewTag = React.useCallback( - (newTagName: string) => { - const newTag: EntityTag = { count: 1, tag: newTagName }; - setNewTags((prevTags) => prevTags.concat(newTag)); + const onChange = useCallback( + async ( + selectedTags: MultiValue, + actionMeta: ActionMeta + ) => { + let callbackValue: TagOptionType | undefined; + + switch (actionMeta.action) { + case "select-option": + case "create-option": { + callbackValue = actionMeta.option; + const success = await submitTagVote( + TagActionType.UPVOTE, + callbackValue?.value + ); + if (!success) { + return; + } + setSelected(selectedTags as TagOptionType[]); + break; + } + case "remove-value": { + callbackValue = actionMeta.removedValue; + const success = await submitTagVote( + TagActionType.WITHDRAW, + callbackValue?.value + ); + if (!success) { + return; + } + setSelected( + selectedTags.filter((tag) => tag.value !== callbackValue?.value) + ); + break; + } + default: + setSelected(selectedTags as TagOptionType[]); + break; + } }, - [setNewTags] + [submitTagVote] ); - const allTags: Array< - EntityTag | ArtistTag | ReleaseGroupTag - > = newTags.concat(tags ? tags.filter((tag) => tag.genre_mbid) : []); + return ( -
-
- {allTags?.length ? ( - allTags - .sort((t1, t2) => t2.count - t1.count) - .map((tag) => ( - - )) - ) : ( - Be the first to add a tag - )} -
- + ({ + value: genre, + label: genre, + entityMBID, + entityType, + }))} + placeholder="Add genre" + isSearchable + isMulti + isDisabled={!musicbrainzAuthToken} + isClearable={false} + openMenuOnClick={false} + onChange={onChange} + components={{ + MultiValueContainer, + DropdownIndicator, + }} + styles={{ + container: (baseStyles, state) => ({ + ...baseStyles, + border: "0px", + }), + }} />
); From da9bd2e876d9b533e93a80ff11a52bb8843bf3e6 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 30 Jun 2023 19:12:39 +0200 Subject: [PATCH 21/44] Tweak tags CSS + separate into its own file --- frontend/css/main.less | 1 + frontend/css/metadata-viewer.less | 82 ------------------------------- frontend/css/tags.less | 60 ++++++++++++++++++++++ 3 files changed, 61 insertions(+), 82 deletions(-) create mode 100644 frontend/css/tags.less diff --git a/frontend/css/main.less b/frontend/css/main.less index dbeed78d9c..a0f16d4cff 100644 --- a/frontend/css/main.less +++ b/frontend/css/main.less @@ -27,6 +27,7 @@ @import "search-track.less"; @import "stats.less"; @import "new-navbar.less"; +@import "tags.less"; @icon-font-path: "/static/fonts/"; diff --git a/frontend/css/metadata-viewer.less b/frontend/css/metadata-viewer.less index c85966f24e..94dfe089f9 100644 --- a/frontend/css/metadata-viewer.less +++ b/frontend/css/metadata-viewer.less @@ -218,88 +218,6 @@ } } - .tags-wrapper { - display: flex; - position: relative; - .tags { - flex: 1; - align-items: center; - // horizontal scroll overflow with hidden scrollbar and faded overlay - display: flex; - overflow-x: scroll; - &::-webkit-scrollbar { - // hidden scrollbar - display: none; - } - .tag { - flex: none; - color: currentcolor; - > * { - background-color: lightgrey; - display: inline-block; - padding: 3px 7px; - &:first-child { - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; - } - &:last-child { - border-top-right-radius: 10px; - border-bottom-right-radius: 10px; - } - } - .tag-vote-button { - border-left: 1px dashed @boxes-background-color; - // Cancelling some .btn styling; we want the .btn class applied - // for interactivity reasons, but not all the CSS styling - margin: 0; - font-size: inherit; - line-height: inherit; - vertical-align: initial; - &.upvote.selected { - color: @btn-success-color; - background-color: lighten(@btn-success-bg, 20%); - } - &.downvote.selected { - color: @btn-danger-color; - background-color: lighten(@btn-danger-bg, 20%); - } - &.selected { - pointer-events: none; - cursor: not-allowed; - } - } - } - .tag + .tag { - margin-left: 0.3em; - } - } - - .add-tag { - flex: none; - position: relative; // For the overlay - &.expanded { - flex:2; - } - form input { - height: 2em; // same height as the buttons - max-width: 11em; - } - // Faded overlay to hide extra tags in .tags - &:before { - content: ""; - position: absolute; - width: 60px; - left: -60px; - height: 100%; - background: linear-gradient( - to right, - transparent, - @boxes-background-color - ); - pointer-events: none; - } - } - } } } } diff --git a/frontend/css/tags.less b/frontend/css/tags.less new file mode 100644 index 0000000000..0f4a50b300 --- /dev/null +++ b/frontend/css/tags.less @@ -0,0 +1,60 @@ +@tagBorderRadius: 6px; +.tag { + flex: none; + color: currentcolor; + > * { + background-color: rgb(236, 235, 228); + display: inline-block; + padding: 3px 7px; + vertical-align: middle; + &:first-child { + border-top-left-radius: @tagBorderRadius; + border-bottom-left-radius: @tagBorderRadius; + } + &:last-child { + border-top-right-radius: @tagBorderRadius; + border-bottom-right-radius: @tagBorderRadius; + } + } + .tag-vote-button { + margin: 0; + border-left: 1px dashed #f7f7f7; + opacity: 0; + line-height: 1.5em; + vertical-align: baseline; + padding-left: 0px; + width: 0px; + padding-right: 0px; + transition-duration: 0.3s; + transition-property: padding, width, opacity; + transition-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28); + // Cancelling some .btn styling; we want the .btn class applied + // for interactivity reasons, but not all the CSS styling + margin: 0; + font-size: inherit; + line-height: inherit; + vertical-align: initial; + + &.upvote:hover, + &.upvote.selected { + color: @btn-success-color; + background-color: lighten(@btn-success-bg, 20%); + } + &.downvote:hover, + &.downvote.selected, + &.delete:hover { + color: @btn-danger-color; + background-color: lighten(@btn-danger-bg, 20%); + } + } + &:hover > .tag-vote-button { + width: 2em; + opacity: 1; + } + &:not(:hover) > :first-child { + border-radius: @tagBorderRadius; + } + & + .tag { + margin-left: 0.3em; + } +} From 9385a0479f9513aad912c6972efb92c411b1748c Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 30 Jun 2023 19:15:42 +0200 Subject: [PATCH 22/44] Move tag components into separate folder Preparing to make them reusable --- frontend/js/src/metadata-viewer/MetadataViewer.tsx | 2 +- frontend/js/src/{metadata-viewer => tags}/TagComponent.tsx | 0 frontend/js/src/{metadata-viewer => tags}/TagsComponent.tsx | 0 frontend/js/src/utils/APIService.ts | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename frontend/js/src/{metadata-viewer => tags}/TagComponent.tsx (100%) rename frontend/js/src/{metadata-viewer => tags}/TagsComponent.tsx (100%) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index ffc97c239f..634cda4660 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -4,7 +4,7 @@ import { faPauseCircle } from "@fortawesome/free-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import * as tinycolor from "tinycolor2"; import { first, isEmpty, isNumber, isPlainObject, pick } from "lodash"; -import TagsComponent from "./TagsComponent"; +import TagsComponent from "../tags/TagsComponent"; import { getArtistName, getAverageRGBOfImage, diff --git a/frontend/js/src/metadata-viewer/TagComponent.tsx b/frontend/js/src/tags/TagComponent.tsx similarity index 100% rename from frontend/js/src/metadata-viewer/TagComponent.tsx rename to frontend/js/src/tags/TagComponent.tsx diff --git a/frontend/js/src/metadata-viewer/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx similarity index 100% rename from frontend/js/src/metadata-viewer/TagsComponent.tsx rename to frontend/js/src/tags/TagsComponent.tsx diff --git a/frontend/js/src/utils/APIService.ts b/frontend/js/src/utils/APIService.ts index 8964048b6a..34ca6f2533 100644 --- a/frontend/js/src/utils/APIService.ts +++ b/frontend/js/src/utils/APIService.ts @@ -1,5 +1,5 @@ import { isNil, isUndefined, kebabCase, lowerCase, omit } from "lodash"; -import { TagActionType } from "../metadata-viewer/TagComponent"; +import { TagActionType } from "../tags/TagComponent"; import APIError from "./APIError"; export default class APIService { From 4bf12dd266c6aaf30c61e638bae61fb07f9e376d Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 3 Jul 2023 18:00:11 +0200 Subject: [PATCH 23/44] Fix button padding --- frontend/css/tags.less | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/css/tags.less b/frontend/css/tags.less index 0f4a50b300..ce00ad16b5 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -22,9 +22,8 @@ opacity: 0; line-height: 1.5em; vertical-align: baseline; - padding-left: 0px; + padding: 3px 0; width: 0px; - padding-right: 0px; transition-duration: 0.3s; transition-property: padding, width, opacity; transition-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28); From 32f8a99c106e649530e0a0fb330296fd68fc6c0c Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 3 Jul 2023 18:06:54 +0200 Subject: [PATCH 24/44] Tagging: Support missing entity MBID Don't allow to vote + disable input if entity MBID is missing (no matched MBID) --- .../js/src/metadata-viewer/MetadataViewer.tsx | 6 +++--- frontend/js/src/tags/TagComponent.tsx | 11 ++++++++++- frontend/js/src/tags/TagsComponent.tsx | 19 ++++++++++++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index 634cda4660..2236b8d137 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -313,7 +313,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { {/*
*/} {Boolean(flattenedRecRels?.length) && ( @@ -405,7 +405,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { {/*
*/} {(artist?.begin_year || artist?.area) && ( diff --git a/frontend/js/src/tags/TagComponent.tsx b/frontend/js/src/tags/TagComponent.tsx index ef4e408244..ee0000488d 100644 --- a/frontend/js/src/tags/TagComponent.tsx +++ b/frontend/js/src/tags/TagComponent.tsx @@ -95,7 +95,16 @@ export default function TagComponent(props: { */ const vote = React.useCallback( async (action: TagActionType) => { - if (!tagEntityMBID || !musicbrainzAuthToken) { + if (!musicbrainzAuthToken) { + toast.warning( + "You need to be logged in to MusicBrainz in order to vote on or create tags" + ); + return; + } + if (!tagEntityMBID) { + toast.error( + "Something went wrong, missing an entity MBID to vote on a tag" + ); return; } const success = await submitTagToMusicBrainz( diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index dad7e42e84..1318b381d0 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -18,7 +18,7 @@ type TagOptionType = { label: string; isFixed?: boolean; entityType: Entity; - entityMBID: string; + entityMBID?: string; }; function DropdownIndicator(props: DropdownIndicatorProps) { @@ -59,7 +59,7 @@ function MultiValueContainer(props: MultiValueGenericProps) { export default function AddTagSelect(props: { entityType: "artist" | "release-group" | "recording"; - entityMBID: string; + entityMBID?: string; tags?: Array; }) { const { tags, entityType, entityMBID } = props; @@ -68,7 +68,7 @@ export default function AddTagSelect(props: { value: tag.tag, label: tag.tag, isFixed: true, - entityMBID, + entityMBID: entityMBID ?? (tag as ArtistTag).artist_mbid ?? undefined, entityType, })) ?? [] ); @@ -81,7 +81,16 @@ export default function AddTagSelect(props: { const submitTagVote = useCallback( async (action: TagActionType, tag?: string) => { - if (!musicbrainzAuthToken || !tag) { + if (!musicbrainzAuthToken) { + toast.warning( + "You need to be logged in to MusicBrainz in order to vote on or create tags" + ); + return false; + } + if (!tag || !entityMBID) { + toast.error( + "Something went wrong, missing some information to vote on a tag (tag name or entity MBID)" + ); return false; } const success = await submitTagToMusicBrainz( @@ -155,7 +164,7 @@ export default function AddTagSelect(props: { placeholder="Add genre" isSearchable isMulti - isDisabled={!musicbrainzAuthToken} + isDisabled={!musicbrainzAuthToken || !entityMBID} isClearable={false} openMenuOnClick={false} onChange={onChange} From 9b2bab2b9ee3ea20b50914248796df8de45ebf00 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 3 Jul 2023 18:28:59 +0200 Subject: [PATCH 25/44] CSS classname fix --- frontend/css/tags.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/css/tags.less b/frontend/css/tags.less index ce00ad16b5..2388b67fa2 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -41,7 +41,7 @@ } &.downvote:hover, &.downvote.selected, - &.delete:hover { + &.withdraw:hover { color: @btn-danger-color; background-color: lighten(@btn-danger-bg, 20%); } From d1525b7e5d5577ae7ecd01bafbc8a02105ccfb4a Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 3 Jul 2023 18:32:14 +0200 Subject: [PATCH 26/44] Lint LESS files --- frontend/css/metadata-viewer.less | 1 - frontend/css/tags.less | 3 --- 2 files changed, 4 deletions(-) diff --git a/frontend/css/metadata-viewer.less b/frontend/css/metadata-viewer.less index 94dfe089f9..69b824f699 100644 --- a/frontend/css/metadata-viewer.less +++ b/frontend/css/metadata-viewer.less @@ -217,7 +217,6 @@ text-align: right; } } - } } } diff --git a/frontend/css/tags.less b/frontend/css/tags.less index 2388b67fa2..87d8bb5b54 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -17,11 +17,8 @@ } } .tag-vote-button { - margin: 0; border-left: 1px dashed #f7f7f7; opacity: 0; - line-height: 1.5em; - vertical-align: baseline; padding: 3px 0; width: 0px; transition-duration: 0.3s; From 04ff0fe6a20228c15e6dbea1cee7f0a6148a1246 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Wed, 5 Jul 2023 15:20:09 +0200 Subject: [PATCH 27/44] Tweak wording on tags multiselect For tag creation specifically. Move away from talking about genres and talk about tags instead --- frontend/js/src/tags/TagsComponent.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index 1318b381d0..e227aacfc2 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -10,6 +10,8 @@ import { } from "react-select"; import CreatableSelect from "react-select/creatable"; import { toast } from "react-toastify"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import TagComponent, { TagActionType } from "./TagComponent"; import GlobalAppContext from "../utils/GlobalAppContext"; @@ -21,6 +23,14 @@ type TagOptionType = { entityMBID?: string; }; +function CreateTagText(input: string) { + return ( + <> + Submit new tag {input} + + ); +} + function DropdownIndicator(props: DropdownIndicatorProps) { const { isDisabled } = props; if (isDisabled) { @@ -29,7 +39,7 @@ function DropdownIndicator(props: DropdownIndicatorProps) { return ( ); @@ -161,7 +171,8 @@ export default function AddTagSelect(props: { entityMBID, entityType, }))} - placeholder="Add genre" + placeholder="Add tag" + formatCreateLabel={CreateTagText} isSearchable isMulti isDisabled={!musicbrainzAuthToken || !entityMBID} From 4d58a8b85a4d8b48bf743a42148cacd7045590b3 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Wed, 5 Jul 2023 16:10:51 +0200 Subject: [PATCH 28/44] CSS tweaks vote button tags + mobile device mechanism to prevent tag clicks on first click --- frontend/css/tags.less | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/css/tags.less b/frontend/css/tags.less index 87d8bb5b54..bd72b07d31 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -21,14 +21,13 @@ opacity: 0; padding: 3px 0; width: 0px; + height: 2.2em; transition-duration: 0.3s; transition-property: padding, width, opacity; transition-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28); // Cancelling some .btn styling; we want the .btn class applied // for interactivity reasons, but not all the CSS styling margin: 0; - font-size: inherit; - line-height: inherit; vertical-align: initial; &.upvote:hover, @@ -53,4 +52,15 @@ & + .tag { margin-left: 0.3em; } + @media (hover: none), (hover: on-demand), (-moz-touch-enabled: 1), (pointer:coarse) { + /* only on touch devices */ + > a { + pointer-events: none; + } + &:hover > a, + &:focus > a, + &:focus-within > a { + pointer-events: initial; + } + } } From baea27c54d7ef4bb3586db732e67be6599012039 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Wed, 5 Jul 2023 16:11:33 +0200 Subject: [PATCH 29/44] Only display tag component if matching entity exists + move lyrics button --- .../js/src/metadata-viewer/MetadataViewer.tsx | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index 2236b8d137..fac4b57151 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -310,11 +310,13 @@ export default function MetadataViewer(props: MetadataViewerProps) { aria-labelledby="headingOne" >
- + {Boolean(metadata?.recording) && ( + + )} {/*
*/} {Boolean(flattenedRecRels?.length) && (
@@ -347,16 +349,6 @@ export default function MetadataViewer(props: MetadataViewerProps) {
)}
- {lyricsLink?.lyrics && ( - - Lyrics - - )}
- + {Boolean(metadata?.artist) && ( + + )} {/*
*/} {(artist?.begin_year || artist?.area) && (
@@ -462,10 +456,22 @@ export default function MetadataViewer(props: MetadataViewerProps) { {artist?.area && ` in ${artist.area}`}
)} - +
+ {lyricsLink?.lyrics && ( + + Lyrics + + )} + +
From ea0a31658ecd2468996fd3651c3f90e24e7040c7 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Wed, 5 Jul 2023 16:12:08 +0200 Subject: [PATCH 30/44] Avoid stale props in state in tags components --- frontend/js/src/tags/TagComponent.tsx | 23 ++++++++++++++----- frontend/js/src/tags/TagsComponent.tsx | 31 +++++++++++++++++++------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/frontend/js/src/tags/TagComponent.tsx b/frontend/js/src/tags/TagComponent.tsx index ee0000488d..4ae76e8440 100644 --- a/frontend/js/src/tags/TagComponent.tsx +++ b/frontend/js/src/tags/TagComponent.tsx @@ -77,10 +77,20 @@ export default function TagComponent(props: { entityMBID?: string; isNew?: boolean; deleteCallback: (tag: string) => void; + initialScore?: UserTagScore; }) { - const { tag, entityType, entityMBID, isNew, deleteCallback } = props; + const { + tag, + entityType, + entityMBID, + isNew, + deleteCallback, + initialScore, + } = props; // TODO: Fetch user's tag votes for this entity? That's a lot of API queries… - const [userScore, setUserScore] = React.useState(0); + const [userScore, setUserScore] = React.useState( + initialScore ?? 0 + ); const { APIService, musicbrainzAuth } = React.useContext(GlobalAppContext); const { access_token: musicbrainzAuthToken } = musicbrainzAuth ?? {}; const { submitTagToMusicBrainz } = APIService; @@ -90,9 +100,10 @@ export default function TagComponent(props: { tagEntityMBID = (tag as ArtistTag).artist_mbid; } - /** We can't generate the callback frunction from an array.map to refactor this, - * the callback functions have to be defined separately, duplicating some code. - */ + React.useEffect(() => { + setUserScore(initialScore ?? 0); + }, [tag, initialScore]); + const vote = React.useCallback( async (action: TagActionType) => { if (!musicbrainzAuthToken) { @@ -131,7 +142,7 @@ export default function TagComponent(props: { } }, [ - tag.tag, + tag, entityType, tagEntityMBID, musicbrainzAuthToken, diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index e227aacfc2..09aa711ad9 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -1,6 +1,6 @@ import { noop } from "lodash"; import * as React from "react"; -import { useCallback, useState } from "react"; +import { useCallback, useState, useEffect } from "react"; import { ActionMeta, DropdownIndicatorProps, @@ -66,6 +66,19 @@ function MultiValueContainer(props: MultiValueGenericProps) { /> ); } +function getOptionFromTag( + tag: ArtistTag | RecordingTag | ReleaseGroupTag, + entityType: Entity, + entityMBID?: string +) { + return { + value: tag.tag, + label: tag.tag, + isFixed: true, + entityMBID: entityMBID ?? (tag as ArtistTag).artist_mbid ?? undefined, + entityType, + }; +} export default function AddTagSelect(props: { entityType: "artist" | "release-group" | "recording"; @@ -74,14 +87,16 @@ export default function AddTagSelect(props: { }) { const { tags, entityType, entityMBID } = props; const [selected, setSelected] = useState( - tags?.map((tag) => ({ - value: tag.tag, - label: tag.tag, - isFixed: true, - entityMBID: entityMBID ?? (tag as ArtistTag).artist_mbid ?? undefined, - entityType, - })) ?? [] + tags?.length + ? tags.map((tag) => getOptionFromTag(tag, entityType, entityMBID)) + : [] ); + useEffect(() => { + if (tags?.length) + setSelected( + tags?.map((tag) => getOptionFromTag(tag, entityType, entityMBID)) + ); + }, [tags, entityType, entityMBID]); const { APIService, musicbrainzAuth, musicbrainzGenres } = React.useContext( GlobalAppContext From 7cde6d847055490d5fd98d4836cde98742aaaed3 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Wed, 5 Jul 2023 17:11:15 +0200 Subject: [PATCH 31/44] Bigger tags on mobile devices --- frontend/css/tags.less | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/css/tags.less b/frontend/css/tags.less index bd72b07d31..088275fcba 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -2,7 +2,9 @@ .tag { flex: none; color: currentcolor; + height: 2.4rem; > * { + height: 100%; background-color: rgb(236, 235, 228); display: inline-block; padding: 3px 7px; @@ -19,17 +21,15 @@ .tag-vote-button { border-left: 1px dashed #f7f7f7; opacity: 0; - padding: 3px 0; width: 0px; - height: 2.2em; transition-duration: 0.3s; transition-property: padding, width, opacity; transition-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28); // Cancelling some .btn styling; we want the .btn class applied // for interactivity reasons, but not all the CSS styling margin: 0; - vertical-align: initial; - + padding: 3px 0; + &.upvote:hover, &.upvote.selected { color: @btn-success-color; @@ -53,7 +53,12 @@ margin-left: 0.3em; } @media (hover: none), (hover: on-demand), (-moz-touch-enabled: 1), (pointer:coarse) { - /* only on touch devices */ + /* on touch devices, make tags bigger so they are easier to interact with */ + height: 3.3rem; + font-size: 1.9rem; + &:hover > .tag-vote-button { + width: 3em; + } > a { pointer-events: none; } From 6be7e4c186abedbb6f1e43dd6b7654d2f87f979b Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 6 Jul 2023 18:30:29 +0200 Subject: [PATCH 32/44] Use key for TagsComponent to reset state remove useEffect used in TagsComponent --- .../js/src/metadata-viewer/MetadataViewer.tsx | 35 +++++++++---------- frontend/js/src/tags/TagsComponent.tsx | 18 ++++------ 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index fac4b57151..af09029b60 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -218,6 +218,8 @@ export default function MetadataViewer(props: MetadataViewerProps) { const artistName = (recordingData?.artist_credit_name ?? fallbackArtistName) || "No artist to show"; + const releaseName = metadata?.release?.name ?? recordingData?.release_name; + const duration = metadata?.recording?.length ?? playingNow?.track_metadata?.additional_info?.duration_ms; @@ -310,13 +312,12 @@ export default function MetadataViewer(props: MetadataViewerProps) { aria-labelledby="headingOne" >
- {Boolean(metadata?.recording) && ( - - )} + {/*
*/} {Boolean(flattenedRecRels?.length) && (
@@ -357,7 +358,7 @@ export default function MetadataViewer(props: MetadataViewerProps) {
- {Boolean(metadata?.release || recordingData?.release_name) && ( + {Boolean(releaseName) && (

-
- {recordingData?.release_name} -
+
{releaseName}
 Album
{metadata?.release?.year}
@@ -395,6 +394,7 @@ export default function MetadataViewer(props: MetadataViewerProps) { >
- {Boolean(metadata?.artist) && ( - - )} + {/*
*/} {(artist?.begin_year || artist?.area) && (
diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index 09aa711ad9..7adfa1361b 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -86,23 +86,17 @@ export default function AddTagSelect(props: { tags?: Array; }) { const { tags, entityType, entityMBID } = props; + const { APIService, musicbrainzAuth, musicbrainzGenres } = React.useContext( + GlobalAppContext + ); + const { access_token: musicbrainzAuthToken } = musicbrainzAuth ?? {}; + const { submitTagToMusicBrainz, MBBaseURI } = APIService; + const [selected, setSelected] = useState( tags?.length ? tags.map((tag) => getOptionFromTag(tag, entityType, entityMBID)) : [] ); - useEffect(() => { - if (tags?.length) - setSelected( - tags?.map((tag) => getOptionFromTag(tag, entityType, entityMBID)) - ); - }, [tags, entityType, entityMBID]); - - const { APIService, musicbrainzAuth, musicbrainzGenres } = React.useContext( - GlobalAppContext - ); - const { access_token: musicbrainzAuthToken } = musicbrainzAuth ?? {}; - const { submitTagToMusicBrainz } = APIService; const submitTagVote = useCallback( async (action: TagActionType, tag?: string) => { From e3ef15b111ad3efe5c7c47d19abf4a4b8ae662a8 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 6 Jul 2023 19:02:41 +0200 Subject: [PATCH 33/44] Fetch user's own tags on MB on entity change --- frontend/css/tags.less | 14 ++++++-- frontend/js/src/tags/TagComponent.tsx | 11 ++++-- frontend/js/src/tags/TagsComponent.tsx | 47 ++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/frontend/css/tags.less b/frontend/css/tags.less index 088275fcba..c2e2f72f00 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -3,9 +3,17 @@ flex: none; color: currentcolor; height: 2.4rem; + background-color: rgb(236, 235, 228); + &.upvoted { + color: @btn-success-color; + background-color: lighten(@btn-success-bg, 20%); + } + &.downvoted { + color: @btn-danger-color; + background-color: lighten(@btn-danger-bg, 20%); + } > * { height: 100%; - background-color: rgb(236, 235, 228); display: inline-block; padding: 3px 7px; vertical-align: middle; @@ -33,13 +41,13 @@ &.upvote:hover, &.upvote.selected { color: @btn-success-color; - background-color: lighten(@btn-success-bg, 20%); + background-color: lighten(@btn-success-bg, 30%); } &.downvote:hover, &.downvote.selected, &.withdraw:hover { color: @btn-danger-color; - background-color: lighten(@btn-danger-bg, 20%); + background-color: lighten(@btn-danger-bg, 30%); } } &:hover > .tag-vote-button { diff --git a/frontend/js/src/tags/TagComponent.tsx b/frontend/js/src/tags/TagComponent.tsx index 4ae76e8440..87ef83df59 100644 --- a/frontend/js/src/tags/TagComponent.tsx +++ b/frontend/js/src/tags/TagComponent.tsx @@ -76,6 +76,7 @@ export default function TagComponent(props: { entityType: "artist" | "release-group" | "recording"; entityMBID?: string; isNew?: boolean; + isOwnTag?: boolean; deleteCallback: (tag: string) => void; initialScore?: UserTagScore; }) { @@ -84,6 +85,7 @@ export default function TagComponent(props: { entityType, entityMBID, isNew, + isOwnTag, deleteCallback, initialScore, } = props; @@ -149,9 +151,14 @@ export default function TagComponent(props: { submitTagToMusicBrainz, ] ); - + let cssClasses = "tag"; + if (userScore === -1) { + cssClasses += " downvoted"; + } else if (userScore === 1 || isOwnTag) { + cssClasses += " upvoted"; + } return ( - + ) { entityType={data.entityType} entityMBID={data.entityMBID} isNew={!data.isFixed} + isOwnTag={data.isOwnTag} deleteCallback={ data.isFixed ? noop @@ -86,6 +87,7 @@ export default function AddTagSelect(props: { tags?: Array; }) { const { tags, entityType, entityMBID } = props; + const [prevEntityMBID, setPrevEntityMBID] = useState(entityMBID); const { APIService, musicbrainzAuth, musicbrainzGenres } = React.useContext( GlobalAppContext ); @@ -97,6 +99,51 @@ export default function AddTagSelect(props: { ? tags.map((tag) => getOptionFromTag(tag, entityType, entityMBID)) : [] ); + const getUserTags = useCallback(async () => { + /* Get user's own tags */ + if (!musicbrainzAuthToken || !entityType || !entityMBID) { + return; + } + const url = `${MBBaseURI}/${entityType}/${entityMBID}?fmt=json&inc=user-tags`; + const response = await fetch(encodeURI(url), { + headers: { + "Content-Type": "application/xml; charset=utf-8", + Authorization: `Bearer ${musicbrainzAuthToken}`, + }, + }); + if (response.ok) { + const responseJSON = await response.json(); + const userTags = responseJSON["user-tags"]; + if (userTags?.length) { + setSelected((prevSelected) => + userTags + .map( + (tag: string): TagOptionType => ({ + value: tag, + label: tag, + entityType, + entityMBID, + isFixed: false, + isOwnTag: true, + }) + ) + .concat(prevSelected) + ); + } + } + }, [entityType, entityMBID, musicbrainzAuthToken, MBBaseURI]); + + if (prevEntityMBID !== entityMBID) { + // Will only run once when the entityMBID changes, + // contrarily to a useEffect with entityMBID as a dependency + // see https://react.dev/reference/react/useState#my-initializer-or-updater-function-runs-twice + setPrevEntityMBID(entityMBID); + try { + getUserTags(); + } catch (error) { + toast.error(error.toString()); + } + } const submitTagVote = useCallback( async (action: TagActionType, tag?: string) => { From 899aad9780b8884ac31fe36e2d811f881abdfa1e Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 6 Jul 2023 19:09:23 +0200 Subject: [PATCH 34/44] Fix type issue woopsie! --- frontend/js/src/tags/TagsComponent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index fd78a8b749..a3c2f744b8 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -19,6 +19,7 @@ type TagOptionType = { value: string; label: string; isFixed?: boolean; + isOwnTag?: boolean; entityType: Entity; entityMBID?: string; }; From c68a08d08aacfcdf0761dbc8671cf1c4f286e703 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 6 Jul 2023 19:19:30 +0200 Subject: [PATCH 35/44] Lint less file --- frontend/css/tags.less | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/css/tags.less b/frontend/css/tags.less index c2e2f72f00..cac7fc1c58 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -37,7 +37,6 @@ // for interactivity reasons, but not all the CSS styling margin: 0; padding: 3px 0; - &.upvote:hover, &.upvote.selected { color: @btn-success-color; @@ -60,7 +59,10 @@ & + .tag { margin-left: 0.3em; } - @media (hover: none), (hover: on-demand), (-moz-touch-enabled: 1), (pointer:coarse) { + @media (hover: none), + (hover: on-demand), + (-moz-touch-enabled: 1), + (pointer: coarse) { /* on touch devices, make tags bigger so they are easier to interact with */ height: 3.3rem; font-size: 1.9rem; From 5784dd5081ece86b128e5413c1a4d6324927c8ad Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 6 Jul 2023 19:24:39 +0200 Subject: [PATCH 36/44] Remove extraneous APIService method Previous implementation? In any case, method is unused and the /profile/mbtoken doesn't seem to exist anymore. --- frontend/js/src/utils/APIService.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/js/src/utils/APIService.ts b/frontend/js/src/utils/APIService.ts index 34ca6f2533..c8dd5442f7 100644 --- a/frontend/js/src/utils/APIService.ts +++ b/frontend/js/src/utils/APIService.ts @@ -1361,12 +1361,6 @@ export default class APIService { }; /** MusicBrainz */ - getMusicBrainzAccessToken = async (): Promise => { - const response = await fetch(`${this.APIBaseURI}/profile/mbtoken`); - await this.checkStatus(response); - return response.text(); - }; - submitTagToMusicBrainz = async ( entityType: string, entityMBID: string, From ffb82a4d1f008dfeb666db787ce4543d36bcc50f Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 7 Jul 2023 15:24:06 +0200 Subject: [PATCH 37/44] Filter and sort tags Only show genre tags, in order of vote count, as well as the user's own tags. We filter out other users' non-genre tags, because it can quickly become too much. --- .../js/src/metadata-viewer/MetadataViewer.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/js/src/metadata-viewer/MetadataViewer.tsx b/frontend/js/src/metadata-viewer/MetadataViewer.tsx index af09029b60..47935f9763 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewer.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewer.tsx @@ -71,6 +71,19 @@ function getNowPlayingRecordingMBID( ); } +function filterAndSortTags(tags?: EntityTag[]): EntityTag[] | undefined { + return tags + ?.filter((tag) => { + return tag.genre_mbid; + }) + .sort((a, b) => { + if (a.genre_mbid && !b.genre_mbid) { + return 1; + } + return b.count - a.count; + }); +} + export default function MetadataViewer(props: MetadataViewerProps) { const { recordingData, playingNow } = props; const { APIService, currentUser } = React.useContext(GlobalAppContext); @@ -314,7 +327,7 @@ export default function MetadataViewer(props: MetadataViewerProps) {
@@ -395,7 +408,7 @@ export default function MetadataViewer(props: MetadataViewerProps) {
@@ -443,7 +456,7 @@ export default function MetadataViewer(props: MetadataViewerProps) {
From 0ed322d0e02f182e84b386ac4d1e3d0dfa42735a Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 7 Jul 2023 15:24:38 +0200 Subject: [PATCH 38/44] Add toast notifications container in page wrapper Couldn't see any error messages otherwise, duh. --- .../src/metadata-viewer/MetadataViewerPageWrapper.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/js/src/metadata-viewer/MetadataViewerPageWrapper.tsx b/frontend/js/src/metadata-viewer/MetadataViewerPageWrapper.tsx index 8ecbac49b6..ab88c0344a 100644 --- a/frontend/js/src/metadata-viewer/MetadataViewerPageWrapper.tsx +++ b/frontend/js/src/metadata-viewer/MetadataViewerPageWrapper.tsx @@ -5,6 +5,7 @@ import { createRoot } from "react-dom/client"; import * as Sentry from "@sentry/react"; import { Integrations } from "@sentry/tracing"; import { io } from "socket.io-client"; +import { ToastContainer } from "react-toastify"; import { withAlertNotifications, WithAlertNotificationsInjectedProps, @@ -149,6 +150,16 @@ document.addEventListener("DOMContentLoaded", async () => { const renderRoot = createRoot(domContainer!); renderRoot.render( + Date: Fri, 7 Jul 2023 15:25:09 +0200 Subject: [PATCH 39/44] CSS tweaks Don't lose tag rounded corners, + tweak colors a bit. --- frontend/css/tags.less | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/css/tags.less b/frontend/css/tags.less index cac7fc1c58..1bee2fa6a6 100644 --- a/frontend/css/tags.less +++ b/frontend/css/tags.less @@ -4,13 +4,14 @@ color: currentcolor; height: 2.4rem; background-color: rgb(236, 235, 228); + border-radius: @tagBorderRadius; &.upvoted { color: @btn-success-color; - background-color: lighten(@btn-success-bg, 20%); + background-color: lighten(@btn-success-bg, 40%); } &.downvoted { color: @btn-danger-color; - background-color: lighten(@btn-danger-bg, 20%); + background-color: lighten(@btn-danger-bg, 40%); } > * { height: 100%; From b28fb3354d93ff30f70c835a5bf9ac83bf15edd0 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 7 Jul 2023 15:25:37 +0200 Subject: [PATCH 40/44] Don't recreate tags on vote count change --- frontend/js/src/tags/TagComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/js/src/tags/TagComponent.tsx b/frontend/js/src/tags/TagComponent.tsx index 87ef83df59..ce69aa5c04 100644 --- a/frontend/js/src/tags/TagComponent.tsx +++ b/frontend/js/src/tags/TagComponent.tsx @@ -158,7 +158,7 @@ export default function TagComponent(props: { cssClasses += " upvoted"; } return ( - + Date: Fri, 7 Jul 2023 15:26:19 +0200 Subject: [PATCH 41/44] Small tweaks in React-ness Mistakes were made ! --- frontend/js/src/tags/TagComponent.tsx | 7 +++---- frontend/js/src/tags/TagsComponent.tsx | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/js/src/tags/TagComponent.tsx b/frontend/js/src/tags/TagComponent.tsx index ce69aa5c04..25218260c8 100644 --- a/frontend/js/src/tags/TagComponent.tsx +++ b/frontend/js/src/tags/TagComponent.tsx @@ -97,10 +97,9 @@ export default function TagComponent(props: { const { access_token: musicbrainzAuthToken } = musicbrainzAuth ?? {}; const { submitTagToMusicBrainz } = APIService; - let tagEntityMBID = entityMBID; - if (entityType === "artist") { - tagEntityMBID = (tag as ArtistTag).artist_mbid; - } + // If we have multiple MB artists, make sure we vote for the tag on the right entity + const tagEntityMBID = + entityType === "artist" ? (tag as ArtistTag).artist_mbid : entityMBID; React.useEffect(() => { setUserScore(initialScore ?? 0); diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index a3c2f744b8..00d091ab10 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -1,4 +1,4 @@ -import { noop } from "lodash"; +import { isUndefined, noop } from "lodash"; import * as React from "react"; import { useCallback, useState, useEffect } from "react"; import { @@ -88,7 +88,7 @@ export default function AddTagSelect(props: { tags?: Array; }) { const { tags, entityType, entityMBID } = props; - const [prevEntityMBID, setPrevEntityMBID] = useState(entityMBID); + const [prevEntityMBID, setPrevEntityMBID] = useState(); const { APIService, musicbrainzAuth, musicbrainzGenres } = React.useContext( GlobalAppContext ); @@ -134,7 +134,7 @@ export default function AddTagSelect(props: { } }, [entityType, entityMBID, musicbrainzAuthToken, MBBaseURI]); - if (prevEntityMBID !== entityMBID) { + if (!isUndefined(entityMBID) && prevEntityMBID !== entityMBID) { // Will only run once when the entityMBID changes, // contrarily to a useEffect with entityMBID as a dependency // see https://react.dev/reference/react/useState#my-initializer-or-updater-function-runs-twice From 99ab9a3ddaaf03255758cb0a87ebec93fe46cc00 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 7 Jul 2023 15:55:42 +0200 Subject: [PATCH 42/44] Fix user tag processing --- frontend/js/src/tags/TagsComponent.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index 00d091ab10..f9bd4ed0cd 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -119,9 +119,9 @@ export default function AddTagSelect(props: { setSelected((prevSelected) => userTags .map( - (tag: string): TagOptionType => ({ - value: tag, - label: tag, + (tag: { name: string }): TagOptionType => ({ + value: tag.name, + label: tag.name, entityType, entityMBID, isFixed: false, From 1cbe16ddaeebd6bc7b7c6a85515b628092ae8c0a Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Fri, 7 Jul 2023 16:15:45 +0200 Subject: [PATCH 43/44] Retain tag information in children We need the artist_mbid for example from the original tag --- frontend/js/src/tags/TagComponent.tsx | 3 +-- frontend/js/src/tags/TagsComponent.tsx | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/js/src/tags/TagComponent.tsx b/frontend/js/src/tags/TagComponent.tsx index 25218260c8..d5c9568c7b 100644 --- a/frontend/js/src/tags/TagComponent.tsx +++ b/frontend/js/src/tags/TagComponent.tsx @@ -98,8 +98,7 @@ export default function TagComponent(props: { const { submitTagToMusicBrainz } = APIService; // If we have multiple MB artists, make sure we vote for the tag on the right entity - const tagEntityMBID = - entityType === "artist" ? (tag as ArtistTag).artist_mbid : entityMBID; + const tagEntityMBID = (tag as ArtistTag).artist_mbid ?? entityMBID; React.useEffect(() => { setUserScore(initialScore ?? 0); diff --git a/frontend/js/src/tags/TagsComponent.tsx b/frontend/js/src/tags/TagsComponent.tsx index f9bd4ed0cd..bbd0ac3eda 100644 --- a/frontend/js/src/tags/TagsComponent.tsx +++ b/frontend/js/src/tags/TagsComponent.tsx @@ -22,6 +22,7 @@ type TagOptionType = { isOwnTag?: boolean; entityType: Entity; entityMBID?: string; + originalTag?: ArtistTag | RecordingTag | ReleaseGroupTag; }; function CreateTagText(input: string) { @@ -50,7 +51,7 @@ function MultiValueContainer(props: MultiValueGenericProps) { const { data, selectProps } = props; return ( ( - tags?.length - ? tags.map((tag) => getOptionFromTag(tag, entityType, entityMBID)) - : [] + tags?.map((tag) => getOptionFromTag(tag, entityType, entityMBID)) ?? [] ); const getUserTags = useCallback(async () => { /* Get user's own tags */ @@ -126,6 +126,7 @@ export default function AddTagSelect(props: { entityMBID, isFixed: false, isOwnTag: true, + originalTag: { tag: tag.name, count: 1 }, }) ) .concat(prevSelected) From d575d618b803752531c924b7668ab63f6ca5c58d Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Tue, 25 Jul 2023 18:52:53 +0200 Subject: [PATCH 44/44] Update package lockfile --- package-lock.json | 473 +++++++++++++++++++--------------------------- 1 file changed, 194 insertions(+), 279 deletions(-) diff --git a/package-lock.json b/package-lock.json index 261ad413e8..5b5b02f56e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "react-lazy-load-image-component": "^1.5.6", "react-loader-spinner": "^3.1.14", "react-responsive": "^8.1.0", - "react-select": "^3.1.0", + "react-select": "^5.7.3", "react-simple-star-rating": "^4.0.0", "react-sortablejs": "^6.1.4", "react-toastify": "^8.2.0", @@ -91,7 +91,7 @@ "@types/react-dom": "^18.0.8", "@types/react-lazy-load-image-component": "^1.5.2", "@types/react-loader-spinner": "^3.1.0", - "@types/react-select": "^3.0.26", + "@types/react-select": "^5.0.1", "@types/react-youtube": "^7.6.2", "@types/sortablejs": "^1.10.6", "@types/spotify-web-playback-sdk": "^0.1.7", @@ -2066,86 +2066,119 @@ "react-dom": ">16.8.0" } }, - "node_modules/@emotion/cache": { - "version": "10.0.29", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", - "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "dependencies": { - "@emotion/sheet": "0.9.4", - "@emotion/stylis": "0.8.5", - "@emotion/utils": "0.11.3", - "@emotion/weak-memoize": "0.2.5" + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" } }, - "node_modules/@emotion/core": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", - "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", - "dependencies": { - "@babel/runtime": "^7.5.5", - "@emotion/cache": "^10.0.27", - "@emotion/css": "^10.0.27", - "@emotion/serialize": "^0.11.15", - "@emotion/sheet": "0.9.4", - "@emotion/utils": "0.11.3" + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@emotion/css": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", - "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "dependencies": { - "@emotion/serialize": "^0.11.15", - "@emotion/utils": "0.11.3", - "babel-plugin-emotion": "^10.0.27" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" } }, "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, "node_modules/@emotion/serialize": { - "version": "0.11.16", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", - "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "dependencies": { - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/unitless": "0.7.5", - "@emotion/utils": "0.11.3", - "csstype": "^2.5.7" + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", - "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } }, "node_modules/@emotion/utils": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", - "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "node_modules/@emotion/weak-memoize": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", @@ -2215,6 +2248,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@floating-ui/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", + "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.5.tgz", + "integrity": "sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==", + "dependencies": { + "@floating-ui/core": "^1.3.1" + } + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "0.2.28", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz", @@ -3908,12 +3954,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-lazy-load-image-component/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", - "dev": true - }, "node_modules/@types/react-loader-spinner": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/react-loader-spinner/-/react-loader-spinner-3.1.0.tgz", @@ -3924,21 +3964,19 @@ } }, "node_modules/@types/react-select": { - "version": "3.0.26", - "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.0.26.tgz", - "integrity": "sha512-rAaiD0SFkBi3PUwp1DrJV04CobPl2LuZXF+kv6MKw8kaeGo82xTOZzjM8DDi4lrdkqGbInZiE2QO9nIJm3bqgw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-5.0.1.tgz", + "integrity": "sha512-h5Im0AP0dr4AVeHtrcvQrLV+gmPa7SA0AGdxl2jOhtwiE6KgXBFSogWw8az32/nusE6AQHlCOHQWjP1S/+oMWA==", + "deprecated": "This is a stub types definition. react-select provides its own type definitions, so you do not need this installed.", "dev": true, "dependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "@types/react-transition-group": "*" + "react-select": "*" } }, "node_modules/@types/react-transition-group": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz", - "integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==", - "dev": true, + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", "dependencies": { "@types/react": "*" } @@ -3952,11 +3990,6 @@ "@types/react": "*" } }, - "node_modules/@types/react/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -5177,23 +5210,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/babel-plugin-emotion": { - "version": "10.0.33", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", - "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/serialize": "^0.11.16", - "babel-plugin-macros": "^2.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^1.0.5", - "find-root": "^1.1.0", - "source-map": "^0.5.7" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", @@ -5223,13 +5239,17 @@ } }, "node_modules/babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "dependencies": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" } }, "node_modules/babel-plugin-polyfill-corejs2": { @@ -5280,11 +5300,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, "node_modules/babel-preset-jest": { "version": "25.1.0", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.1.0.tgz", @@ -6065,40 +6080,18 @@ "dev": true }, "node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dependencies": { "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", + "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cosmiconfig/node_modules/parse-json": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", - "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "yaml": "^1.10.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cosmiconfig/node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/cross-fetch": { @@ -6269,9 +6262,9 @@ "dev": true }, "node_modules/csstype": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.10.tgz", - "integrity": "sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/d3-array": { "version": "2.3.3", @@ -6586,15 +6579,6 @@ "node": ">=8" } }, - "node_modules/dir-glob/node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -6622,11 +6606,6 @@ "csstype": "^3.0.2" } }, - "node_modules/dom-helpers/node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -8756,22 +8735,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -8781,33 +8744,6 @@ "node": ">=8" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -13708,9 +13644,9 @@ } }, "node_modules/memoize-one": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", - "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, "node_modules/meow": { "version": "9.0.0", @@ -13765,24 +13701,6 @@ "node": ">=10" } }, - "node_modules/meow/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/meow/node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -14594,6 +14512,23 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -14674,6 +14609,14 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -15264,14 +15207,6 @@ "react-dom": "^15.5.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-input-autosize": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", - "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==", - "dependencies": { - "prop-types": "^15.5.8" - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -15319,18 +15254,23 @@ } }, "node_modules/react-select": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.0.tgz", - "integrity": "sha512-wBFVblBH1iuCBprtpyGtd1dGMadsG36W5/t2Aj8OE6WbByDg5jIFyT7X5gT+l0qmT5TqWhxX+VsKJvCEl2uL9g==", - "dependencies": { - "@babel/runtime": "^7.4.4", - "@emotion/cache": "^10.0.9", - "@emotion/core": "^10.0.9", - "@emotion/css": "^10.0.9", - "memoize-one": "^5.0.0", + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.4.tgz", + "integrity": "sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", "prop-types": "^15.6.0", - "react-input-autosize": "^2.2.2", - "react-transition-group": "^4.3.0" + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-shallow-renderer": { @@ -16992,22 +16932,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/stylelint/node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/stylelint/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -17072,33 +16996,6 @@ "node": ">=8.6" } }, - "node_modules/stylelint/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylelint/node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/stylelint/node_modules/slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -17183,6 +17080,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -17984,6 +17886,19 @@ "node": ">=0.10.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",