{count} {labelPlural.toLowerCase()}
diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js
index 15f31d3c5..770dc4720 100644
--- a/frontend/src/Settings/Tags/TagsConnector.js
+++ b/frontend/src/Settings/Tags/TagsConnector.js
@@ -3,14 +3,12 @@ import React, { Component } from 'react';
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 { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags';
function createMapStateToProps() {
return createSelector(
- createSortedSectionSelector('tags', sortByProp('label')),
+ (state) => state.tags,
(tags) => {
const isFetching = tags.isFetching || tags.details.isFetching;
const error = tags.error || tags.details.error;
@@ -27,7 +25,6 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
- dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchImportLists: fetchImportLists,
@@ -44,7 +41,6 @@ class MetadatasConnector extends Component {
componentDidMount() {
const {
- dispatchFetchTags,
dispatchFetchTagDetails,
dispatchFetchDelayProfiles,
dispatchFetchImportLists,
@@ -54,7 +50,6 @@ class MetadatasConnector extends Component {
dispatchFetchDownloadClients
} = this.props;
- dispatchFetchTags();
dispatchFetchTagDetails();
dispatchFetchDelayProfiles();
dispatchFetchImportLists();
@@ -77,7 +72,6 @@ class MetadatasConnector extends Component {
}
MetadatasConnector.propTypes = {
- dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js
index cc27829df..c8135e17f 100644
--- a/frontend/src/Settings/UI/UISettings.js
+++ b/frontend/src/Settings/UI/UISettings.js
@@ -22,19 +22,19 @@ export const firstDayOfWeekOptions = [
];
export const weekColumnOptions = [
- { key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
- { key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
- { key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
- { key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }
+ { key: 'ddd M/D', value: 'Tue 3/25' },
+ { key: 'ddd MM/DD', value: 'Tue 03/25' },
+ { key: 'ddd D/M', value: 'Tue 25/3' },
+ { key: 'ddd DD/MM', value: 'Tue 25/03' }
];
const shortDateFormatOptions = [
- { key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
- { key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
- { key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
- { key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' },
- { key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' },
- { key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }
+ { key: 'MMM D YYYY', value: 'Mar 25 2014' },
+ { key: 'DD MMM YYYY', value: '25 Mar 2014' },
+ { key: 'MM/D/YYYY', value: '03/25/2014' },
+ { key: 'MM/DD/YYYY', value: '03/25/2014' },
+ { key: 'DD/MM/YYYY', value: '25/03/2014' },
+ { key: 'YYYY-MM-DD', value: '2014-03-25' }
];
const longDateFormatOptions = [
diff --git a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js
index 3de794bdf..dfe29ace8 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 e35157dbd..ca26883fb 100644
--- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
+++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
@@ -1,11 +1,8 @@
-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) {
@@ -20,25 +17,10 @@ function createTestProviderHandler(section, url) {
return function(getState, payload, dispatch) {
dispatch(set({ section, isTesting: true }));
- 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 testData = getProviderState(payload, getState, section);
const ajaxOptions = {
- url: `${url}/test?${$.param(params, true)}`,
+ url: `${url}/test`,
method: 'POST',
contentType: 'application/json',
dataType: 'json',
@@ -50,8 +32,6 @@ 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 3b8a209f9..4a175abea 100644
--- a/frontend/src/Store/Actions/Settings/customFormats.js
+++ b/frontend/src/Store/Actions/Settings/customFormats.js
@@ -1,12 +1,7 @@
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';
@@ -26,9 +21,6 @@ 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
@@ -36,9 +28,6 @@ export const SET_MANAGE_CUSTOM_FORMATS_SORT = 'settings/downloadClients/setManag
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 {
@@ -58,30 +47,20 @@ export default {
// State
defaultState: {
- isFetching: false,
- isPopulated: false,
- error: null,
- isSaving: false,
- saveError: null,
- isDeleting: false,
- deleteError: null,
- items: [],
- pendingChanges: {},
-
isSchemaFetching: false,
isSchemaPopulated: false,
- schemaError: null,
+ isFetching: false,
+ isPopulated: false,
schema: {
includeCustomFormatWhenRenaming: false
},
-
- sortKey: 'name',
- sortDirection: sortDirections.ASCENDING,
- sortPredicates: {
- name: ({ name }) => {
- return name.toLocaleLowerCase();
- }
- }
+ error: null,
+ isDeleting: false,
+ deleteError: null,
+ isSaving: false,
+ saveError: null,
+ items: [],
+ pendingChanges: {}
},
//
@@ -103,10 +82,7 @@ export default {
}));
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
- },
-
- [BULK_EDIT_CUSTOM_FORMATS]: createBulkEditItemHandler(section, '/customformat/bulk'),
- [BULK_DELETE_CUSTOM_FORMATS]: createBulkRemoveItemHandler(section, '/customformat/bulk')
+ }
},
//
@@ -126,9 +102,7 @@ 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 1113e7daf..aee945ef5 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: ({ name }) => {
- return name.toLocaleLowerCase();
+ name: function(item) {
+ return item.name.toLowerCase();
}
}
},
diff --git a/frontend/src/Store/Actions/Settings/indexerFlags.js b/frontend/src/Store/Actions/Settings/indexerFlags.js
deleted file mode 100644
index a53fe1c61..000000000
--- a/frontend/src/Store/Actions/Settings/indexerFlags.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
-import { createThunk } from 'Store/thunks';
-
-//
-// Variables
-
-const section = 'settings.indexerFlags';
-
-//
-// Actions Types
-
-export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
-
-//
-// Action Creators
-
-export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
-
-//
-// Details
-
-export default {
-
- //
- // State
-
- defaultState: {
- isFetching: false,
- isPopulated: false,
- error: null,
- items: []
- },
-
- //
- // Action Handlers
-
- actionHandlers: {
- [FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
- },
-
- //
- // Reducers
-
- reducers: {
-
- }
-
-};
diff --git a/frontend/src/Store/Actions/Settings/indexers.js b/frontend/src/Store/Actions/Settings/indexers.js
index 511a2e475..1e9aded2f 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: ({ name }) => {
- return name.toLocaleLowerCase();
+ name: function(item) {
+ return item.name.toLowerCase();
}
}
},
diff --git a/frontend/src/Store/Actions/albumSelectionActions.js b/frontend/src/Store/Actions/albumSelectionActions.js
deleted file mode 100644
index f19f5b691..000000000
--- a/frontend/src/Store/Actions/albumSelectionActions.js
+++ /dev/null
@@ -1,86 +0,0 @@
-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 736502460..72cb20142 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 sortByProp from 'Utilities/Array/sortByProp';
+import sortByName from 'Utilities/Array/sortByName';
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: true,
+ isSortable: false,
isVisible: false
},
{
@@ -334,7 +334,7 @@ export const defaultState = {
return acc;
}, []);
- return tagList.sort(sortByProp('name'));
+ return tagList.sort(sortByName);
}
},
{
diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js
index e13ff4672..d473f1368 100644
--- a/frontend/src/Store/Actions/calendarActions.js
+++ b/frontend/src/Store/Actions/calendarActions.js
@@ -4,10 +4,9 @@ import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import * as calendarViews from 'Calendar/calendarViews';
import * as commandNames from 'Commands/commandNames';
-import { filterBuilderTypes, filterBuilderValueTypes, filterTypes } from 'Helpers/Props';
+import { filterTypes } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
-import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
import { set, update } from './baseActions';
import { executeCommandHelper } from './commandActions';
@@ -55,8 +54,8 @@ export const defaultState = {
label: () => translate('All'),
filters: [
{
- key: 'unmonitored',
- value: [true],
+ key: 'monitored',
+ value: false,
type: filterTypes.EQUAL
}
]
@@ -66,35 +65,19 @@ export const defaultState = {
label: () => translate('MonitoredOnly'),
filters: [
{
- key: 'unmonitored',
- value: [false],
+ key: 'monitored',
+ value: true,
type: filterTypes.EQUAL
}
]
}
- ],
-
- filterBuilderProps: [
- {
- name: 'unmonitored',
- label: () => translate('IncludeUnmonitored'),
- type: filterBuilderTypes.EQUAL,
- valueType: filterBuilderValueTypes.BOOL
- },
- {
- name: 'tags',
- label: () => translate('Tags'),
- type: filterBuilderTypes.CONTAINS,
- valueType: filterBuilderValueTypes.TAG
- }
]
};
export const persistState = [
'calendar.view',
'calendar.selectedFilterKey',
- 'calendar.options',
- 'calendar.customFilters'
+ 'calendar.options'
];
//
@@ -206,10 +189,6 @@ function isRangePopulated(start, end, state) {
return false;
}
-function getCustomFilters(state, type) {
- return state.customFilters.items.filter((customFilter) => customFilter.type === type);
-}
-
//
// Action Creators
@@ -231,8 +210,7 @@ export const actionHandlers = handleThunks({
[FETCH_CALENDAR]: function(getState, payload, dispatch) {
const state = getState();
const calendar = state.calendar;
- const customFilters = getCustomFilters(state, section);
- const selectedFilters = findSelectedFilters(calendar.selectedFilterKey, calendar.filters, customFilters);
+ const unmonitored = calendar.selectedFilterKey === 'all';
const {
time = calendar.time,
@@ -259,26 +237,13 @@ export const actionHandlers = handleThunks({
dispatch(set(attrs));
- const requestParams = {
- start,
- end
- };
-
- selectedFilters.forEach((selectedFilter) => {
- if (selectedFilter.key === 'unmonitored') {
- requestParams.unmonitored = selectedFilter.value.includes(true);
- }
-
- if (selectedFilter.key === 'tags') {
- requestParams.tags = selectedFilter.value.join(',');
- }
- });
-
- requestParams.unmonitored = requestParams.unmonitored ?? false;
-
const promise = createAjaxRequest({
url: '/calendar',
- data: requestParams
+ data: {
+ unmonitored,
+ start,
+ end
+ }
}).request;
promise.done((data) => {
diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js
index 9d16d29c4..225698229 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('ImportCompleteFailed'),
+ label: () => translate('ImportFailed'),
filters: [
{
key: 'eventType',
diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js
index 85fda482b..95b02d089 100644
--- a/frontend/src/Store/Actions/index.js
+++ b/frontend/src/Store/Actions/index.js
@@ -1,6 +1,5 @@
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';
@@ -14,7 +13,6 @@ 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';
@@ -30,28 +28,26 @@ 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 a250292c5..e0e295568 100644
--- a/frontend/src/Store/Actions/interactiveImportActions.js
+++ b/frontend/src/Store/Actions/interactiveImportActions.js
@@ -16,6 +16,7 @@ import createSetClientSideCollectionSortReducer from './Creators/Reducers/create
export const section = 'interactiveImport';
+const albumsSection = `${section}.albums`;
const trackFilesSection = `${section}.trackFiles`;
let abortCurrentFetchRequest = null;
let abortCurrentRequest = null;
@@ -57,6 +58,15 @@ export const defaultState = {
}
},
+ albums: {
+ isFetching: false,
+ isPopulated: false,
+ error: null,
+ sortKey: 'albumTitle',
+ sortDirection: sortDirections.ASCENDING,
+ items: []
+ },
+
trackFiles: {
isFetching: false,
isPopulated: false,
@@ -87,6 +97,10 @@ 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';
@@ -103,6 +117,10 @@ 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);
@@ -190,7 +208,6 @@ export const actionHandlers = handleThunks({
trackIds: (item.tracks || []).map((e) => e.id),
quality: item.quality,
releaseGroup: item.releaseGroup,
- indexerFlags: item.indexerFlags,
downloadId: item.downloadId,
additionalFile: item.additionalFile,
replaceExistingFiles: item.replaceExistingFiles,
@@ -235,6 +252,8 @@ export const actionHandlers = handleThunks({
});
},
+ [FETCH_INTERACTIVE_IMPORT_ALBUMS]: createFetchHandler(albumsSection, '/album'),
+
[FETCH_INTERACTIVE_IMPORT_TRACKFILES]: createFetchHandler(trackFilesSection, '/trackFile')
});
@@ -316,6 +335,14 @@ 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
deleted file mode 100644
index d4b6e9bcb..000000000
--- a/frontend/src/Store/Actions/parseActions.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-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 c4955c915..1c9b6f5ef 100644
--- a/frontend/src/Store/Actions/releaseActions.js
+++ b/frontend/src/Store/Actions/releaseActions.js
@@ -219,9 +219,8 @@ export const defaultState = {
};
export const persistState = [
- 'releases.album.selectedFilterKey',
+ 'releases.selectedFilterKey',
'releases.album.customFilters',
- 'releases.artist.selectedFilterKey',
'releases.artist.customFilters'
];
diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js
index 54b059083..b787110c1 100644
--- a/frontend/src/Store/Actions/settingsActions.js
+++ b/frontend/src/Store/Actions/settingsActions.js
@@ -11,7 +11,6 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import importListExclusions from './Settings/importListExclusions';
import importLists from './Settings/importLists';
-import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions';
import indexers from './Settings/indexers';
import languages from './Settings/languages';
@@ -39,7 +38,6 @@ export * from './Settings/downloadClientOptions';
export * from './Settings/general';
export * from './Settings/importLists';
export * from './Settings/importListExclusions';
-export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions';
export * from './Settings/indexers';
export * from './Settings/languages';
@@ -75,7 +73,6 @@ export const defaultState = {
downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState,
general: general.defaultState,
- indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState,
importLists: importLists.defaultState,
@@ -122,7 +119,6 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers,
...general.actionHandlers,
- ...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers,
...indexers.actionHandlers,
...importLists.actionHandlers,
@@ -160,7 +156,6 @@ export const reducers = createHandleActions({
...downloadClients.reducers,
...downloadClientOptions.reducers,
...general.reducers,
- ...indexerFlags.reducers,
...indexerOptions.reducers,
...indexers.reducers,
...importLists.reducers,
diff --git a/frontend/src/Store/Actions/trackActions.js b/frontend/src/Store/Actions/trackActions.js
index a71388c88..bd1f472c3 100644
--- a/frontend/src/Store/Actions/trackActions.js
+++ b/frontend/src/Store/Actions/trackActions.js
@@ -77,15 +77,6 @@ export const defaultState = {
}),
isVisible: false
},
- {
- name: 'indexerFlags',
- columnLabel: () => translate('IndexerFlags'),
- label: React.createElement(Icon, {
- name: icons.FLAG,
- title: () => translate('IndexerFlags')
- }),
- isVisible: false
- },
{
name: 'status',
label: () => translate('Status'),
diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js
index 61d6f7752..35aa162d4 100644
--- a/frontend/src/Store/Actions/wantedActions.js
+++ b/frontend/src/Store/Actions/wantedActions.js
@@ -52,12 +52,6 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
- {
- name: 'albums.lastSearchTime',
- label: () => translate('LastSearched'),
- isSortable: true,
- isVisible: false
- },
// {
// name: 'status',
// label: 'Status',
@@ -137,12 +131,6 @@ 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.ts b/frontend/src/Store/Selectors/createAllArtistSelector.js
similarity index 71%
rename from frontend/src/Store/Selectors/createAllArtistSelector.ts
rename to frontend/src/Store/Selectors/createAllArtistSelector.js
index 6b6010429..38b1bcef1 100644
--- a/frontend/src/Store/Selectors/createAllArtistSelector.ts
+++ b/frontend/src/Store/Selectors/createAllArtistSelector.js
@@ -1,9 +1,8 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createAllArtistSelector() {
return createSelector(
- (state: AppState) => state.artist,
+ (state) => state.artist,
(artist) => {
return artist.items;
}
diff --git a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts
index 414a451f5..2ae54a10c 100644
--- a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts
+++ b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts
@@ -1,5 +1,4 @@
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';
@@ -8,11 +7,11 @@ function createArtistAlbumsSelector(artistId: number) {
return createSelector(
(state: AppState) => state.albums,
createArtistSelectorForHook(artistId),
- (albums: AlbumAppState, artist = {} as Artist) => {
+ (albums, artist = {} as Artist) => {
const { isFetching, isPopulated, error, items } = albums;
const filteredAlbums = items.filter(
- (album) => album.artistId === artist.id
+ (album) => album.artist.artistMetadataId === artist.artistMetadataId
);
return {
diff --git a/frontend/src/Store/Selectors/createArtistCountSelector.ts b/frontend/src/Store/Selectors/createArtistCountSelector.js
similarity index 65%
rename from frontend/src/Store/Selectors/createArtistCountSelector.ts
rename to frontend/src/Store/Selectors/createArtistCountSelector.js
index b432d64a7..31e0a39fc 100644
--- a/frontend/src/Store/Selectors/createArtistCountSelector.ts
+++ b/frontend/src/Store/Selectors/createArtistCountSelector.js
@@ -1,19 +1,18 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
import createAllArtistSelector from './createAllArtistSelector';
function createArtistCountSelector() {
return createSelector(
createAllArtistSelector(),
- (state: AppState) => state.artist.error,
- (state: AppState) => state.artist.isFetching,
- (state: AppState) => state.artist.isPopulated,
+ (state) => state.artist.error,
+ (state) => state.artist.isFetching,
+ (state) => 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 fa60d936d..0acbd3997 100644
--- a/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts
+++ b/frontend/src/Store/Selectors/createArtistMetadataProfileSelector.ts
@@ -1,14 +1,13 @@
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: MetadataProfile[], artist = {} as Artist) => {
+ (metadataProfiles, 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 67639919b..99325276f 100644
--- a/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts
+++ b/frontend/src/Store/Selectors/createArtistQualityProfileSelector.ts
@@ -1,14 +1,13 @@
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: QualityProfile[], artist = {} as Artist) => {
+ (qualityProfiles, artist = {} as Artist) => {
return qualityProfiles.find(
(profile) => profile.id === artist.qualityProfileId
);
diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts b/frontend/src/Store/Selectors/createCommandExecutingSelector.js
similarity index 50%
rename from frontend/src/Store/Selectors/createCommandExecutingSelector.ts
rename to frontend/src/Store/Selectors/createCommandExecutingSelector.js
index 6a80e172b..6037d5820 100644
--- a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts
+++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.js
@@ -2,10 +2,13 @@ import { createSelector } from 'reselect';
import { isCommandExecuting } from 'Utilities/Command';
import createCommandSelector from './createCommandSelector';
-function createCommandExecutingSelector(name: string, contraints = {}) {
- return createSelector(createCommandSelector(name, contraints), (command) => {
- return isCommandExecuting(command);
- });
+function createCommandExecutingSelector(name, 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
new file mode 100644
index 000000000..709dfebaf
--- /dev/null
+++ b/frontend/src/Store/Selectors/createCommandSelector.js
@@ -0,0 +1,14 @@
+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
deleted file mode 100644
index cced7b186..000000000
--- a/frontend/src/Store/Selectors/createCommandSelector.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-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.ts b/frontend/src/Store/Selectors/createCommandsSelector.js
similarity index 71%
rename from frontend/src/Store/Selectors/createCommandsSelector.ts
rename to frontend/src/Store/Selectors/createCommandsSelector.js
index 2dd5d24a2..7b9edffd9 100644
--- a/frontend/src/Store/Selectors/createCommandsSelector.ts
+++ b/frontend/src/Store/Selectors/createCommandsSelector.js
@@ -1,9 +1,8 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createCommandsSelector() {
return createSelector(
- (state: AppState) => state.commands,
+ (state) => state.commands,
(commands) => {
return commands.items;
}
diff --git a/frontend/src/Store/Selectors/createDeepEqualSelector.js b/frontend/src/Store/Selectors/createDeepEqualSelector.js
new file mode 100644
index 000000000..85562f28b
--- /dev/null
+++ b/frontend/src/Store/Selectors/createDeepEqualSelector.js
@@ -0,0 +1,9 @@
+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
deleted file mode 100644
index 9d4a63d2e..000000000
--- a/frontend/src/Store/Selectors/createDeepEqualSelector.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-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.ts b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js
similarity index 78%
rename from frontend/src/Store/Selectors/createExecutingCommandsSelector.ts
rename to frontend/src/Store/Selectors/createExecutingCommandsSelector.js
index dd16571fc..266865a8a 100644
--- a/frontend/src/Store/Selectors/createExecutingCommandsSelector.ts
+++ b/frontend/src/Store/Selectors/createExecutingCommandsSelector.js
@@ -1,10 +1,9 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
import { isCommandExecuting } from 'Utilities/Command';
function createExecutingCommandsSelector() {
return createSelector(
- (state: AppState) => state.commands.items,
+ (state) => state.commands.items,
(commands) => {
return commands.filter((command) => isCommandExecuting(command));
}
diff --git a/frontend/src/Store/Selectors/createExistingArtistSelector.ts b/frontend/src/Store/Selectors/createExistingArtistSelector.js
similarity index 58%
rename from frontend/src/Store/Selectors/createExistingArtistSelector.ts
rename to frontend/src/Store/Selectors/createExistingArtistSelector.js
index 91b5bc4d6..4811f2034 100644
--- a/frontend/src/Store/Selectors/createExistingArtistSelector.ts
+++ b/frontend/src/Store/Selectors/createExistingArtistSelector.js
@@ -1,15 +1,13 @@
-import { some } from 'lodash';
+import _ from 'lodash';
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
import createAllArtistSelector from './createAllArtistSelector';
function createExistingArtistSelector() {
return createSelector(
- (_: AppState, { foreignArtistId }: { foreignArtistId: string }) =>
- foreignArtistId,
+ (state, { foreignArtistId }) => foreignArtistId,
createAllArtistSelector(),
(foreignArtistId, artist) => {
- return some(artist, { foreignArtistId });
+ return _.some(artist, { foreignArtistId });
}
);
}
diff --git a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts b/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts
deleted file mode 100644
index 90587639c..000000000
--- a/frontend/src/Store/Selectors/createIndexerFlagsSelector.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
-
-const createIndexerFlagsSelector = createSelector(
- (state: AppState) => state.settings.indexerFlags,
- (indexerFlags) => indexerFlags
-);
-
-export default createIndexerFlagsSelector;
diff --git a/frontend/src/Store/Selectors/createMetadataProfileSelector.js b/frontend/src/Store/Selectors/createMetadataProfileSelector.js
new file mode 100644
index 000000000..bdd0d0636
--- /dev/null
+++ b/frontend/src/Store/Selectors/createMetadataProfileSelector.js
@@ -0,0 +1,15 @@
+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
deleted file mode 100644
index ae4c061db..000000000
--- a/frontend/src/Store/Selectors/createMetadataProfileSelector.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-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/createMultiArtistsSelector.ts b/frontend/src/Store/Selectors/createMultiArtistsSelector.ts
deleted file mode 100644
index d8f7ea92b..000000000
--- a/frontend/src/Store/Selectors/createMultiArtistsSelector.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
-import Artist from 'Artist/Artist';
-
-function createMultiArtistsSelector(artistIds: number[]) {
- return createSelector(
- (state: AppState) => state.artist.itemMap,
- (state: AppState) => state.artist.items,
- (itemMap, allArtists) => {
- return artistIds.reduce((acc: Artist[], artistId) => {
- const artist = allArtists[itemMap[artistId]];
-
- if (artist) {
- acc.push(artist);
- }
-
- return acc;
- }, []);
- }
- );
-}
-
-export default createMultiArtistsSelector;
diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js
new file mode 100644
index 000000000..84fefb83e
--- /dev/null
+++ b/frontend/src/Store/Selectors/createProfileInUseSelector.js
@@ -0,0 +1,24 @@
+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
deleted file mode 100644
index 85f0c3211..000000000
--- a/frontend/src/Store/Selectors/createProfileInUseSelector.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-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
new file mode 100644
index 000000000..611dfc903
--- /dev/null
+++ b/frontend/src/Store/Selectors/createQualityProfileSelector.js
@@ -0,0 +1,26 @@
+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
deleted file mode 100644
index b913e0c46..000000000
--- a/frontend/src/Store/Selectors/createQualityProfileSelector.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-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.ts b/frontend/src/Store/Selectors/createQueueItemSelector.js
similarity index 52%
rename from frontend/src/Store/Selectors/createQueueItemSelector.ts
rename to frontend/src/Store/Selectors/createQueueItemSelector.js
index 54951a724..c85d7ed82 100644
--- a/frontend/src/Store/Selectors/createQueueItemSelector.ts
+++ b/frontend/src/Store/Selectors/createQueueItemSelector.js
@@ -1,16 +1,21 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createQueueItemSelector() {
return createSelector(
- (_: AppState, { albumId }: { albumId: number }) => albumId,
- (state: AppState) => state.queue.details.items,
+ (state, { albumId }) => albumId,
+ (state) => state.queue.details.items,
(albumId, details) => {
if (!albumId || !details) {
return null;
}
- return details.find((item) => item.albumId === albumId);
+ return details.find((item) => {
+ if (item.album) {
+ return item.album.id === albumId;
+ }
+
+ return false;
+ });
}
);
}
diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts
index 432f9056d..a016d7665 100644
--- a/frontend/src/Store/Selectors/createRootFoldersSelector.ts
+++ b/frontend/src/Store/Selectors/createRootFoldersSelector.ts
@@ -1,15 +1,11 @@
import { createSelector } from 'reselect';
import { RootFolderAppState } from 'App/State/SettingsAppState';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
-import RootFolder from 'typings/RootFolder';
-import sortByProp from 'Utilities/Array/sortByProp';
+import sortByName from 'Utilities/Array/sortByName';
export default function createRootFoldersSelector() {
return createSelector(
- createSortedSectionSelector
(
- 'settings.rootFolders',
- sortByProp('name')
- ),
+ createSortedSectionSelector('settings.rootFolders', sortByName),
(rootFolders: RootFolderAppState) => rootFolders
);
}
diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.ts b/frontend/src/Store/Selectors/createSortedSectionSelector.js
similarity index 68%
rename from frontend/src/Store/Selectors/createSortedSectionSelector.ts
rename to frontend/src/Store/Selectors/createSortedSectionSelector.js
index abee01f75..331d890c9 100644
--- a/frontend/src/Store/Selectors/createSortedSectionSelector.ts
+++ b/frontend/src/Store/Selectors/createSortedSectionSelector.js
@@ -1,18 +1,14 @@
import { createSelector } from 'reselect';
import getSectionState from 'Utilities/State/getSectionState';
-function createSortedSectionSelector(
- section: string,
- comparer: (a: T, b: T) => number
-) {
+function createSortedSectionSelector(section, comparer) {
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.ts b/frontend/src/Store/Selectors/createSystemStatusSelector.js
similarity index 70%
rename from frontend/src/Store/Selectors/createSystemStatusSelector.ts
rename to frontend/src/Store/Selectors/createSystemStatusSelector.js
index f5e276069..df586bbb9 100644
--- a/frontend/src/Store/Selectors/createSystemStatusSelector.ts
+++ b/frontend/src/Store/Selectors/createSystemStatusSelector.js
@@ -1,9 +1,8 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createSystemStatusSelector() {
return createSelector(
- (state: AppState) => state.system.status,
+ (state) => state.system.status,
(status) => {
return status.item;
}
diff --git a/frontend/src/Store/Selectors/createTagDetailsSelector.ts b/frontend/src/Store/Selectors/createTagDetailsSelector.js
similarity index 62%
rename from frontend/src/Store/Selectors/createTagDetailsSelector.ts
rename to frontend/src/Store/Selectors/createTagDetailsSelector.js
index 2a271cafe..dd178944c 100644
--- a/frontend/src/Store/Selectors/createTagDetailsSelector.ts
+++ b/frontend/src/Store/Selectors/createTagDetailsSelector.js
@@ -1,10 +1,9 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createTagDetailsSelector() {
return createSelector(
- (_: AppState, { id }: { id: number }) => id,
- (state: AppState) => state.tags.details.items,
+ (state, { id }) => id,
+ (state) => state.tags.details.items,
(id, tagDetails) => {
return tagDetails.find((t) => t.id === id);
}
diff --git a/frontend/src/Store/Selectors/createTagsSelector.ts b/frontend/src/Store/Selectors/createTagsSelector.js
similarity index 68%
rename from frontend/src/Store/Selectors/createTagsSelector.ts
rename to frontend/src/Store/Selectors/createTagsSelector.js
index f653ff6e3..fbfd91cdb 100644
--- a/frontend/src/Store/Selectors/createTagsSelector.ts
+++ b/frontend/src/Store/Selectors/createTagsSelector.js
@@ -1,9 +1,8 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createTagsSelector() {
return createSelector(
- (state: AppState) => state.tags.items,
+ (state) => state.tags.items,
(tags) => {
return tags;
}
diff --git a/frontend/src/Store/Selectors/createTrackFileSelector.ts b/frontend/src/Store/Selectors/createTrackFileSelector.js
similarity index 66%
rename from frontend/src/Store/Selectors/createTrackFileSelector.ts
rename to frontend/src/Store/Selectors/createTrackFileSelector.js
index a162df1fa..bcfc5cb0b 100644
--- a/frontend/src/Store/Selectors/createTrackFileSelector.ts
+++ b/frontend/src/Store/Selectors/createTrackFileSelector.js
@@ -1,10 +1,9 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createTrackFileSelector() {
return createSelector(
- (_: AppState, { trackFileId }: { trackFileId: number }) => trackFileId,
- (state: AppState) => state.trackFiles,
+ (state, { trackFileId }) => trackFileId,
+ (state) => state.trackFiles,
(trackFileId, trackFiles) => {
if (!trackFileId) {
return;
diff --git a/frontend/src/Store/Selectors/createUISettingsSelector.ts b/frontend/src/Store/Selectors/createUISettingsSelector.js
similarity index 69%
rename from frontend/src/Store/Selectors/createUISettingsSelector.ts
rename to frontend/src/Store/Selectors/createUISettingsSelector.js
index ff539679b..b256d0e98 100644
--- a/frontend/src/Store/Selectors/createUISettingsSelector.ts
+++ b/frontend/src/Store/Selectors/createUISettingsSelector.js
@@ -1,9 +1,8 @@
import { createSelector } from 'reselect';
-import AppState from 'App/State/AppState';
function createUISettingsSelector() {
return createSelector(
- (state: AppState) => state.settings.ui,
+ (state) => state.settings.ui,
(ui) => {
return ui.item;
}
diff --git a/frontend/src/Styles/Themes/index.js b/frontend/src/Styles/Themes/index.js
index 4dec39164..d93c5dd8c 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 def48f28e..3b0077c5a 100644
--- a/frontend/src/Styles/Variables/fonts.js
+++ b/frontend/src/Styles/Variables/fonts.js
@@ -2,6 +2,7 @@ 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 5339a8590..83736c617 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,16 +77,15 @@ class LogFiles extends Component {
- {translate('LogFilesLocation', {
- location
- })}
+ Log files are located in: {location}
- {currentLogView === 'Log Files' ? (
-
-
-
- ) : null}
+ {
+ currentLogView === 'Log Files' &&
+
+ The log level defaults to 'Info' and can be changed in General Settings
+
+ }
{
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
index 6e38929c9..034804711 100644
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
@@ -10,6 +10,15 @@
width: 100%;
}
+.commandName {
+ display: inline-block;
+ min-width: 220px;
+}
+
+.userAgent {
+ color: #b0b0b0;
+}
+
.queued,
.started,
.ended {
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts
index 2c6010533..3bc00b738 100644
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css.d.ts
@@ -2,12 +2,14 @@
// Please do not change this file!
interface CssExports {
'actions': string;
+ 'commandName': string;
'duration': string;
'ended': string;
'queued': string;
'started': string;
'trigger': string;
'triggerContent': string;
+ 'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js
new file mode 100644
index 000000000..6f4da3828
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js
@@ -0,0 +1,279 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import Icon from 'Components/Icon';
+import IconButton from 'Components/Link/IconButton';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import TableRow from 'Components/Table/TableRow';
+import { icons, kinds } from 'Helpers/Props';
+import formatDate from 'Utilities/Date/formatDate';
+import formatDateTime from 'Utilities/Date/formatDateTime';
+import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
+import titleCase from 'Utilities/String/titleCase';
+import translate from 'Utilities/String/translate';
+import styles from './QueuedTaskRow.css';
+
+function getStatusIconProps(status, message) {
+ const title = titleCase(status);
+
+ switch (status) {
+ case 'queued':
+ return {
+ name: icons.PENDING,
+ title
+ };
+
+ case 'started':
+ return {
+ name: icons.REFRESH,
+ isSpinning: true,
+ title
+ };
+
+ case 'completed':
+ return {
+ name: icons.CHECK,
+ kind: kinds.SUCCESS,
+ title: message === 'Completed' ? title : `${title}: ${message}`
+ };
+
+ case 'failed':
+ return {
+ name: icons.FATAL,
+ kind: kinds.DANGER,
+ title: `${title}: ${message}`
+ };
+
+ default:
+ return {
+ name: icons.UNKNOWN,
+ title
+ };
+ }
+}
+
+function getFormattedDates(props) {
+ const {
+ queued,
+ started,
+ ended,
+ showRelativeDates,
+ shortDateFormat
+ } = props;
+
+ if (showRelativeDates) {
+ return {
+ queuedAt: moment(queued).fromNow(),
+ startedAt: started ? moment(started).fromNow() : '-',
+ endedAt: ended ? moment(ended).fromNow() : '-'
+ };
+ }
+
+ return {
+ queuedAt: formatDate(queued, shortDateFormat),
+ startedAt: started ? formatDate(started, shortDateFormat) : '-',
+ endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
+ };
+}
+
+class QueuedTaskRow extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ ...getFormattedDates(props),
+ isCancelConfirmModalOpen: false
+ };
+
+ this._updateTimeoutId = null;
+ }
+
+ componentDidMount() {
+ this.setUpdateTimer();
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ queued,
+ started,
+ ended
+ } = this.props;
+
+ if (
+ queued !== prevProps.queued ||
+ started !== prevProps.started ||
+ ended !== prevProps.ended
+ ) {
+ this.setState(getFormattedDates(this.props));
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._updateTimeoutId) {
+ this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
+ }
+ }
+
+ //
+ // Control
+
+ setUpdateTimer() {
+ this._updateTimeoutId = setTimeout(() => {
+ this.setState(getFormattedDates(this.props));
+ this.setUpdateTimer();
+ }, 30000);
+ }
+
+ //
+ // Listeners
+
+ onCancelPress = () => {
+ this.setState({
+ isCancelConfirmModalOpen: true
+ });
+ };
+
+ onAbortCancel = () => {
+ this.setState({
+ isCancelConfirmModalOpen: false
+ });
+ };
+
+ //
+ // Render
+
+ render() {
+ const {
+ trigger,
+ commandName,
+ queued,
+ started,
+ ended,
+ status,
+ duration,
+ message,
+ clientUserAgent,
+ longDateFormat,
+ timeFormat,
+ onCancelPress
+ } = this.props;
+
+ const {
+ queuedAt,
+ startedAt,
+ endedAt,
+ isCancelConfirmModalOpen
+ } = this.state;
+
+ let triggerIcon = icons.QUICK;
+
+ if (trigger === 'manual') {
+ triggerIcon = icons.INTERACTIVE;
+ } else if (trigger === 'scheduled') {
+ triggerIcon = icons.SCHEDULED;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {commandName}
+
+ {
+ clientUserAgent ?
+
+ from: {clientUserAgent}
+ :
+ null
+ }
+
+
+
+ {queuedAt}
+
+
+
+ {startedAt}
+
+
+
+ {endedAt}
+
+
+
+ {formatTimeSpan(duration)}
+
+
+
+ {
+ status === 'queued' &&
+
+ }
+
+
+
+
+ );
+ }
+}
+
+QueuedTaskRow.propTypes = {
+ trigger: PropTypes.string.isRequired,
+ commandName: PropTypes.string.isRequired,
+ queued: PropTypes.string.isRequired,
+ started: PropTypes.string,
+ ended: PropTypes.string,
+ status: PropTypes.string.isRequired,
+ duration: PropTypes.string,
+ message: PropTypes.string,
+ clientUserAgent: PropTypes.string,
+ showRelativeDates: PropTypes.bool.isRequired,
+ shortDateFormat: PropTypes.string.isRequired,
+ longDateFormat: PropTypes.string.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ onCancelPress: PropTypes.func.isRequired
+};
+
+export default QueuedTaskRow;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
deleted file mode 100644
index 4511bcbf4..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRow.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import moment from 'moment';
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { CommandBody } from 'Commands/Command';
-import Icon from 'Components/Icon';
-import IconButton from 'Components/Link/IconButton';
-import ConfirmModal from 'Components/Modal/ConfirmModal';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import TableRow from 'Components/Table/TableRow';
-import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
-import { icons, kinds } from 'Helpers/Props';
-import { cancelCommand } from 'Store/Actions/commandActions';
-import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import formatDate from 'Utilities/Date/formatDate';
-import formatDateTime from 'Utilities/Date/formatDateTime';
-import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
-import titleCase from 'Utilities/String/titleCase';
-import translate from 'Utilities/String/translate';
-import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
-import styles from './QueuedTaskRow.css';
-
-function getStatusIconProps(status: string, message: string | undefined) {
- const title = titleCase(status);
-
- switch (status) {
- case 'queued':
- return {
- name: icons.PENDING,
- title,
- };
-
- case 'started':
- return {
- name: icons.REFRESH,
- isSpinning: true,
- title,
- };
-
- case 'completed':
- return {
- name: icons.CHECK,
- kind: kinds.SUCCESS,
- title: message === 'Completed' ? title : `${title}: ${message}`,
- };
-
- case 'failed':
- return {
- name: icons.FATAL,
- kind: kinds.DANGER,
- title: `${title}: ${message}`,
- };
-
- default:
- return {
- name: icons.UNKNOWN,
- title,
- };
- }
-}
-
-function getFormattedDates(
- queued: string,
- started: string | undefined,
- ended: string | undefined,
- showRelativeDates: boolean,
- shortDateFormat: string
-) {
- if (showRelativeDates) {
- return {
- queuedAt: moment(queued).fromNow(),
- startedAt: started ? moment(started).fromNow() : '-',
- endedAt: ended ? moment(ended).fromNow() : '-',
- };
- }
-
- return {
- queuedAt: formatDate(queued, shortDateFormat),
- startedAt: started ? formatDate(started, shortDateFormat) : '-',
- endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
- };
-}
-
-interface QueuedTimes {
- queuedAt: string;
- startedAt: string;
- endedAt: string;
-}
-
-export interface QueuedTaskRowProps {
- id: number;
- trigger: string;
- commandName: string;
- queued: string;
- started?: string;
- ended?: string;
- status: string;
- duration?: string;
- message?: string;
- body: CommandBody;
- clientUserAgent?: string;
-}
-
-export default function QueuedTaskRow(props: QueuedTaskRowProps) {
- const {
- id,
- trigger,
- commandName,
- queued,
- started,
- ended,
- status,
- duration,
- message,
- body,
- clientUserAgent,
- } = props;
-
- const dispatch = useDispatch();
- const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
- useSelector(createUISettingsSelector());
-
- const updateTimeTimeoutId = useRef | null>(
- null
- );
- const [times, setTimes] = useState(
- getFormattedDates(
- queued,
- started,
- ended,
- showRelativeDates,
- shortDateFormat
- )
- );
-
- const [
- isCancelConfirmModalOpen,
- openCancelConfirmModal,
- closeCancelConfirmModal,
- ] = useModalOpenState(false);
-
- const handleCancelPress = useCallback(() => {
- dispatch(cancelCommand({ id }));
- }, [id, dispatch]);
-
- useEffect(() => {
- updateTimeTimeoutId.current = setTimeout(() => {
- setTimes(
- getFormattedDates(
- queued,
- started,
- ended,
- showRelativeDates,
- shortDateFormat
- )
- );
- }, 30000);
-
- return () => {
- if (updateTimeTimeoutId.current) {
- clearTimeout(updateTimeTimeoutId.current);
- }
- };
- }, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
-
- const { queuedAt, startedAt, endedAt } = times;
-
- let triggerIcon = icons.QUICK;
-
- if (trigger === 'manual') {
- triggerIcon = icons.INTERACTIVE;
- } else if (trigger === 'scheduled') {
- triggerIcon = icons.SCHEDULED;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {queuedAt}
-
-
-
- {startedAt}
-
-
-
- {endedAt}
-
-
-
- {formatTimeSpan(duration)}
-
-
-
- {status === 'queued' && (
-
- )}
-
-
-
-
- );
-}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
new file mode 100644
index 000000000..f55ab985a
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { cancelCommand } from 'Store/Actions/commandActions';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import QueuedTaskRow from './QueuedTaskRow';
+
+function createMapStateToProps() {
+ return createSelector(
+ createUISettingsSelector(),
+ (uiSettings) => {
+ return {
+ showRelativeDates: uiSettings.showRelativeDates,
+ shortDateFormat: uiSettings.shortDateFormat,
+ longDateFormat: uiSettings.longDateFormat,
+ timeFormat: uiSettings.timeFormat
+ };
+ }
+ );
+}
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onCancelPress() {
+ dispatch(cancelCommand({
+ id: props.id
+ }));
+ }
+ };
+}
+
+export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css
deleted file mode 100644
index 41acb33f8..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.commandName {
- display: inline-block;
- min-width: 220px;
-}
-
-.userAgent {
- color: #b0b0b0;
-}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
deleted file mode 100644
index fc9081492..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.css.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// This file is automatically generated.
-// Please do not change this file!
-interface CssExports {
- 'commandName': string;
- 'userAgent': string;
-}
-export const cssExports: CssExports;
-export default cssExports;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
deleted file mode 100644
index 41a307d5f..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react';
-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;
- clientUserAgent?: string;
-}
-
-export default function QueuedTaskRowNameCell(
- props: QueuedTaskRowNameCellProps
-) {
- const { commandName, body, clientUserAgent } = props;
- const movieIds = [...(body.artistIds ?? [])];
-
- if (body.artistId) {
- movieIds.push(body.artistId);
- }
-
- const artists = useSelector(createMultiArtistsSelector(movieIds));
- const sortedArtists = artists.sort(sortByProp('sortName'));
-
- return (
-
-
- {commandName}
- {sortedArtists.length ? (
- - {formatTitles(sortedArtists.map((a) => a.artistName))}
- ) : null}
-
-
- {clientUserAgent ? (
-
- {translate('From')}: {clientUserAgent}
-
- ) : null}
-
- );
-}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js
new file mode 100644
index 000000000..dac38f1d4
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTasks.js
@@ -0,0 +1,90 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import FieldSet from 'Components/FieldSet';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import translate from 'Utilities/String/translate';
+import QueuedTaskRowConnector from './QueuedTaskRowConnector';
+
+const columns = [
+ {
+ name: 'trigger',
+ label: '',
+ isVisible: true
+ },
+ {
+ name: 'commandName',
+ label: () => translate('Name'),
+ isVisible: true
+ },
+ {
+ name: 'queued',
+ label: () => translate('Queued'),
+ isVisible: true
+ },
+ {
+ name: 'started',
+ label: () => translate('Started'),
+ isVisible: true
+ },
+ {
+ name: 'ended',
+ label: () => translate('Ended'),
+ isVisible: true
+ },
+ {
+ name: 'duration',
+ label: () => translate('Duration'),
+ isVisible: true
+ },
+ {
+ name: 'actions',
+ isVisible: true
+ }
+];
+
+function QueuedTasks(props) {
+ const {
+ isFetching,
+ isPopulated,
+ items
+ } = props;
+
+ return (
+
+ );
+}
+
+QueuedTasks.propTypes = {
+ isFetching: PropTypes.bool.isRequired,
+ isPopulated: PropTypes.bool.isRequired,
+ items: PropTypes.array.isRequired
+};
+
+export default QueuedTasks;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx b/frontend/src/System/Tasks/Queued/QueuedTasks.tsx
deleted file mode 100644
index e79deed7c..000000000
--- a/frontend/src/System/Tasks/Queued/QueuedTasks.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { useEffect } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import AppState from 'App/State/AppState';
-import FieldSet from 'Components/FieldSet';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import { fetchCommands } from 'Store/Actions/commandActions';
-import translate from 'Utilities/String/translate';
-import QueuedTaskRow from './QueuedTaskRow';
-
-const columns = [
- {
- name: 'trigger',
- label: '',
- isVisible: true,
- },
- {
- name: 'commandName',
- label: () => translate('Name'),
- isVisible: true,
- },
- {
- name: 'queued',
- label: () => translate('Queued'),
- isVisible: true,
- },
- {
- name: 'started',
- label: () => translate('Started'),
- isVisible: true,
- },
- {
- name: 'ended',
- label: () => translate('Ended'),
- isVisible: true,
- },
- {
- name: 'duration',
- label: () => translate('Duration'),
- isVisible: true,
- },
- {
- name: 'actions',
- isVisible: true,
- },
-];
-
-export default function QueuedTasks() {
- const dispatch = useDispatch();
- const { isFetching, isPopulated, items } = useSelector(
- (state: AppState) => state.commands
- );
-
- useEffect(() => {
- dispatch(fetchCommands());
- }, [dispatch]);
-
- return (
-
- );
-}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
new file mode 100644
index 000000000..5fa4d9ead
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
@@ -0,0 +1,46 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { fetchCommands } from 'Store/Actions/commandActions';
+import QueuedTasks from './QueuedTasks';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.commands,
+ (commands) => {
+ return commands;
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ dispatchFetchCommands: fetchCommands
+};
+
+class QueuedTasksConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount() {
+ this.props.dispatchFetchCommands();
+ }
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+QueuedTasksConnector.propTypes = {
+ dispatchFetchCommands: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js
index 03a3b6ce4..032dbede8 100644
--- a/frontend/src/System/Tasks/Tasks.js
+++ b/frontend/src/System/Tasks/Tasks.js
@@ -2,7 +2,7 @@ import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate';
-import QueuedTasks from './Queued/QueuedTasks';
+import QueuedTasksConnector from './Queued/QueuedTasksConnector';
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
function Tasks() {
@@ -10,7 +10,7 @@ function Tasks() {
-
+
);
diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js
new file mode 100644
index 000000000..3588069a0
--- /dev/null
+++ b/frontend/src/System/Updates/UpdateChanges.js
@@ -0,0 +1,46 @@
+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
deleted file mode 100644
index 3e5ba1c9b..000000000
--- a/frontend/src/System/Updates/UpdateChanges.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-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
new file mode 100644
index 000000000..528441cbe
--- /dev/null
+++ b/frontend/src/System/Updates/Updates.js
@@ -0,0 +1,249 @@
+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
deleted file mode 100644
index 300ab1f99..000000000
--- a/frontend/src/System/Updates/Updates.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-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
new file mode 100644
index 000000000..77d75dbda
--- /dev/null
+++ b/frontend/src/System/Updates/UpdatesConnector.js
@@ -0,0 +1,98 @@
+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 (
+