-
Notifications
You must be signed in to change notification settings - Fork 461
Add more export data #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
.dropdown-toggle::after { | ||
display: none; | ||
} | ||
|
||
.dropdown-menu { | ||
box-shadow: 1px 1px 4px 0px rgba(0,0,0,0.2); | ||
} | ||
|
||
.dropdown-item { | ||
&:active, &:hover { | ||
color: black; | ||
background: none; | ||
} | ||
|
||
label { | ||
display: block; | ||
cursor: pointer; | ||
} | ||
} | ||
|
||
.dropdown { | ||
button { | ||
padding: 0; | ||
margin: 0 20px; | ||
height: 31px; | ||
color: #dee2e6; | ||
|
||
&:hover { | ||
color: silver; | ||
} | ||
} | ||
|
||
&.show { | ||
button { | ||
color: #5cb85c; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import './ConfigDropdown.scss' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is tsx? A quick Google is only returning cars. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haha. The idea is that introducing typing is the front-line of defence against introducing bugs. |
||
|
||
import React from "react" | ||
import { Dropdown, Form } from "react-bootstrap" | ||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" | ||
|
||
type ConfigDropdownProps = { | ||
onConfigChanged: (config: any) => void | ||
} | ||
|
||
class ConfigDropdown extends React.Component<ConfigDropdownProps> { | ||
private includeArtistsDataCheck = React.createRef<HTMLInputElement>() | ||
private includeAudioFeaturesDataCheck = React.createRef<HTMLInputElement>() | ||
|
||
handleCheckClick = (event: React.MouseEvent) => { | ||
event.stopPropagation() | ||
|
||
if ((event.target as HTMLElement).nodeName === "INPUT") { | ||
this.props.onConfigChanged({ | ||
includeArtistsData: this.includeArtistsDataCheck.current?.checked || false, | ||
includeAudioFeaturesData: this.includeAudioFeaturesDataCheck.current?.checked || false | ||
}) | ||
} | ||
} | ||
|
||
render() { | ||
return ( | ||
<Dropdown> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So much less verbose than React javascript calls. |
||
<Dropdown.Toggle variant="link"> | ||
<FontAwesomeIcon icon={['fas', 'cog']} size="lg" /> | ||
</Dropdown.Toggle> | ||
<Dropdown.Menu align="right"> | ||
<Dropdown.Item onClickCapture={this.handleCheckClick} as="div"> | ||
<Form.Check | ||
id="config-include-artists-data" | ||
type="switch" | ||
label="Include artists data" | ||
ref={this.includeArtistsDataCheck} | ||
/> | ||
</Dropdown.Item> | ||
<Dropdown.Item onClickCapture={this.handleCheckClick} as="div"> | ||
<Form.Check | ||
id="config-include-audio-features-data" | ||
type="switch" | ||
label="Include audio features data" | ||
ref={this.includeAudioFeaturesDataCheck}/> | ||
</Dropdown.Item> | ||
</Dropdown.Menu> | ||
</Dropdown> | ||
) | ||
} | ||
} | ||
|
||
export default ConfigDropdown |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,8 @@ import { saveAs } from "file-saver" | |
|
||
import TracksData from "components/tracks_data/TracksData" | ||
import TracksBaseData from "components/tracks_data/TracksBaseData" | ||
import TracksArtistsData from "components/tracks_data/TracksArtistsData" | ||
import TracksAudioFeaturesData from "components/tracks_data/TracksAudioFeaturesData" | ||
|
||
class TracksCsvFile { | ||
playlist: any | ||
|
@@ -49,10 +51,12 @@ class TracksCsvFile { | |
class PlaylistExporter { | ||
accessToken: string | ||
playlist: any | ||
config: any | ||
|
||
constructor(accessToken: string, playlist: any) { | ||
constructor(accessToken: string, playlist: any, config: any) { | ||
this.accessToken = accessToken | ||
this.playlist = playlist | ||
this.config = config | ||
} | ||
|
||
async export() { | ||
|
@@ -67,6 +71,17 @@ class PlaylistExporter { | |
const tracksBaseData = new TracksBaseData(this.accessToken, this.playlist) | ||
|
||
await tracksCsvFile.addData(tracksBaseData) | ||
const tracks = await tracksBaseData.tracks() | ||
|
||
if (this.config.includeArtistsData) { | ||
const tracksArtistsData = new TracksArtistsData(this.accessToken, tracks) | ||
await tracksCsvFile.addData(tracksArtistsData) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. love that async/await |
||
} | ||
|
||
if (this.config.includeAudioFeaturesData) { | ||
const tracksAudioFeaturesData = new TracksAudioFeaturesData(this.accessToken, tracks) | ||
await tracksCsvFile.addData(tracksAudioFeaturesData) | ||
} | ||
|
||
tracksBaseData.tracks() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,7 +75,6 @@ describe("single playlist exporting", () => { | |
fireEvent.click(linkElement) | ||
|
||
await waitFor(() => { | ||
expect(handlerCalled).toHaveBeenCalledTimes(4) | ||
expect(handlerCalled.mock.calls).toEqual([ // Ensure API call order and no duplicates | ||
[ 'https://api.spotify.com/v1/me' ], | ||
[ 'https://api.spotify.com/v1/users/watsonbox/playlists' ], | ||
|
@@ -87,8 +86,88 @@ describe("single playlist exporting", () => { | |
expect(saveAsMock).toHaveBeenCalledWith( | ||
{ | ||
content: [ | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Disc Number","Track Number","Track Duration (ms)","Added By","Added At"\n' + | ||
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","1","3","198093","","2020-07-19T09:24:39Z"\n' | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Album Release Date","Disc Number","Track Number","Track Duration (ms)","Explicit","Popularity","Added By","Added At"\n' + | ||
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","2017-02-17","1","3","198093","false","2","","2020-07-19T09:24:39Z"\n' | ||
], | ||
options: { type: 'text/csv;charset=utf-8' } | ||
}, | ||
'liked.csv', | ||
true | ||
) | ||
}) | ||
}) | ||
|
||
test("including additional artist data", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you elaborate for me what this testing framework is about and how to use it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm actually going to write something (possibly a blog article) on working with the specifics of working with Jest and MSW for the first time. Overally it's been great though with a few caveats. I'm not sure if that's what you were asking or if you were asking a more general question. Just in case, in a more general sense, the test suite is there to validate the behavior of the app. It's mostly about guarding against regressions so I'm free to refactor without worrying too much about breaking things. In this particular case, most of my tests take the form of mocking out the API calls as close to the HTTP transport layer as possible (so as to be client-agnostic), and then asserting that the app behaves in the expected way given a particular set of responses. The tests don't actually call the API, so it wouldn't help in the case of breaking changes to the Spotify API. For that I have to rely on user feedback but I also have a last line of defence: exception monitoring with Bugsnag. |
||
const saveAsMock = jest.spyOn(FileSaver, "saveAs") | ||
saveAsMock.mockImplementation(jest.fn()) | ||
|
||
render(<PlaylistTable accessToken="TEST_ACCESS_TOKEN" config={{ includeArtistsData: true }} />); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText(/Export All/)).toBeInTheDocument() | ||
}) | ||
|
||
const linkElement = screen.getAllByText("Export")[0] | ||
|
||
expect(linkElement).toBeInTheDocument() | ||
|
||
fireEvent.click(linkElement) | ||
|
||
await waitFor(() => { | ||
expect(handlerCalled.mock.calls).toEqual([ // Ensure API call order and no duplicates | ||
[ 'https://api.spotify.com/v1/me' ], | ||
[ 'https://api.spotify.com/v1/users/watsonbox/playlists' ], | ||
[ 'https://api.spotify.com/v1/users/watsonbox/tracks' ], | ||
[ 'https://api.spotify.com/v1/me/tracks?offset=0&limit=20' ], | ||
[ 'https://api.spotify.com/v1/artists?ids=4TXdHyuAOl3rAOFmZ6MeKz' ] | ||
]) | ||
|
||
expect(saveAsMock).toHaveBeenCalledTimes(1) | ||
expect(saveAsMock).toHaveBeenCalledWith( | ||
{ | ||
content: [ | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Album Release Date","Disc Number","Track Number","Track Duration (ms)","Explicit","Popularity","Added By","Added At","Artist Genres"\n' + | ||
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","2017-02-17","1","3","198093","false","2","","2020-07-19T09:24:39Z","nottingham indie"\n' | ||
], | ||
options: { type: 'text/csv;charset=utf-8' } | ||
}, | ||
'liked.csv', | ||
true | ||
) | ||
}) | ||
}) | ||
|
||
test("including additional audio features data", async () => { | ||
const saveAsMock = jest.spyOn(FileSaver, "saveAs") | ||
saveAsMock.mockImplementation(jest.fn()) | ||
|
||
render(<PlaylistTable accessToken="TEST_ACCESS_TOKEN" config={{ includeAudioFeaturesData: true }} />); | ||
|
||
await waitFor(() => { | ||
expect(screen.getByText(/Export All/)).toBeInTheDocument() | ||
}) | ||
|
||
const linkElement = screen.getAllByText("Export")[0] | ||
|
||
expect(linkElement).toBeInTheDocument() | ||
|
||
fireEvent.click(linkElement) | ||
|
||
await waitFor(() => { | ||
expect(handlerCalled.mock.calls).toEqual([ // Ensure API call order and no duplicates | ||
[ 'https://api.spotify.com/v1/me' ], | ||
[ 'https://api.spotify.com/v1/users/watsonbox/playlists' ], | ||
[ 'https://api.spotify.com/v1/users/watsonbox/tracks' ], | ||
[ 'https://api.spotify.com/v1/me/tracks?offset=0&limit=20' ], | ||
[ 'https://api.spotify.com/v1/audio-features?ids=1GrLfs4TEvAZ86HVzXHchS' ] | ||
]) | ||
|
||
expect(saveAsMock).toHaveBeenCalledTimes(1) | ||
expect(saveAsMock).toHaveBeenCalledWith( | ||
{ | ||
content: [ | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Album Release Date","Disc Number","Track Number","Track Duration (ms)","Explicit","Popularity","Added By","Added At","Danceability","Energy","Key","Loudness","Mode","Speechiness","Acousticness","Instrumentalness","Liveness","Valence","Tempo","Time Signature"\n' + | ||
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","2017-02-17","1","3","198093","false","2","","2020-07-19T09:24:39Z","0.416","0.971","0","-5.55","1","0.0575","0.00104","0.0391","0.44","0.19","131.988","4"\n' | ||
], | ||
options: { type: 'text/csv;charset=utf-8' } | ||
}, | ||
|
@@ -121,7 +200,7 @@ describe("single playlist exporting", () => { | |
expect(saveAsMock).toHaveBeenCalledWith( | ||
{ | ||
content: [ | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Disc Number","Track Number","Track Duration (ms)","Added By","Added At"\n' | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Album Release Date","Disc Number","Track Number","Track Duration (ms)","Explicit","Popularity","Added By","Added At"\n' | ||
], | ||
options: { type: 'text/csv;charset=utf-8' } | ||
}, | ||
|
@@ -156,14 +235,14 @@ test("exporting of all playlists", async () => { | |
expect(jsZipFileMock).toHaveBeenCalledTimes(2) | ||
expect(jsZipFileMock).toHaveBeenCalledWith( | ||
"liked.csv", | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Disc Number","Track Number","Track Duration (ms)","Added By","Added At"\n' + | ||
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","1","3","198093","","2020-07-19T09:24:39Z"\n' | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Album Release Date","Disc Number","Track Number","Track Duration (ms)","Explicit","Popularity","Added By","Added At"\n' + | ||
'"spotify:track:1GrLfs4TEvAZ86HVzXHchS","Crying","spotify:artist:4TXdHyuAOl3rAOFmZ6MeKz","Six by Seven","spotify:album:4iwv7b8gDPKztLkKCbWyhi","Best of Six By Seven","2017-02-17","1","3","198093","false","2","","2020-07-19T09:24:39Z"\n' | ||
) | ||
expect(jsZipFileMock).toHaveBeenCalledWith( | ||
"ghostpoet_–_peanut_butter_blues_and_melancholy_jam.csv", | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Disc Number","Track Number","Track Duration (ms)","Added By","Added At"\n' + | ||
'"spotify:track:7ATyvp3TmYBmGW7YuC8DJ3","One Twos / Run Run Run","spotify:artist:69lEbRQRe29JdyLrewNAvD","Ghostpoet","spotify:album:6jiLkuSnhzDvzsHJlweoGh","Peanut Butter Blues and Melancholy Jam","1","1","241346","spotify:user:watsonbox","2020-11-03T15:19:04Z"\n' + | ||
'"spotify:track:0FNanBLvmFEDyD75Whjj52","Us Against Whatever Ever","spotify:artist:69lEbRQRe29JdyLrewNAvD","Ghostpoet","spotify:album:6jiLkuSnhzDvzsHJlweoGh","Peanut Butter Blues and Melancholy Jam","1","2","269346","spotify:user:watsonbox","2020-11-03T15:19:04Z"\n' | ||
'"Track URI","Track Name","Artist URI","Artist Name","Album URI","Album Name","Album Release Date","Disc Number","Track Number","Track Duration (ms)","Explicit","Popularity","Added By","Added At"\n' + | ||
'"spotify:track:7ATyvp3TmYBmGW7YuC8DJ3","One Twos / Run Run Run","spotify:artist:69lEbRQRe29JdyLrewNAvD","Ghostpoet","spotify:album:6jiLkuSnhzDvzsHJlweoGh","Peanut Butter Blues and Melancholy Jam","2011","1","1","241346","false","22","spotify:user:watsonbox","2020-11-03T15:19:04Z"\n' + | ||
'"spotify:track:0FNanBLvmFEDyD75Whjj52","Us Against Whatever Ever","spotify:artist:69lEbRQRe29JdyLrewNAvD","Ghostpoet","spotify:album:6jiLkuSnhzDvzsHJlweoGh","Peanut Butter Blues and Melancholy Jam","2011","1","2","269346","false","36","spotify:user:watsonbox","2020-11-03T15:19:04Z"\n' | ||
) | ||
}) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cog is a really neat UI feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, glad you like it. Yep I think it works well and doesn't get in the way.