{count} {labelPlural.toLowerCase()}
diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js
index 9f8bdee5b..15f31d3c5 100644
--- a/frontend/src/Settings/Tags/TagsConnector.js
+++ b/frontend/src/Settings/Tags/TagsConnector.js
@@ -4,11 +4,13 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
+import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
+import sortByProp from 'Utilities/Array/sortByProp';
import Tags from './Tags';
function createMapStateToProps() {
return createSelector(
- (state) => state.tags,
+ createSortedSectionSelector('tags', sortByProp('label')),
(tags) => {
const isFetching = tags.isFetching || tags.details.isFetching;
const error = tags.error || tags.details.error;
diff --git a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js
index dfe29ace8..3de794bdf 100644
--- a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js
+++ b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js
@@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
return function(getState, payload, dispatch) {
const {
id,
- ...queryParams
+ queryParams
} = payload;
dispatch(set({ section, isDeleting: true }));
diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
index ca26883fb..e35157dbd 100644
--- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
+++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
@@ -1,8 +1,11 @@
+import $ from 'jquery';
+import _ from 'lodash';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getProviderState from 'Utilities/State/getProviderState';
import { set } from '../baseActions';
const abortCurrentRequests = {};
+let lastTestData = null;
export function createCancelTestProviderHandler(section) {
return function(getState, payload, dispatch) {
@@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
return function(getState, payload, dispatch) {
dispatch(set({ section, isTesting: true }));
- const testData = getProviderState(payload, getState, section);
+ const {
+ queryParams = {},
+ ...otherPayload
+ } = payload;
+
+ const testData = getProviderState({ ...otherPayload }, getState, section);
+ const params = { ...queryParams };
+
+ // If the user is re-testing the same provider without changes
+ // force it to be tested.
+
+ if (_.isEqual(testData, lastTestData)) {
+ params.forceTest = true;
+ }
+
+ lastTestData = testData;
const ajaxOptions = {
- url: `${url}/test`,
+ url: `${url}/test?${$.param(params, true)}`,
method: 'POST',
contentType: 'application/json',
dataType: 'json',
@@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
abortCurrentRequests[section] = abortRequest;
request.done((data) => {
+ lastTestData = null;
+
dispatch(set({
section,
isTesting: false,
diff --git a/frontend/src/Store/Actions/Settings/customFormats.js b/frontend/src/Store/Actions/Settings/customFormats.js
index 4a175abea..3b8a209f9 100644
--- a/frontend/src/Store/Actions/Settings/customFormats.js
+++ b/frontend/src/Store/Actions/Settings/customFormats.js
@@ -1,7 +1,12 @@
import { createAction } from 'redux-actions';
+import { sortDirections } from 'Helpers/Props';
+import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
+import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
+import createSetClientSideCollectionSortReducer
+ from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
@@ -21,6 +26,9 @@ export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
+export const BULK_EDIT_CUSTOM_FORMATS = 'settings/downloadClients/bulkEditCustomFormats';
+export const BULK_DELETE_CUSTOM_FORMATS = 'settings/downloadClients/bulkDeleteCustomFormats';
+export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManageCustomFormatsSort';
//
// Action Creators
@@ -28,6 +36,9 @@ export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
+export const bulkEditCustomFormats = createThunk(BULK_EDIT_CUSTOM_FORMATS);
+export const bulkDeleteCustomFormats = createThunk(BULK_DELETE_CUSTOM_FORMATS);
+export const setManageCustomFormatsSort = createAction(SET_MANAGE_CUSTOM_FORMATS_SORT);
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
return {
@@ -47,20 +58,30 @@ export default {
// State
defaultState: {
- isSchemaFetching: false,
- isSchemaPopulated: false,
isFetching: false,
isPopulated: false,
+ error: null,
+ isSaving: false,
+ saveError: null,
+ isDeleting: false,
+ deleteError: null,
+ items: [],
+ pendingChanges: {},
+
+ isSchemaFetching: false,
+ isSchemaPopulated: false,
+ schemaError: null,
schema: {
includeCustomFormatWhenRenaming: false
},
- error: null,
- isDeleting: false,
- deleteError: null,
- isSaving: false,
- saveError: null,
- items: [],
- pendingChanges: {}
+
+ sortKey: 'name',
+ sortDirection: sortDirections.ASCENDING,
+ sortPredicates: {
+ name: ({ name }) => {
+ return name.toLocaleLowerCase();
+ }
+ }
},
//
@@ -82,7 +103,10 @@ export default {
}));
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
- }
+ },
+
+ [BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'),
+ [BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk')
},
//
@@ -102,7 +126,9 @@ export default {
newState.pendingChanges = pendingChanges;
return updateSectionState(state, section, newState);
- }
+ },
+
+ [SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section)
}
};
diff --git a/frontend/src/Store/Actions/Settings/downloadClients.js b/frontend/src/Store/Actions/Settings/downloadClients.js
index aee945ef5..1113e7daf 100644
--- a/frontend/src/Store/Actions/Settings/downloadClients.js
+++ b/frontend/src/Store/Actions/Settings/downloadClients.js
@@ -96,8 +96,8 @@ export default {
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
- name: function(item) {
- return item.name.toLowerCase();
+ name: ({ name }) => {
+ return name.toLocaleLowerCase();
}
}
},
diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js
index 1e9aded2f..511a2e475 100644
--- a/frontend/src/Store/Actions/Settings/indexers.js
+++ b/frontend/src/Store/Actions/Settings/indexers.js
@@ -100,8 +100,8 @@ export default {
sortKey: 'name',
sortDirection: sortDirections.ASCENDING,
sortPredicates: {
- name: function(item) {
- return item.name.toLowerCase();
+ name: ({ name }) => {
+ return name.toLocaleLowerCase();
}
}
},
diff --git a/frontend/src/Store/Actions/albumSelectionActions.js b/frontend/src/Store/Actions/albumSelectionActions.js
new file mode 100644
index 000000000..f19f5b691
--- /dev/null
+++ b/frontend/src/Store/Actions/albumSelectionActions.js
@@ -0,0 +1,86 @@
+import moment from 'moment';
+import { createAction } from 'redux-actions';
+import { sortDirections } from 'Helpers/Props';
+import { createThunk, handleThunks } from 'Store/thunks';
+import updateSectionState from 'Utilities/State/updateSectionState';
+import createFetchHandler from './Creators/createFetchHandler';
+import createHandleActions from './Creators/createHandleActions';
+import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
+
+//
+// Variables
+
+export const section = 'albumSelection';
+
+//
+// State
+
+export const defaultState = {
+ isFetching: false,
+ isReprocessing: false,
+ isPopulated: false,
+ error: null,
+ sortKey: 'title',
+ sortDirection: sortDirections.ASCENDING,
+ items: [],
+ sortPredicates: {
+ title: ({ title }) => {
+ return title.toLocaleLowerCase();
+ },
+
+ releaseDate: function({ releaseDate }, direction) {
+ if (releaseDate) {
+ return moment(releaseDate).unix();
+ }
+
+ if (direction === sortDirections.DESCENDING) {
+ return 0;
+ }
+
+ return Number.MAX_VALUE;
+ }
+ }
+};
+
+export const persistState = [
+ 'albumSelection.sortKey',
+ 'albumSelection.sortDirection'
+];
+
+//
+// Actions Types
+
+export const FETCH_ALBUMS = 'albumSelection/fetchAlbums';
+export const SET_ALBUMS_SORT = 'albumSelection/setAlbumsSort';
+export const CLEAR_ALBUMS = 'albumSelection/clearAlbums';
+
+//
+// Action Creators
+
+export const fetchAlbums = createThunk(FETCH_ALBUMS);
+export const setAlbumsSort = createAction(SET_ALBUMS_SORT);
+export const clearAlbums = createAction(CLEAR_ALBUMS);
+
+//
+// Action Handlers
+
+export const actionHandlers = handleThunks({
+ [FETCH_ALBUMS]: createFetchHandler(section, '/album')
+});
+
+//
+// Reducers
+
+export const reducers = createHandleActions({
+
+ [SET_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(section),
+
+ [CLEAR_ALBUMS]: (state) => {
+ return updateSectionState(state, section, {
+ ...defaultState,
+ sortKey: state.sortKey,
+ sortDirection: state.sortDirection
+ });
+ }
+
+}, defaultState, section);
diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js
index 72cb20142..736502460 100644
--- a/frontend/src/Store/Actions/artistIndexActions.js
+++ b/frontend/src/Store/Actions/artistIndexActions.js
@@ -1,6 +1,6 @@
import { createAction } from 'redux-actions';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props';
-import sortByName from 'Utilities/Array/sortByName';
+import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import { filterPredicates, filters, sortPredicates } from './artistActions';
import createHandleActions from './Creators/createHandleActions';
@@ -151,7 +151,7 @@ export const defaultState = {
{
name: 'genres',
label: () => translate('Genres'),
- isSortable: false,
+ isSortable: true,
isVisible: false
},
{
@@ -334,7 +334,7 @@ export const defaultState = {
return acc;
}, []);
- return tagList.sort(sortByName);
+ return tagList.sort(sortByProp('name'));
}
},
{
diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js
index 225698229..9d16d29c4 100644
--- a/frontend/src/Store/Actions/historyActions.js
+++ b/frontend/src/Store/Actions/historyActions.js
@@ -150,7 +150,7 @@ export const defaultState = {
},
{
key: 'importFailed',
- label: () => translate('ImportFailed'),
+ label: () => translate('ImportCompleteFailed'),
filters: [
{
key: 'eventType',
diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js
index 95b02d089..85fda482b 100644
--- a/frontend/src/Store/Actions/index.js
+++ b/frontend/src/Store/Actions/index.js
@@ -1,5 +1,6 @@
import * as albums from './albumActions';
import * as albumHistory from './albumHistoryActions';
+import * as albumSelection from './albumSelectionActions';
import * as app from './appActions';
import * as artist from './artistActions';
import * as artistHistory from './artistHistoryActions';
@@ -13,6 +14,7 @@ import * as history from './historyActions';
import * as interactiveImportActions from './interactiveImportActions';
import * as oAuth from './oAuthActions';
import * as organizePreview from './organizePreviewActions';
+import * as parse from './parseActions';
import * as paths from './pathActions';
import * as providerOptions from './providerOptionActions';
import * as queue from './queueActions';
@@ -28,26 +30,28 @@ import * as wanted from './wantedActions';
export default [
app,
+ albums,
+ albumHistory,
+ albumSelection,
+ artist,
+ artistHistory,
+ artistIndex,
blocklist,
captcha,
calendar,
commands,
customFilters,
- albums,
trackFiles,
- albumHistory,
history,
interactiveImportActions,
oAuth,
organizePreview,
retagPreview,
+ parse,
paths,
providerOptions,
queue,
releases,
- artist,
- artistHistory,
- artistIndex,
search,
settings,
system,
diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js
index d174f443b..a250292c5 100644
--- a/frontend/src/Store/Actions/interactiveImportActions.js
+++ b/frontend/src/Store/Actions/interactiveImportActions.js
@@ -16,7 +16,6 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create
export const section = 'interactiveImport';
-const albumsSection = `${section}.albums`;
const trackFilesSection = `${section}.trackFiles`;
let abortCurrentFetchRequest = null;
let abortCurrentRequest = null;
@@ -58,15 +57,6 @@ export const defaultState = {
}
},
- albums: {
- isFetching: false,
- isPopulated: false,
- error: null,
- sortKey: 'albumTitle',
- sortDirection: sortDirections.ASCENDING,
- items: []
- },
-
trackFiles: {
isFetching: false,
isPopulated: false,
@@ -97,10 +87,6 @@ export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
-export const FETCH_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/fetchInteractiveImportAlbums';
-export const SET_INTERACTIVE_IMPORT_ALBUMS_SORT = 'interactiveImport/clearInteractiveImportAlbumsSort';
-export const CLEAR_INTERACTIVE_IMPORT_ALBUMS = 'interactiveImport/clearInteractiveImportAlbums';
-
export const FETCH_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/fetchInteractiveImportTrackFiles';
export const CLEAR_INTERACTIVE_IMPORT_TRACKFILES = 'interactiveImport/clearInteractiveImportTrackFiles';
@@ -117,10 +103,6 @@ export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
-export const fetchInteractiveImportAlbums = createThunk(FETCH_INTERACTIVE_IMPORT_ALBUMS);
-export const setInteractiveImportAlbumsSort = createAction(SET_INTERACTIVE_IMPORT_ALBUMS_SORT);
-export const clearInteractiveImportAlbums = createAction(CLEAR_INTERACTIVE_IMPORT_ALBUMS);
-
export const fetchInteractiveImportTrackFiles = createThunk(FETCH_INTERACTIVE_IMPORT_TRACKFILES);
export const clearInteractiveImportTrackFiles = createAction(CLEAR_INTERACTIVE_IMPORT_TRACKFILES);
@@ -253,8 +235,6 @@ export const actionHandlers = handleThunks({
});
},
- [FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'),
-
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
});
@@ -336,14 +316,6 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { importMode: payload.importMode });
},
- [SET_INTERACTIVE_IMPORT_ALBUMS_SORT]: createSetClientSideCollectionSortReducer(albumsSection),
-
- [CLEAR_INTERACTIVE_IMPORT_ALBUMS]: (state) => {
- return updateSectionState(state, albumsSection, {
- ...defaultState.albums
- });
- },
-
[CLEAR_INTERACTIVE_IMPORT_TRACKFILES]: (state) => {
return updateSectionState(state, trackFilesSection, {
...defaultState.trackFiles
diff --git a/frontend/src/Store/Actions/parseActions.ts b/frontend/src/Store/Actions/parseActions.ts
new file mode 100644
index 000000000..d4b6e9bcb
--- /dev/null
+++ b/frontend/src/Store/Actions/parseActions.ts
@@ -0,0 +1,111 @@
+import { Dispatch } from 'redux';
+import { createAction } from 'redux-actions';
+import { batchActions } from 'redux-batched-actions';
+import AppState from 'App/State/AppState';
+import { createThunk, handleThunks } from 'Store/thunks';
+import createAjaxRequest from 'Utilities/createAjaxRequest';
+import { set, update } from './baseActions';
+import createHandleActions from './Creators/createHandleActions';
+import createClearReducer from './Creators/Reducers/createClearReducer';
+
+interface FetchPayload {
+ title: string;
+}
+
+//
+// Variables
+
+export const section = 'parse';
+let parseTimeout: number | null = null;
+let abortCurrentRequest: (() => void) | null = null;
+
+//
+// State
+
+export const defaultState = {
+ isFetching: false,
+ isPopulated: false,
+ error: null,
+ item: {},
+};
+
+//
+// Actions Types
+
+export const FETCH = 'parse/fetch';
+export const CLEAR = 'parse/clear';
+
+//
+// Action Creators
+
+export const fetch = createThunk(FETCH);
+export const clear = createAction(CLEAR);
+
+//
+// Action Handlers
+
+export const actionHandlers = handleThunks({
+ [FETCH]: function (
+ _getState: () => AppState,
+ payload: FetchPayload,
+ dispatch: Dispatch
+ ) {
+ if (parseTimeout) {
+ clearTimeout(parseTimeout);
+ }
+
+ parseTimeout = window.setTimeout(async () => {
+ dispatch(set({ section, isFetching: true }));
+
+ if (abortCurrentRequest) {
+ abortCurrentRequest();
+ }
+
+ const { request, abortRequest } = createAjaxRequest({
+ url: '/parse',
+ data: {
+ title: payload.title,
+ },
+ });
+
+ try {
+ const data = await request;
+
+ dispatch(
+ batchActions([
+ update({ section, data }),
+
+ set({
+ section,
+ isFetching: false,
+ isPopulated: true,
+ error: null,
+ }),
+ ])
+ );
+ } catch (error) {
+ dispatch(
+ set({
+ section,
+ isAdding: false,
+ isAdded: false,
+ addError: error,
+ })
+ );
+ }
+
+ abortCurrentRequest = abortRequest;
+ }, 300);
+ },
+});
+
+//
+// Reducers
+
+export const reducers = createHandleActions(
+ {
+ [CLEAR]: createClearReducer(section, defaultState),
+ },
+ defaultState,
+ section
+);
diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js
index 1c9b6f5ef..c4955c915 100644
--- a/frontend/src/Store/Actions/releaseActions.js
+++ b/frontend/src/Store/Actions/releaseActions.js
@@ -219,8 +219,9 @@ export const defaultState = {
};
export const persistState = [
- 'releases.selectedFilterKey',
+ 'releases.album.selectedFilterKey',
'releases.album.customFilters',
+ 'releases.artist.selectedFilterKey',
'releases.artist.customFilters'
];
diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js
index 35aa162d4..61d6f7752 100644
--- a/frontend/src/Store/Actions/wantedActions.js
+++ b/frontend/src/Store/Actions/wantedActions.js
@@ -52,6 +52,12 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
+ {
+ name: 'albums.lastSearchTime',
+ label: () => translate('LastSearched'),
+ isSortable: true,
+ isVisible: false
+ },
// {
// name: 'status',
// label: 'Status',
@@ -131,6 +137,12 @@ export const defaultState = {
// label: 'Status',
// isVisible: true
// },
+ {
+ name: 'albums.lastSearchTime',
+ label: () => translate('LastSearched'),
+ isSortable: true,
+ isVisible: false
+ },
{
name: 'actions',
columnLabel: () => translate('Actions'),
diff --git a/frontend/src/Store/Selectors/createAllArtistSelector.js b/frontend/src/Store/Selectors/createAllArtistSelector.ts
similarity index 71%
rename from frontend/src/Store/Selectors/createAllArtistSelector.js
rename to frontend/src/Store/Selectors/createAllArtistSelector.ts
index 38b1bcef1..6b6010429 100644
--- a/frontend/src/Store/Selectors/createAllArtistSelector.js
+++ b/frontend/src/Store/Selectors/createAllArtistSelector.ts
@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createAllArtistSelector() {
return createSelector(
- (state) => state.artist,
+ (state: AppState) => state.artist,
(artist) => {
return artist.items;
}
diff --git a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts
index 2ae54a10c..414a451f5 100644
--- a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts
+++ b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts
@@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
+import AlbumAppState from 'App/State/AlbumAppState';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import { createArtistSelectorForHook } from './createArtistSelector';
@@ -7,11 +8,11 @@ function createArtistAlbumsSelector(artistId: number) {
return createSelector(
(state: AppState) => state.albums,
createArtistSelectorForHook(artistId),
- (albums, artist = {} as Artist) => {
+ (albums: AlbumAppState, artist = {} as Artist) => {
const { isFetching, isPopulated, error, items } = albums;
const filteredAlbums = items.filter(
- (album) => album.artist.artistMetadataId === artist.artistMetadataId
+ (album) => album.artistId === artist.id
);
return {
diff --git a/frontend/src/Store/Selectors/createArtistCountSelector.js b/frontend/src/Store/Selectors/createArtistCountSelector.ts
similarity index 65%
rename from frontend/src/Store/Selectors/createArtistCountSelector.js
rename to frontend/src/Store/Selectors/createArtistCountSelector.ts
index 31e0a39fc..b432d64a7 100644
--- a/frontend/src/Store/Selectors/createArtistCountSelector.js
+++ b/frontend/src/Store/Selectors/createArtistCountSelector.ts
@@ -1,18 +1,19 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
import createAllArtistSelector from './createAllArtistSelector';
function createArtistCountSelector() {
return createSelector(
createAllArtistSelector(),
- (state) => state.artist.error,
- (state) => state.artist.isFetching,
- (state) => state.artist.isPopulated,
+ (state: AppState) => state.artist.error,
+ (state: AppState) => state.artist.isFetching,
+ (state: AppState) => state.artist.isPopulated,
(artists, error, isFetching, isPopulated) => {
return {
count: artists.length,
error,
isFetching,
- isPopulated
+ isPopulated,
};
}
);
diff --git a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts
index 0acbd3997..fa60d936d 100644
--- a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts
+++ b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts
@@ -1,13 +1,14 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
+import MetadataProfile from 'typings/MetadataProfile';
import { createArtistSelectorForHook } from './createArtistSelector';
function createArtistMetadataProfileSelector(artistId: number) {
return createSelector(
(state: AppState) => state.settings.metadataProfiles.items,
createArtistSelectorForHook(artistId),
- (metadataProfiles, artist = {} as Artist) => {
+ (metadataProfiles: MetadataProfile[], artist = {} as Artist) => {
return metadataProfiles.find((profile) => {
return profile.id === artist.metadataProfileId;
});
diff --git a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts
index 99325276f..67639919b 100644
--- a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts
+++ b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts
@@ -1,13 +1,14 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
+import QualityProfile from 'typings/QualityProfile';
import { createArtistSelectorForHook } from './createArtistSelector';
function createArtistQualityProfileSelector(artistId: number) {
return createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
createArtistSelectorForHook(artistId),
- (qualityProfiles, artist = {} as Artist) => {
+ (qualityProfiles: QualityProfile[], artist = {} as Artist) => {
return qualityProfiles.find(
(profile) => profile.id === artist.qualityProfileId
);
diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.js b/frontend/src/Store/Selectors/createCommandExecutingSelector.ts
similarity index 50%
rename from frontend/src/Store/Selectors/createCommandExecutingSelector.js
rename to frontend/src/Store/Selectors/createCommandExecutingSelector.ts
index 6037d5820..6a80e172b 100644
--- a/frontend/src/Store/Selectors/createCommandExecutingSelector.js
+++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.ts
@@ -2,13 +2,10 @@ import { createSelector } from 'reselect';
import { isCommandExecuting } from 'Utilities/Command';
import createCommandSelector from './createCommandSelector';
-function createCommandExecutingSelector(name, contraints = {}) {
- return createSelector(
- createCommandSelector(name, contraints),
- (command) => {
- return isCommandExecuting(command);
- }
- );
+function createCommandExecutingSelector(name: string, contraints = {}) {
+ return createSelector(createCommandSelector(name, contraints), (command) => {
+ return isCommandExecuting(command);
+ });
}
export default createCommandExecutingSelector;
diff --git a/frontend/src/Store/Selectors/createCommandSelector.js b/frontend/src/Store/Selectors/createCommandSelector.js
deleted file mode 100644
index 709dfebaf..000000000
--- a/frontend/src/Store/Selectors/createCommandSelector.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createSelector } from 'reselect';
-import { findCommand } from 'Utilities/Command';
-import createCommandsSelector from './createCommandsSelector';
-
-function createCommandSelector(name, contraints = {}) {
- return createSelector(
- createCommandsSelector(),
- (commands) => {
- return findCommand(commands, { name, ...contraints });
- }
- );
-}
-
-export default createCommandSelector;
diff --git a/frontend/src/Store/Selectors/createCommandSelector.ts b/frontend/src/Store/Selectors/createCommandSelector.ts
new file mode 100644
index 000000000..cced7b186
--- /dev/null
+++ b/frontend/src/Store/Selectors/createCommandSelector.ts
@@ -0,0 +1,11 @@
+import { createSelector } from 'reselect';
+import { findCommand } from 'Utilities/Command';
+import createCommandsSelector from './createCommandsSelector';
+
+function createCommandSelector(name: string, contraints = {}) {
+ return createSelector(createCommandsSelector(), (commands) => {
+ return findCommand(commands, { name, ...contraints });
+ });
+}
+
+export default createCommandSelector;
diff --git a/frontend/src/Store/Selectors/createCommandsSelector.js b/frontend/src/Store/Selectors/createCommandsSelector.ts
similarity index 71%
rename from frontend/src/Store/Selectors/createCommandsSelector.js
rename to frontend/src/Store/Selectors/createCommandsSelector.ts
index 7b9edffd9..2dd5d24a2 100644
--- a/frontend/src/Store/Selectors/createCommandsSelector.js
+++ b/frontend/src/Store/Selectors/createCommandsSelector.ts
@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createCommandsSelector() {
return createSelector(
- (state) => state.commands,
+ (state: AppState) => state.commands,
(commands) => {
return commands.items;
}
diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.js b/frontend/src/Store/Selectors/createDeepEqualSelector.js
deleted file mode 100644
index 85562f28b..000000000
--- a/frontend/src/Store/Selectors/createDeepEqualSelector.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import _ from 'lodash';
-import { createSelectorCreator, defaultMemoize } from 'reselect';
-
-const createDeepEqualSelector = createSelectorCreator(
- defaultMemoize,
- _.isEqual
-);
-
-export default createDeepEqualSelector;
diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.ts b/frontend/src/Store/Selectors/createDeepEqualSelector.ts
new file mode 100644
index 000000000..9d4a63d2e
--- /dev/null
+++ b/frontend/src/Store/Selectors/createDeepEqualSelector.ts
@@ -0,0 +1,6 @@
+import { isEqual } from 'lodash';
+import { createSelectorCreator, defaultMemoize } from 'reselect';
+
+const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
+
+export default createDeepEqualSelector;
diff --git a/frontend/src/Store/Selectors/createExecutingCommandsSelector.js b/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts
similarity index 78%
rename from frontend/src/Store/Selectors/createExecutingCommandsSelector.js
rename to frontend/src/Store/Selectors/createExecutingCommandsSelector.ts
index 266865a8a..dd16571fc 100644
--- a/frontend/src/Store/Selectors/createExecutingCommandsSelector.js
+++ b/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts
@@ -1,9 +1,10 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
import { isCommandExecuting } from 'Utilities/Command';
function createExecutingCommandsSelector() {
return createSelector(
- (state) => state.commands.items,
+ (state: AppState) => state.commands.items,
(commands) => {
return commands.filter((command) => isCommandExecuting(command));
}
diff --git a/frontend/src/Store/Selectors/createExistingArtistSelector.js b/frontend/src/Store/Selectors/createExistingArtistSelector.ts
similarity index 58%
rename from frontend/src/Store/Selectors/createExistingArtistSelector.js
rename to frontend/src/Store/Selectors/createExistingArtistSelector.ts
index 4811f2034..91b5bc4d6 100644
--- a/frontend/src/Store/Selectors/createExistingArtistSelector.js
+++ b/frontend/src/Store/Selectors/createExistingArtistSelector.ts
@@ -1,13 +1,15 @@
-import _ from 'lodash';
+import { some } from 'lodash';
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
import createAllArtistSelector from './createAllArtistSelector';
function createExistingArtistSelector() {
return createSelector(
- (state, { foreignArtistId }) => foreignArtistId,
+ (_: AppState, { foreignArtistId }: { foreignArtistId: string }) =>
+ foreignArtistId,
createAllArtistSelector(),
(foreignArtistId, artist) => {
- return _.some(artist, { foreignArtistId });
+ return some(artist, { foreignArtistId });
}
);
}
diff --git a/frontend/src/Store/Selectors/createMetadataProfileSelector.js b/frontend/src/Store/Selectors/createMetadataProfileSelector.js
deleted file mode 100644
index bdd0d0636..000000000
--- a/frontend/src/Store/Selectors/createMetadataProfileSelector.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { createSelector } from 'reselect';
-
-function createMetadataProfileSelector() {
- return createSelector(
- (state, { metadataProfileId }) => metadataProfileId,
- (state) => state.settings.metadataProfiles.items,
- (metadataProfileId, metadataProfiles) => {
- return metadataProfiles.find((profile) => {
- return profile.id === metadataProfileId;
- });
- }
- );
-}
-
-export default createMetadataProfileSelector;
diff --git a/frontend/src/Store/Selectors/createMetadataProfileSelector.ts b/frontend/src/Store/Selectors/createMetadataProfileSelector.ts
new file mode 100644
index 000000000..ae4c061db
--- /dev/null
+++ b/frontend/src/Store/Selectors/createMetadataProfileSelector.ts
@@ -0,0 +1,17 @@
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+
+function createMetadataProfileSelector() {
+ return createSelector(
+ (_: AppState, { metadataProfileId }: { metadataProfileId: number }) =>
+ metadataProfileId,
+ (state: AppState) => state.settings.metadataProfiles.items,
+ (metadataProfileId, metadataProfiles) => {
+ return metadataProfiles.find(
+ (profile) => profile.id === metadataProfileId
+ );
+ }
+ );
+}
+
+export default createMetadataProfileSelector;
diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js
deleted file mode 100644
index 84fefb83e..000000000
--- a/frontend/src/Store/Selectors/createProfileInUseSelector.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import _ from 'lodash';
-import { createSelector } from 'reselect';
-import createAllArtistSelector from './createAllArtistSelector';
-
-function createProfileInUseSelector(profileProp) {
- return createSelector(
- (state, { id }) => id,
- createAllArtistSelector(),
- (state) => state.settings.importLists.items,
- (id, artist, lists) => {
- if (!id) {
- return false;
- }
-
- if (_.some(artist, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) {
- return true;
- }
-
- return false;
- }
- );
-}
-
-export default createProfileInUseSelector;
diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.ts b/frontend/src/Store/Selectors/createProfileInUseSelector.ts
new file mode 100644
index 000000000..85f0c3211
--- /dev/null
+++ b/frontend/src/Store/Selectors/createProfileInUseSelector.ts
@@ -0,0 +1,25 @@
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+import Artist from 'Artist/Artist';
+import ImportList from 'typings/ImportList';
+import createAllArtistSelector from './createAllArtistSelector';
+
+function createProfileInUseSelector(profileProp: string) {
+ return createSelector(
+ (_: AppState, { id }: { id: number }) => id,
+ createAllArtistSelector(),
+ (state: AppState) => state.settings.importLists.items,
+ (id, artists, lists) => {
+ if (!id) {
+ return false;
+ }
+
+ return (
+ artists.some((a) => a[profileProp as keyof Artist] === id) ||
+ lists.some((list) => list[profileProp as keyof ImportList] === id)
+ );
+ }
+ );
+}
+
+export default createProfileInUseSelector;
diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.js b/frontend/src/Store/Selectors/createQualityProfileSelector.js
deleted file mode 100644
index 611dfc903..000000000
--- a/frontend/src/Store/Selectors/createQualityProfileSelector.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { createSelector } from 'reselect';
-
-export function createQualityProfileSelectorForHook(qualityProfileId) {
- return createSelector(
- (state) => state.settings.qualityProfiles.items,
- (qualityProfiles) => {
- return qualityProfiles.find((profile) => {
- return profile.id === qualityProfileId;
- });
- }
- );
-}
-
-function createQualityProfileSelector() {
- return createSelector(
- (state, { qualityProfileId }) => qualityProfileId,
- (state) => state.settings.qualityProfiles.items,
- (qualityProfileId, qualityProfiles) => {
- return qualityProfiles.find((profile) => {
- return profile.id === qualityProfileId;
- });
- }
- );
-}
-
-export default createQualityProfileSelector;
diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.ts b/frontend/src/Store/Selectors/createQualityProfileSelector.ts
new file mode 100644
index 000000000..b913e0c46
--- /dev/null
+++ b/frontend/src/Store/Selectors/createQualityProfileSelector.ts
@@ -0,0 +1,24 @@
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+
+export function createQualityProfileSelectorForHook(qualityProfileId: number) {
+ return createSelector(
+ (state: AppState) => state.settings.qualityProfiles.items,
+ (qualityProfiles) => {
+ return qualityProfiles.find((profile) => profile.id === qualityProfileId);
+ }
+ );
+}
+
+function createQualityProfileSelector() {
+ return createSelector(
+ (_: AppState, { qualityProfileId }: { qualityProfileId: number }) =>
+ qualityProfileId,
+ (state: AppState) => state.settings.qualityProfiles.items,
+ (qualityProfileId, qualityProfiles) => {
+ return qualityProfiles.find((profile) => profile.id === qualityProfileId);
+ }
+ );
+}
+
+export default createQualityProfileSelector;
diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.js b/frontend/src/Store/Selectors/createQueueItemSelector.ts
similarity index 52%
rename from frontend/src/Store/Selectors/createQueueItemSelector.js
rename to frontend/src/Store/Selectors/createQueueItemSelector.ts
index c85d7ed82..54951a724 100644
--- a/frontend/src/Store/Selectors/createQueueItemSelector.js
+++ b/frontend/src/Store/Selectors/createQueueItemSelector.ts
@@ -1,21 +1,16 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createQueueItemSelector() {
return createSelector(
- (state, { albumId }) => albumId,
- (state) => state.queue.details.items,
+ (_: AppState, { albumId }: { albumId: number }) => albumId,
+ (state: AppState) => state.queue.details.items,
(albumId, details) => {
if (!albumId || !details) {
return null;
}
- return details.find((item) => {
- if (item.album) {
- return item.album.id === albumId;
- }
-
- return false;
- });
+ return details.find((item) => item.albumId === albumId);
}
);
}
diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts
index a016d7665..432f9056d 100644
--- a/frontend/src/Store/Selectors/createRootFoldersSelector.ts
+++ b/frontend/src/Store/Selectors/createRootFoldersSelector.ts
@@ -1,11 +1,15 @@
import { createSelector } from 'reselect';
import { RootFolderAppState } from 'App/State/SettingsAppState';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
-import sortByName from 'Utilities/Array/sortByName';
+import RootFolder from 'typings/RootFolder';
+import sortByProp from 'Utilities/Array/sortByProp';
export default function createRootFoldersSelector() {
return createSelector(
- createSortedSectionSelector('settings.rootFolders', sortByName),
+ createSortedSectionSelector
(
+ 'settings.rootFolders',
+ sortByProp('name')
+ ),
(rootFolders: RootFolderAppState) => rootFolders
);
}
diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.js b/frontend/src/Store/Selectors/createSortedSectionSelector.ts
similarity index 68%
rename from frontend/src/Store/Selectors/createSortedSectionSelector.js
rename to frontend/src/Store/Selectors/createSortedSectionSelector.ts
index 331d890c9..abee01f75 100644
--- a/frontend/src/Store/Selectors/createSortedSectionSelector.js
+++ b/frontend/src/Store/Selectors/createSortedSectionSelector.ts
@@ -1,14 +1,18 @@
import { createSelector } from 'reselect';
import getSectionState from 'Utilities/State/getSectionState';
-function createSortedSectionSelector(section, comparer) {
+function createSortedSectionSelector(
+ section: string,
+ comparer: (a: T, b: T) => number
+) {
return createSelector(
(state) => state,
(state) => {
const sectionState = getSectionState(state, section, true);
+
return {
...sectionState,
- items: [...sectionState.items].sort(comparer)
+ items: [...sectionState.items].sort(comparer),
};
}
);
diff --git a/frontend/src/Store/Selectors/createSystemStatusSelector.js b/frontend/src/Store/Selectors/createSystemStatusSelector.ts
similarity index 70%
rename from frontend/src/Store/Selectors/createSystemStatusSelector.js
rename to frontend/src/Store/Selectors/createSystemStatusSelector.ts
index df586bbb9..f5e276069 100644
--- a/frontend/src/Store/Selectors/createSystemStatusSelector.js
+++ b/frontend/src/Store/Selectors/createSystemStatusSelector.ts
@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createSystemStatusSelector() {
return createSelector(
- (state) => state.system.status,
+ (state: AppState) => state.system.status,
(status) => {
return status.item;
}
diff --git a/frontend/src/Store/Selectors/createTagDetailsSelector.js b/frontend/src/Store/Selectors/createTagDetailsSelector.ts
similarity index 62%
rename from frontend/src/Store/Selectors/createTagDetailsSelector.js
rename to frontend/src/Store/Selectors/createTagDetailsSelector.ts
index dd178944c..2a271cafe 100644
--- a/frontend/src/Store/Selectors/createTagDetailsSelector.js
+++ b/frontend/src/Store/Selectors/createTagDetailsSelector.ts
@@ -1,9 +1,10 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createTagDetailsSelector() {
return createSelector(
- (state, { id }) => id,
- (state) => state.tags.details.items,
+ (_: AppState, { id }: { id: number }) => id,
+ (state: AppState) => state.tags.details.items,
(id, tagDetails) => {
return tagDetails.find((t) => t.id === id);
}
diff --git a/frontend/src/Store/Selectors/createTagsSelector.js b/frontend/src/Store/Selectors/createTagsSelector.ts
similarity index 68%
rename from frontend/src/Store/Selectors/createTagsSelector.js
rename to frontend/src/Store/Selectors/createTagsSelector.ts
index fbfd91cdb..f653ff6e3 100644
--- a/frontend/src/Store/Selectors/createTagsSelector.js
+++ b/frontend/src/Store/Selectors/createTagsSelector.ts
@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createTagsSelector() {
return createSelector(
- (state) => state.tags.items,
+ (state: AppState) => state.tags.items,
(tags) => {
return tags;
}
diff --git a/frontend/src/Store/Selectors/createTrackFileSelector.js b/frontend/src/Store/Selectors/createTrackFileSelector.ts
similarity index 66%
rename from frontend/src/Store/Selectors/createTrackFileSelector.js
rename to frontend/src/Store/Selectors/createTrackFileSelector.ts
index bcfc5cb0b..a162df1fa 100644
--- a/frontend/src/Store/Selectors/createTrackFileSelector.js
+++ b/frontend/src/Store/Selectors/createTrackFileSelector.ts
@@ -1,9 +1,10 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createTrackFileSelector() {
return createSelector(
- (state, { trackFileId }) => trackFileId,
- (state) => state.trackFiles,
+ (_: AppState, { trackFileId }: { trackFileId: number }) => trackFileId,
+ (state: AppState) => state.trackFiles,
(trackFileId, trackFiles) => {
if (!trackFileId) {
return;
diff --git a/frontend/src/Store/Selectors/createUISettingsSelector.js b/frontend/src/Store/Selectors/createUISettingsSelector.ts
similarity index 69%
rename from frontend/src/Store/Selectors/createUISettingsSelector.js
rename to frontend/src/Store/Selectors/createUISettingsSelector.ts
index b256d0e98..ff539679b 100644
--- a/frontend/src/Store/Selectors/createUISettingsSelector.js
+++ b/frontend/src/Store/Selectors/createUISettingsSelector.ts
@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
function createUISettingsSelector() {
return createSelector(
- (state) => state.settings.ui,
+ (state: AppState) => state.settings.ui,
(ui) => {
return ui.item;
}
diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js
index d93c5dd8c..4dec39164 100644
--- a/frontend/src/Styles/Themes/index.js
+++ b/frontend/src/Styles/Themes/index.js
@@ -2,7 +2,7 @@ import * as dark from './dark';
import * as light from './light';
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
-const auto = defaultDark ? { ...dark } : { ...light };
+const auto = defaultDark ? dark : light;
export default {
auto,
diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js
index 3b0077c5a..def48f28e 100644
--- a/frontend/src/Styles/Variables/fonts.js
+++ b/frontend/src/Styles/Variables/fonts.js
@@ -2,7 +2,6 @@ module.exports = {
// Families
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
- passwordFamily: 'text-security-disc',
// Sizes
extraSmallFontSize: '11px',
diff --git a/frontend/src/System/Logs/Files/LogFiles.js b/frontend/src/System/Logs/Files/LogFiles.js
index 83736c617..5339a8590 100644
--- a/frontend/src/System/Logs/Files/LogFiles.js
+++ b/frontend/src/System/Logs/Files/LogFiles.js
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
-import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -77,15 +77,16 @@ class LogFiles extends Component {
- Log files are located in: {location}
+ {translate('LogFilesLocation', {
+ location
+ })}
- {
- currentLogView === 'Log Files' &&
-
- The log level defaults to 'Info' and can be changed in General Settings
-
- }
+ {currentLogView === 'Log Files' ? (
+
+
+
+ ) : null}
{
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
index 9fc4f9e21..41a307d5f 100644
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
@@ -3,9 +3,26 @@ import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import createMultiArtistsSelector from 'Store/Selectors/createMultiArtistsSelector';
+import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRowNameCell.css';
+function formatTitles(titles: string[]) {
+ if (!titles) {
+ return null;
+ }
+
+ if (titles.length > 11) {
+ return (
+
+ {titles.slice(0, 10).join(', ')}, {titles.length - 10} more
+
+ );
+ }
+
+ return {titles.join(', ')};
+}
+
export interface QueuedTaskRowNameCellProps {
commandName: string;
body: CommandBody;
@@ -23,16 +40,14 @@ export default function QueuedTaskRowNameCell(
}
const artists = useSelector(createMultiArtistsSelector(movieIds));
- const sortedArtists = artists.sort((a, b) =>
- a.sortName.localeCompare(b.sortName)
- );
+ const sortedArtists = artists.sort(sortByProp('sortName'));
return (
{commandName}
{sortedArtists.length ? (
- - {sortedArtists.map((a) => a.artistName).join(', ')}
+ - {formatTitles(sortedArtists.map((a) => a.artistName))}
) : null}
diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js
deleted file mode 100644
index 3588069a0..000000000
--- a/frontend/src/System/Updates/UpdateChanges.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
-import styles from './UpdateChanges.css';
-
-class UpdateChanges extends Component {
-
- //
- // Render
-
- render() {
- const {
- title,
- changes
- } = this.props;
-
- if (changes.length === 0) {
- return null;
- }
-
- return (
-
-
{title}
-
- {
- changes.map((change, index) => {
- return (
- -
-
-
- );
- })
- }
-
-
- );
- }
-
-}
-
-UpdateChanges.propTypes = {
- title: PropTypes.string.isRequired,
- changes: PropTypes.arrayOf(PropTypes.string)
-};
-
-export default UpdateChanges;
diff --git a/frontend/src/System/Updates/UpdateChanges.tsx b/frontend/src/System/Updates/UpdateChanges.tsx
new file mode 100644
index 000000000..3e5ba1c9b
--- /dev/null
+++ b/frontend/src/System/Updates/UpdateChanges.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
+import styles from './UpdateChanges.css';
+
+interface UpdateChangesProps {
+ title: string;
+ changes: string[];
+}
+
+function UpdateChanges(props: UpdateChangesProps) {
+ const { title, changes } = props;
+
+ if (changes.length === 0) {
+ return null;
+ }
+
+ const uniqueChanges = [...new Set(changes)];
+
+ return (
+
+
{title}
+
+ {uniqueChanges.map((change, index) => {
+ const checkChange = change.replace(
+ /#\d{4,5}\b/g,
+ (match) =>
+ `[${match}](https://github.com/Lidarr/Lidarr/issues/${match.substring(
+ 1
+ )})`
+ );
+
+ return (
+ -
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default UpdateChanges;
diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js
deleted file mode 100644
index 528441cbe..000000000
--- a/frontend/src/System/Updates/Updates.js
+++ /dev/null
@@ -1,249 +0,0 @@
-import _ from 'lodash';
-import PropTypes from 'prop-types';
-import React, { Component, Fragment } from 'react';
-import Alert from 'Components/Alert';
-import Icon from 'Components/Icon';
-import Label from 'Components/Label';
-import SpinnerButton from 'Components/Link/SpinnerButton';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
-import PageContent from 'Components/Page/PageContent';
-import PageContentBody from 'Components/Page/PageContentBody';
-import { icons, kinds } from 'Helpers/Props';
-import formatDate from 'Utilities/Date/formatDate';
-import formatDateTime from 'Utilities/Date/formatDateTime';
-import translate from 'Utilities/String/translate';
-import UpdateChanges from './UpdateChanges';
-import styles from './Updates.css';
-
-class Updates extends Component {
-
- //
- // Render
-
- render() {
- const {
- currentVersion,
- isFetching,
- isPopulated,
- updatesError,
- generalSettingsError,
- items,
- isInstallingUpdate,
- updateMechanism,
- updateMechanismMessage,
- shortDateFormat,
- longDateFormat,
- timeFormat,
- onInstallLatestPress
- } = this.props;
-
- const hasError = !!(updatesError || generalSettingsError);
- const hasUpdates = isPopulated && !hasError && items.length > 0;
- const noUpdates = isPopulated && !hasError && !items.length;
- const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
- const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
-
- const externalUpdaterPrefix = 'Unable to update Lidarr directly,';
- const externalUpdaterMessages = {
- external: 'Lidarr is configured to use an external update mechanism',
- apt: 'use apt to install the update',
- docker: 'update the docker container to receive the update'
- };
-
- return (
-
-
- {
- !isPopulated && !hasError &&
-
- }
-
- {
- noUpdates &&
-
- {translate('NoUpdatesAreAvailable')}
-
- }
-
- {
- hasUpdateToInstall &&
-
- {
- updateMechanism === 'builtIn' || updateMechanism === 'script' ?
-
- Install Latest
- :
-
-
-
-
-
- {externalUpdaterPrefix}
-
-
- }
-
- {
- isFetching &&
-
- }
-
- }
-
- {
- noUpdateToInstall &&
-
-
-
- The latest version of Lidarr is already installed
-
-
- {
- isFetching &&
-
- }
-
- }
-
- {
- hasUpdates &&
-
- {
- items.map((update) => {
- const hasChanges = !!update.changes;
-
- return (
-
-
-
{update.version}
-
—
-
- {formatDate(update.releaseDate, shortDateFormat)}
-
-
- {
- update.branch === 'master' ?
- null :
-
- }
-
- {
- update.version === currentVersion ?
-
:
- null
- }
-
- {
- update.version !== currentVersion && update.installedOn ?
-
:
- null
- }
-
-
- {
- !hasChanges &&
-
- {translate('MaintenanceRelease')}
-
- }
-
- {
- hasChanges &&
-
-
-
-
-
- }
-
- );
- })
- }
-
- }
-
- {
- !!updatesError &&
-
- Failed to fetch updates
-
- }
-
- {
- !!generalSettingsError &&
-
- Failed to update settings
-
- }
-
-
- );
- }
-
-}
-
-Updates.propTypes = {
- currentVersion: PropTypes.string.isRequired,
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- updatesError: PropTypes.object,
- generalSettingsError: PropTypes.object,
- items: PropTypes.array.isRequired,
- isInstallingUpdate: PropTypes.bool.isRequired,
- updateMechanism: PropTypes.string,
- updateMechanismMessage: PropTypes.string,
- shortDateFormat: PropTypes.string.isRequired,
- longDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired,
- onInstallLatestPress: PropTypes.func.isRequired
-};
-
-export default Updates;
diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx
new file mode 100644
index 000000000..300ab1f99
--- /dev/null
+++ b/frontend/src/System/Updates/Updates.tsx
@@ -0,0 +1,303 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
+import * as commandNames from 'Commands/commandNames';
+import Alert from 'Components/Alert';
+import Icon from 'Components/Icon';
+import Label from 'Components/Label';
+import SpinnerButton from 'Components/Link/SpinnerButton';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
+import PageContent from 'Components/Page/PageContent';
+import PageContentBody from 'Components/Page/PageContentBody';
+import { icons, kinds } from 'Helpers/Props';
+import { executeCommand } from 'Store/Actions/commandActions';
+import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
+import { fetchUpdates } from 'Store/Actions/systemActions';
+import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
+import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import { UpdateMechanism } from 'typings/Settings/General';
+import formatDate from 'Utilities/Date/formatDate';
+import formatDateTime from 'Utilities/Date/formatDateTime';
+import translate from 'Utilities/String/translate';
+import UpdateChanges from './UpdateChanges';
+import styles from './Updates.css';
+
+const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
+
+function createUpdatesSelector() {
+ return createSelector(
+ (state: AppState) => state.system.updates,
+ (state: AppState) => state.settings.general,
+ (updates, generalSettings) => {
+ const { error: updatesError, items } = updates;
+
+ const isFetching = updates.isFetching || generalSettings.isFetching;
+ const isPopulated = updates.isPopulated && generalSettings.isPopulated;
+
+ return {
+ isFetching,
+ isPopulated,
+ updatesError,
+ generalSettingsError: generalSettings.error,
+ items,
+ updateMechanism: generalSettings.item.updateMechanism,
+ };
+ }
+ );
+}
+
+function Updates() {
+ const currentVersion = useSelector((state: AppState) => state.app.version);
+ const { packageUpdateMechanismMessage } = useSelector(
+ createSystemStatusSelector()
+ );
+ const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
+ createUISettingsSelector()
+ );
+ const isInstallingUpdate = useSelector(
+ createCommandExecutingSelector(commandNames.APPLICATION_UPDATE)
+ );
+
+ const {
+ isFetching,
+ isPopulated,
+ updatesError,
+ generalSettingsError,
+ items,
+ updateMechanism,
+ } = useSelector(createUpdatesSelector());
+
+ const dispatch = useDispatch();
+ const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false);
+ const hasError = !!(updatesError || generalSettingsError);
+ const hasUpdates = isPopulated && !hasError && items.length > 0;
+ const noUpdates = isPopulated && !hasError && !items.length;
+
+ const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError');
+ const externalUpdaterMessages: Partial> = {
+ external: translate('ExternalUpdater'),
+ apt: translate('AptUpdater'),
+ docker: translate('DockerUpdater'),
+ };
+
+ const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => {
+ const majorVersion = parseInt(
+ currentVersion.match(VERSION_REGEX)?.[0] ?? '0'
+ );
+
+ const latestVersion = items[0]?.version;
+ const latestMajorVersion = parseInt(
+ latestVersion?.match(VERSION_REGEX)?.[0] ?? '0'
+ );
+
+ return {
+ isMajorUpdate: latestMajorVersion > majorVersion,
+ hasUpdateToInstall: items.some(
+ (update) => update.installable && update.latest
+ ),
+ };
+ }, [currentVersion, items]);
+
+ const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
+
+ const handleInstallLatestPress = useCallback(() => {
+ if (isMajorUpdate) {
+ setIsMajorUpdateModalOpen(true);
+ } else {
+ dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE }));
+ }
+ }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]);
+
+ const handleInstallLatestMajorVersionPress = useCallback(() => {
+ setIsMajorUpdateModalOpen(false);
+
+ dispatch(
+ executeCommand({
+ name: commandNames.APPLICATION_UPDATE,
+ installMajorUpdate: true,
+ })
+ );
+ }, [setIsMajorUpdateModalOpen, dispatch]);
+
+ const handleCancelMajorVersionPress = useCallback(() => {
+ setIsMajorUpdateModalOpen(false);
+ }, [setIsMajorUpdateModalOpen]);
+
+ useEffect(() => {
+ dispatch(fetchUpdates());
+ dispatch(fetchGeneralSettings());
+ }, [dispatch]);
+
+ return (
+
+
+ {isPopulated || hasError ? null : }
+
+ {noUpdates ? (
+ {translate('NoUpdatesAreAvailable')}
+ ) : null}
+
+ {hasUpdateToInstall ? (
+
+ {updateMechanism === 'builtIn' || updateMechanism === 'script' ? (
+
+ {translate('InstallLatest')}
+
+ ) : (
+ <>
+
+
+
+ {externalUpdaterPrefix}{' '}
+
+
+ >
+ )}
+
+ {isFetching ? (
+
+ ) : null}
+
+ ) : null}
+
+ {noUpdateToInstall && (
+
+
+
{translate('OnLatestVersion')}
+
+ {isFetching && (
+
+ )}
+
+ )}
+
+ {hasUpdates && (
+
+ {items.map((update) => {
+ return (
+
+
+
{update.version}
+
—
+
+ {formatDate(update.releaseDate, shortDateFormat)}
+
+
+ {update.branch === 'master' ? null : (
+
+ )}
+
+ {update.version === currentVersion ? (
+
+ ) : null}
+
+ {update.version !== currentVersion && update.installedOn ? (
+
+ ) : null}
+
+
+ {update.changes ? (
+
+
+
+
+
+ ) : (
+
{translate('MaintenanceRelease')}
+ )}
+
+ );
+ })}
+
+ )}
+
+ {updatesError ? (
+
+ {translate('FailedToFetchUpdates')}
+
+ ) : null}
+
+ {generalSettingsError ? (
+
+ {translate('FailedToFetchSettings')}
+
+ ) : null}
+
+
+ {translate('InstallMajorVersionUpdateMessage')}
+
+
+
+
+ }
+ confirmLabel={translate('Install')}
+ onConfirm={handleInstallLatestMajorVersionPress}
+ onCancel={handleCancelMajorVersionPress}
+ />
+
+
+ );
+}
+
+export default Updates;
diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js
deleted file mode 100644
index 77d75dbda..000000000
--- a/frontend/src/System/Updates/UpdatesConnector.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import * as commandNames from 'Commands/commandNames';
-import { executeCommand } from 'Store/Actions/commandActions';
-import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
-import { fetchUpdates } from 'Store/Actions/systemActions';
-import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
-import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import Updates from './Updates';
-
-function createMapStateToProps() {
- return createSelector(
- (state) => state.app.version,
- createSystemStatusSelector(),
- (state) => state.system.updates,
- (state) => state.settings.general,
- createUISettingsSelector(),
- createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
- (
- currentVersion,
- status,
- updates,
- generalSettings,
- uiSettings,
- isInstallingUpdate
- ) => {
- const {
- error: updatesError,
- items
- } = updates;
-
- const isFetching = updates.isFetching || generalSettings.isFetching;
- const isPopulated = updates.isPopulated && generalSettings.isPopulated;
-
- return {
- currentVersion,
- isFetching,
- isPopulated,
- updatesError,
- generalSettingsError: generalSettings.error,
- items,
- isInstallingUpdate,
- updateMechanism: generalSettings.item.updateMechanism,
- updateMechanismMessage: status.packageUpdateMechanismMessage,
- shortDateFormat: uiSettings.shortDateFormat,
- longDateFormat: uiSettings.longDateFormat,
- timeFormat: uiSettings.timeFormat
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- dispatchFetchUpdates: fetchUpdates,
- dispatchFetchGeneralSettings: fetchGeneralSettings,
- dispatchExecuteCommand: executeCommand
-};
-
-class UpdatesConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- this.props.dispatchFetchUpdates();
- this.props.dispatchFetchGeneralSettings();
- }
-
- //
- // Listeners
-
- onInstallLatestPress = () => {
- this.props.dispatchExecuteCommand({ name: commandNames.APPLICATION_UPDATE });
- };
-
- //
- // Render
-
- render() {
- return (
-