diff --git a/client/src/api.ts b/client/src/api.ts index df4093c..d1cb334 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -331,17 +331,17 @@ export interface RegisterUserResponse { } export function checkPassword(password: string): boolean { const result = (password.length < 32) && (password.length >= 8) && - /^[\x00-\x7F]*$/.test(password) && // is ASCII + password.split("").every(char => char.charCodeAt(0) <= 127) && // is ASCII (/[a-z]/g.test(password)) && // has lowercase (/[A-Z]/g.test(password)) && // has uppercase (/[0-9]/g.test(password)) && // has number - (/[!@#\$%\^&\*\(\)_\+/]/g.test(password)) // has special character; + (/[!@#$%^&*()_+/]/g.test(password)) // has special character; console.log("Password check for ", password, ": ", result); return result; } export function checkEmail(email: string): boolean { - const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; const result = re.test(String(email).toLowerCase()); console.log("Email check for ", email, ": ", result); return result; diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx index 725f24b..2241615 100644 --- a/client/src/components/MainWindow.tsx +++ b/client/src/components/MainWindow.tsx @@ -1,19 +1,17 @@ -import React, { useReducer, Reducer, useContext } from 'react'; +import React from 'react'; import { ThemeProvider, CssBaseline, createMuiTheme } from '@material-ui/core'; import { grey } from '@material-ui/core/colors'; import AppBar, { AppBarTab } from './appbar/AppBar'; -import QueryWindow, { QueryWindowReducer } from './windows/query/QueryWindow'; -import { newWindowState, newWindowReducer, WindowType } from './windows/Windows'; +import QueryWindow from './windows/query/QueryWindow'; import ArtistWindow from './windows/artist/ArtistWindow'; import AlbumWindow from './windows/album/AlbumWindow'; import TagWindow from './windows/tag/TagWindow'; import SongWindow from './windows/song/SongWindow'; import ManageTagsWindow from './windows/manage_tags/ManageTagsWindow'; -import { BrowserRouter, Switch, Route, useParams, Redirect } from 'react-router-dom'; +import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom'; import LoginWindow from './windows/login/LoginWindow'; import { useAuth } from '../lib/useAuth'; import RegisterWindow from './windows/register/RegisterWindow'; -var _ = require('lodash'); const darkTheme = createMuiTheme({ palette: { diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx index 07e113c..b39601f 100644 --- a/client/src/components/appbar/AppBar.tsx +++ b/client/src/components/appbar/AppBar.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography, Menu, MenuItem } from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; import SearchIcon from '@material-ui/icons/Search'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import { Link, useHistory } from 'react-router-dom'; -import { WindowType } from '../windows/Windows'; import { useAuth } from '../../lib/useAuth'; export enum AppBarTab { diff --git a/client/src/components/querybuilder/QBLeafElem.tsx b/client/src/components/querybuilder/QBLeafElem.tsx index 6398b7e..dad3d03 100644 --- a/client/src/components/querybuilder/QBLeafElem.tsx +++ b/client/src/components/querybuilder/QBLeafElem.tsx @@ -110,56 +110,56 @@ export function QBLeafElem(props: IProps) { : undefined; - if (e.a == QueryLeafBy.ArtistName && - e.leafOp == QueryLeafOp.Equals && + if (e.a === QueryLeafBy.ArtistName && + e.leafOp === QueryLeafOp.Equals && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.ArtistName && - e.leafOp == QueryLeafOp.Like && + } else if (e.a === QueryLeafBy.ArtistName && + e.leafOp === QueryLeafOp.Like && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.AlbumName && - e.leafOp == QueryLeafOp.Equals && + } else if (e.a === QueryLeafBy.AlbumName && + e.leafOp === QueryLeafOp.Equals && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.AlbumName && - e.leafOp == QueryLeafOp.Like && + } else if (e.a === QueryLeafBy.AlbumName && + e.leafOp === QueryLeafOp.Like && typeof e.b == "string") { return - } if (e.a == QueryLeafBy.SongTitle && - e.leafOp == QueryLeafOp.Equals && + } if (e.a === QueryLeafBy.SongTitle && + e.leafOp === QueryLeafOp.Equals && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.SongTitle && - e.leafOp == QueryLeafOp.Like && + } else if (e.a === QueryLeafBy.SongTitle && + e.leafOp === QueryLeafOp.Like && typeof e.b == "string") { return - } else if (e.a == QueryLeafBy.TagInfo && - e.leafOp == QueryLeafOp.Equals && + } else if (e.a === QueryLeafBy.TagInfo && + e.leafOp === QueryLeafOp.Equals && isTagQueryInfo(e.b)) { return - }else if (e.leafOp == QueryLeafOp.Placeholder) { + }else if (e.leafOp === QueryLeafOp.Placeholder) { return }); - if (e.nodeOp == QueryNodeOp.And) { + if (e.nodeOp === QueryNodeOp.And) { return {children} - } else if (e.nodeOp == QueryNodeOp.Or) { + } else if (e.nodeOp === QueryNodeOp.Or) { return {children} } - throw "Unsupported node element"; + throw new Error("Unsupported node element"); } \ No newline at end of file diff --git a/client/src/components/querybuilder/QBSelectWithRequest.tsx b/client/src/components/querybuilder/QBSelectWithRequest.tsx index 107e982..77d4574 100644 --- a/client/src/components/querybuilder/QBSelectWithRequest.tsx +++ b/client/src/components/querybuilder/QBSelectWithRequest.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete from '@material-ui/lab/Autocomplete'; import CircularProgress from '@material-ui/core/CircularProgress'; @@ -26,26 +26,26 @@ export default function QBSelectWithRequest(props: IProps & any) { const loading: boolean = !options || options.forInput !== input; - const updateOptions = (forInput: string, options: any[]) => { + const updateOptions = useCallback((forInput: string, options: any[]) => { if (forInput === input) { setOptions({ forInput: forInput, options: options, }); } - } + }, [setOptions, input]); - const startRequest = (_input: string) => { + const startRequest = useCallback((_input: string) => { setInput(_input); (async () => { const newOptions = await getNewOptions(_input); updateOptions(_input, newOptions); })(); - }; + }, [setInput, getNewOptions, updateOptions]); useEffect(() => { startRequest(input); - }, [input]); + }, [input, startRequest]); const onInputChange = (e: any, val: any, reason: any) => { if (reason === 'reset') { diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx index 5fee3d1..1bdce75 100644 --- a/client/src/components/tables/ResultsTable.tsx +++ b/client/src/components/tables/ResultsTable.tsx @@ -50,10 +50,8 @@ export default function SongTable(props: { const artistNames = props.songGetters.getArtistNames(song); const artist = stringifyList(artistNames); const mainArtistId = props.songGetters.getArtistIds(song)[0]; - const mainArtistName = artistNames[0]; const albumNames = props.songGetters.getAlbumNames(song); const album = stringifyList(albumNames); - const mainAlbumName = albumNames[0]; const mainAlbumId = props.songGetters.getAlbumIds(song)[0]; const songId = props.songGetters.getId(song); const tagIds = props.songGetters.getTagIds(song); diff --git a/client/src/components/windows/album/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx index 07daa38..7d14329 100644 --- a/client/src/components/windows/album/AlbumWindow.tsx +++ b/client/src/components/windows/album/AlbumWindow.tsx @@ -12,7 +12,6 @@ import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { queryAlbums, querySongs } from '../../../lib/backend/queries'; import { songGetters } from '../../../lib/songGetters'; import { useParams } from 'react-router'; -var _ = require('lodash'); export type AlbumMetadata = serverApi.AlbumDetails; export type AlbumMetadataChanges = serverApi.ModifyAlbumRequest; @@ -76,40 +75,40 @@ export function AlbumWindowControlled(props: { state: AlbumWindowState, dispatch: (action: any) => void, }) { - let metadata = props.state.metadata; - let pendingChanges = props.state.pendingChanges; + let { id: albumId, metadata, pendingChanges, songsOnAlbum } = props.state; + let { dispatch } = props; // Effect to get the album's metadata. useEffect(() => { - getAlbumMetadata(props.state.id) + getAlbumMetadata(albumId) .then((m: AlbumMetadata) => { - props.dispatch({ + dispatch({ type: AlbumWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.name]); + }, [albumId, dispatch]); // Effect to get the album's songs. useEffect(() => { - if (props.state.songsOnAlbum) { return; } + if (songsOnAlbum) { return; } (async () => { const songs = await querySongs({ query: { a: QueryLeafBy.AlbumId, - b: props.state.id, + b: albumId, leafOp: QueryLeafOp.Equals, }, offset: 0, limit: -1, }); - props.dispatch({ + dispatch({ type: AlbumWindowStateActions.SetSongs, value: songs, }); })(); - }, [props.state.songsOnAlbum]); + }, [songsOnAlbum, albumId, dispatch]); const [editingName, setEditingName] = useState(null); const name = { const store = whichStore(link); return store && void, }) { - let metadata = props.state.metadata; - let pendingChanges = props.state.pendingChanges; + let { metadata, id: artistId, pendingChanges, songsByArtist } = props.state; + let { dispatch } = props; // Effect to get the artist's metadata. useEffect(() => { - getArtistMetadata(props.state.id) + getArtistMetadata(artistId) .then((m: ArtistMetadata) => { - props.dispatch({ + dispatch({ type: ArtistWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.name]); + }, [artistId, dispatch]); // Effect to get the artist's songs. useEffect(() => { - if (props.state.songsByArtist) { return; } + if (songsByArtist) { return; } (async () => { const songs = await querySongs({ query: { a: QueryLeafBy.ArtistId, - b: props.state.id, + b: artistId, leafOp: QueryLeafOp.Equals, }, offset: 0, limit: -1, }); - props.dispatch({ + dispatch({ type: ArtistWindowStateActions.SetSongs, value: songs, }); })(); - }, [props.state.songsByArtist]); + }, [songsByArtist, dispatch, artistId]); const [editingName, setEditingName] = useState(null); const name = { const store = whichStore(link); return store && void, }) { const [newTagMenuPos, setNewTagMenuPos] = React.useState(null); + let { fetchedTags } = props.state; + let { dispatch } = props; const onOpenNewTagMenu = (e: any) => { setNewTagMenuPos([e.clientX, e.clientY]) @@ -364,19 +364,19 @@ export function ManageTagsWindowControlled(props: { }; useEffect(() => { - if (props.state.fetchedTags !== null) { + if (fetchedTags !== null) { return; } (async () => { const allTags = await getAllTags(); // We have the tags in list form. Now, we want to organize // them hierarchically by giving each tag a "children" prop. - props.dispatch({ + dispatch({ type: ManageTagsWindowActions.SetFetchedTags, value: allTags, }); })(); - }, [props.state.fetchedTags]); + }, [fetchedTags, dispatch]); const tagsWithChanges = annotateTagsWithChanges(props.state.fetchedTags || {}, props.state.pendingChanges || []) const changedTags = organiseTags( diff --git a/client/src/components/windows/manage_tags/NewTagMenu.tsx b/client/src/components/windows/manage_tags/NewTagMenu.tsx index 3d16315..988ed93 100644 --- a/client/src/components/windows/manage_tags/NewTagMenu.tsx +++ b/client/src/components/windows/manage_tags/NewTagMenu.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Menu, MenuItem, TextField, Input } from '@material-ui/core'; +import React from 'react'; +import { Menu } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; import MenuEditText from '../../common/MenuEditText'; diff --git a/client/src/components/windows/manage_tags/TagChange.tsx b/client/src/components/windows/manage_tags/TagChange.tsx index cce1c06..b2e9c35 100644 --- a/client/src/components/windows/manage_tags/TagChange.tsx +++ b/client/src/components/windows/manage_tags/TagChange.tsx @@ -1,7 +1,5 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { Typography, Chip, CircularProgress, Box, Paper } from '@material-ui/core'; -import { queryTags } from '../../../lib/backend/queries'; -import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import DiscardChangesButton from '../../common/DiscardChangesButton'; import SubmitChangesButton from '../../common/SubmitChangesButton'; import { createTag, modifyTag, deleteTag, mergeTag } from '../../../lib/backend/tags'; @@ -31,7 +29,7 @@ export async function submitTagChanges(changes: TagChange[]) { var id_lookup: Record = {} const getId = (id_string: string) => { - return (Number(id_string) === NaN) ? + return (isNaN(Number(id_string))) ? id_lookup[id_string] : Number(id_string); } diff --git a/client/src/components/windows/query/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx index 9561883..85afec5 100644 --- a/client/src/components/windows/query/QueryWindow.tsx +++ b/client/src/components/windows/query/QueryWindow.tsx @@ -1,24 +1,13 @@ -import React, { useEffect, useReducer } from 'react'; -import { createMuiTheme, Box, LinearProgress } from '@material-ui/core'; -import { QueryElem, toApiQuery, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; +import React, { useEffect, useReducer, useCallback } from 'react'; +import { Box, LinearProgress } from '@material-ui/core'; +import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import QueryBuilder from '../../querybuilder/QueryBuilder'; -import * as serverApi from '../../../api'; import SongTable from '../../tables/ResultsTable'; import { songGetters } from '../../../lib/songGetters'; import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries'; -import { grey } from '@material-ui/core/colors'; import { WindowState } from '../Windows'; var _ = require('lodash'); -const darkTheme = createMuiTheme({ - palette: { - type: 'dark', - primary: { - main: grey[100], - } - }, -}); - export interface ResultsForQuery { for: QueryElem, results: any[], @@ -112,23 +101,23 @@ export function QueryWindowControlled(props: { state: QueryWindowState, dispatch: (action: any) => void, }) { - let query = props.state.query; - let editing = props.state.editingQuery; - let resultsFor = props.state.resultsForQuery; + let { query, editingQuery: editing, resultsForQuery: resultsFor } = props.state; + let { dispatch } = props; + let setQuery = (q: QueryElem | null) => { props.dispatch({ type: QueryWindowStateActions.SetQuery, value: q }); } let setEditingQuery = (e: boolean) => { props.dispatch({ type: QueryWindowStateActions.SetEditingQuery, value: e }); } - let setResultsForQuery = (r: ResultsForQuery | null) => { - props.dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); - } + let setResultsForQuery = useCallback((r: ResultsForQuery | null) => { + dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); + }, [ dispatch ]); const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); - const showResults = (query && resultsFor && query == resultsFor.for) ? resultsFor.results : []; + const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : []; - const doQuery = async (_query: QueryElem) => { + const doQuery = useCallback(async (_query: QueryElem) => { const songs = await querySongs({ query: _query, offset: 0, @@ -141,7 +130,7 @@ export function QueryWindowControlled(props: { results: songs, }) } - } + }, [query, setResultsForQuery]); useEffect(() => { if (query) { @@ -149,7 +138,7 @@ export function QueryWindowControlled(props: { } else { setResultsForQuery(null); } - }, [query]); + }, [query, doQuery, setResultsForQuery]); return void, }) { let history: any = useHistory(); - let location: any = useLocation(); let auth: Auth = useAuth(); - let { from } = location.state || { from: { pathname: "/" } }; const onSubmit = (event: any) => { event.preventDefault(); diff --git a/client/src/components/windows/song/SongWindow.tsx b/client/src/components/windows/song/SongWindow.tsx index 8ba8b6a..c0f9e93 100644 --- a/client/src/components/windows/song/SongWindow.tsx +++ b/client/src/components/windows/song/SongWindow.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useReducer } from 'react'; -import { Box, Typography, IconButton, Button, CircularProgress } from '@material-ui/core'; +import { Box, Typography, IconButton, CircularProgress } from '@material-ui/core'; import AudiotrackIcon from '@material-ui/icons/Audiotrack'; import PersonIcon from '@material-ui/icons/Person'; import AlbumIcon from '@material-ui/icons/Album'; @@ -13,7 +13,6 @@ import SubmitChangesButton from '../../common/SubmitChangesButton'; import { saveSongChanges } from '../../../lib/saveChanges'; import { QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query'; import { querySongs } from '../../../lib/backend/queries'; -import { songGetters } from '../../../lib/songGetters'; import { useParams } from 'react-router'; export type SongMetadata = serverApi.SongDetails; @@ -71,18 +70,18 @@ export function SongWindowControlled(props: { state: SongWindowState, dispatch: (action: any) => void, }) { - let metadata = props.state.metadata; - let pendingChanges = props.state.pendingChanges; + let { pendingChanges, metadata, id: songId } = props.state; + let { dispatch } = props; useEffect(() => { - getSongMetadata(props.state.id) + getSongMetadata(songId) .then((m: SongMetadata) => { - props.dispatch({ + dispatch({ type: SongWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.title]); + }, [songId, dispatch]); const [editingTitle, setEditingTitle] = useState(null); const title = { const store = whichStore(link); return store && { - getTagMetadata(props.state.id) + getTagMetadata(tagId) .then((m: TagMetadata) => { - props.dispatch({ + dispatch({ type: TagWindowStateActions.SetMetadata, value: m }); }) - }, [metadata?.name]); + }, [tagId, dispatch]); // Effect to get the tag's songs. useEffect(() => { - if (props.state.songsWithTag) { return; } + if (songsWithTag) { return; } (async () => { const songs = await querySongs({ query: { a: QueryLeafBy.TagId, - b: props.state.id, + b: tagId, leafOp: QueryLeafOp.Equals, }, offset: 0, limit: -1, }); - props.dispatch({ + dispatch({ type: TagWindowStateActions.SetSongs, value: songs, - }); + }); })(); - }, [props.state.songsWithTag]); + }, [songsWithTag, tagId, dispatch]); const [editingName, setEditingName] = useState(null); const name = const fullName = {metadata?.fullName.map((n: string, i: number) => { - if (metadata?.fullName && i == metadata?.fullName.length - 1) { + if (metadata?.fullName && i === metadata?.fullName.length - 1) { return name; } else if (i >= (metadata?.fullName.length || 0) - 1) { return undefined; @@ -160,7 +161,7 @@ export function TagWindowControlled(props: { const storeLinks = metadata?.storeLinks && metadata?.storeLinks.map((link: string) => { const store = whichStore(link); return store && { - if (isLeafElem(op) && op.leafOp == QueryLeafOp.Placeholder) { + if (isLeafElem(op) && op.leafOp === QueryLeafOp.Placeholder) { return; } const newOp = removePlaceholders(op); @@ -136,14 +136,14 @@ export function removePlaceholders(q: QueryElem | null): QueryElem | null { } }) - if (newOperands.length == 0) { + if (newOperands.length === 0) { return null; } - if (newOperands.length == 1) { + if (newOperands.length === 1) { return newOperands[0]; } return { operands: newOperands, nodeOp: q.nodeOp }; - } else if (q && isLeafElem(q) && q.leafOp == QueryLeafOp.Placeholder) { + } else if (q && isLeafElem(q) && q.leafOp === QueryLeafOp.Placeholder) { return null; }