-
\\^$.|?*+()[{ have special meanings and need escaping with a \\
' }} />
- {'More details'} {'Here'}
+
- {'Regular expressions can be tested '}
- Here
+
+
+
+
}
diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx
index 734f5efab..b2c1208cb 100644
--- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx
+++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
+import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
-import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteDownloadClients,
bulkEditDownloadClients,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageDownloadClientsModalRow
>['onSelectedChange'];
-const COLUMNS = [
+const COLUMNS: Column[] = [
{
name: 'name',
label: () => translate('Name'),
@@ -82,8 +82,6 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps {
onModalClose(): void;
- sortKey?: string;
- sortDirection?: SortDirection;
}
function ManageDownloadClientsModalContent(
diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js
index 2799af7d8..d50fb2385 100644
--- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js
+++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContent.js
@@ -292,7 +292,7 @@ function EditImportListModalContent(props) {
diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx
index dbb394959..997d1b566 100644
--- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx
+++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx
@@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
+import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
-import SortDirection from 'Helpers/Props/SortDirection';
import {
bulkDeleteIndexers,
bulkEditIndexers,
@@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageIndexersModalRow
>['onSelectedChange'];
-const COLUMNS = [
+const COLUMNS: Column[] = [
{
name: 'name',
label: () => translate('Name'),
@@ -82,8 +82,6 @@ const COLUMNS = [
interface ManageIndexersModalContentProps {
onModalClose(): void;
- sortKey?: string;
- sortDirection?: SortDirection;
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
diff --git a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js
index 47e5dfcf1..dc91e4622 100644
--- a/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js
+++ b/frontend/src/Settings/MediaManagement/RootFolder/RootFolder.js
@@ -94,9 +94,9 @@ class RootFolder extends Component {
diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js
index 383cf057b..ee51799f2 100644
--- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js
+++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js
@@ -105,7 +105,7 @@ function EditNotificationModalContent(props) {
diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js
index da1bf7f44..0d1225a93 100644
--- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js
+++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js
@@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
{
!isFetching && !!error ?
-
- {translate('UnableToAddANewQualityProfilePleaseTryAgain')}
-
:
+
+ {translate('AddDelayProfileError')}
+ :
null
}
@@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
{
id === 1 ?
- {translate('DefaultDelayProfileHelpText')}
+ {translate('DefaultDelayProfileArtist')}
:
@@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
type={inputTypes.TAG}
name="tags"
{...tags}
- helpText={translate('TagsHelpText')}
+ helpText={translate('DelayProfileArtistTagsHelpText')}
onChange={onInputChange}
/>
diff --git a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js
index c6c297c81..e1c695c42 100644
--- a/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js
+++ b/frontend/src/Settings/Profiles/Release/EditReleaseProfileModalContent.js
@@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) {
diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css b/frontend/src/Settings/Quality/Definition/QualityDefinition.css
index e090428a1..860333725 100644
--- a/frontend/src/Settings/Quality/Definition/QualityDefinition.css
+++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css
@@ -24,19 +24,19 @@
height: 20px;
}
-.bar {
+.track {
top: 9px;
margin: 0 5px;
height: 3px;
background-color: var(--sliderAccentColor);
box-shadow: 0 0 0 #000;
- &:nth-child(3n+1) {
+ &:nth-child(3n + 1) {
background-color: #ddd;
}
}
-.handle {
+.thumb {
top: 1px;
z-index: 0 !important;
width: 18px;
diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts b/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts
index 2b92fb212..9c9e8393a 100644
--- a/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts
+++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css.d.ts
@@ -1,8 +1,6 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
- 'bar': string;
- 'handle': string;
'kilobitsPerSecond': string;
'quality': string;
'qualityDefinition': string;
@@ -10,7 +8,9 @@ interface CssExports {
'sizeLimit': string;
'sizes': string;
'slider': string;
+ 'thumb': string;
'title': string;
+ 'track': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js
index 7d8a78737..48251abfb 100644
--- a/frontend/src/Settings/Quality/Definition/QualityDefinition.js
+++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js
@@ -55,6 +55,27 @@ class QualityDefinition extends Component {
};
}
+ //
+ // Control
+
+ trackRenderer(props, state) {
+ return (
+
+ );
+ }
+
+ thumbRenderer(props, state) {
+ return (
+
+ );
+ }
+
//
// Listeners
@@ -174,6 +195,7 @@ class QualityDefinition extends Component {
diff --git a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js
index 2ab1e4a1c..04302729b 100644
--- a/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js
+++ b/frontend/src/Settings/Tags/AutoTagging/Specifications/EditSpecificationModalContent.js
@@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
-
+
-
+
}
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/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js
index 055e92662..736502460 100644
--- a/frontend/src/Store/Actions/artistIndexActions.js
+++ b/frontend/src/Store/Actions/artistIndexActions.js
@@ -151,7 +151,7 @@ export const defaultState = {
{
name: 'genres',
label: () => translate('Genres'),
- isSortable: false,
+ isSortable: true,
isVisible: false
},
{
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/Selectors/createArtistAlbumsSelector.ts b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts
index baffd87ec..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,7 +8,7 @@ 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(
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/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/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx
index ef3d20288..300ab1f99 100644
--- a/frontend/src/System/Updates/Updates.tsx
+++ b/frontend/src/System/Updates/Updates.tsx
@@ -270,7 +270,7 @@ function Updates() {
{generalSettingsError ? (
- {translate('FailedToUpdateSettings')}
+ {translate('FailedToFetchSettings')}
) : null}
diff --git a/frontend/src/Utilities/Date/getRelativeDate.js b/frontend/src/Utilities/Date/getRelativeDate.ts
similarity index 65%
rename from frontend/src/Utilities/Date/getRelativeDate.js
rename to frontend/src/Utilities/Date/getRelativeDate.ts
index 812064272..178d14fb7 100644
--- a/frontend/src/Utilities/Date/getRelativeDate.js
+++ b/frontend/src/Utilities/Date/getRelativeDate.ts
@@ -6,15 +6,33 @@ import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
-function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
+interface GetRelativeDateOptions {
+ timeFormat?: string;
+ includeSeconds?: boolean;
+ timeForToday?: boolean;
+}
+
+function getRelativeDate(
+ date: string | undefined,
+ shortDateFormat: string,
+ showRelativeDates: boolean,
+ {
+ timeFormat,
+ includeSeconds = false,
+ timeForToday = false,
+ }: GetRelativeDateOptions = {}
+) {
if (!date) {
- return null;
+ return '';
}
const isTodayDate = isToday(date);
if (isTodayDate && timeForToday && timeFormat) {
- return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
+ return formatTime(date, timeFormat, {
+ includeMinuteZero: true,
+ includeSeconds,
+ });
}
if (!showRelativeDates) {
diff --git a/frontend/src/Utilities/String/translate.ts b/frontend/src/Utilities/String/translate.ts
index 98a0418ad..5571ef6b0 100644
--- a/frontend/src/Utilities/String/translate.ts
+++ b/frontend/src/Utilities/String/translate.ts
@@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise {
translations = data.strings;
resolve(true);
- } catch (error) {
+ } catch {
resolve(false);
}
});
@@ -27,6 +27,12 @@ export default function translate(
key: string,
tokens: Record = {}
) {
+ const { isProduction = true } = window.Lidarr;
+
+ if (!isProduction && !(key in translations)) {
+ console.warn(`Missing translation for key: ${key}`);
+ }
+
const translation = translations[key] || key;
tokens.appName = 'Lidarr';
diff --git a/frontend/src/bootstrap.tsx b/frontend/src/bootstrap.tsx
index 6a6d7fc67..9ecf27e0e 100644
--- a/frontend/src/bootstrap.tsx
+++ b/frontend/src/bootstrap.tsx
@@ -1,6 +1,6 @@
import { createBrowserHistory } from 'history';
import React from 'react';
-import { render } from 'react-dom';
+import { createRoot } from 'react-dom/client';
import createAppStore from 'Store/createAppStore';
import App from './App/App';
@@ -9,9 +9,8 @@ import 'Diag/ConsoleApi';
export async function bootstrap() {
const history = createBrowserHistory();
const store = createAppStore(history);
+ const container = document.getElementById('root');
- render(
- ,
- document.getElementById('root')
- );
+ const root = createRoot(container!); // createRoot(container!) if you use TypeScript
+ root.render();
}
diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs
index 5968082b4..a893149d5 100644
--- a/frontend/src/index.ejs
+++ b/frontend/src/index.ejs
@@ -33,7 +33,7 @@
sizes="16x16"
href="/Content/Images/Icons/favicon-16x16.png"
/>
-
+
diff --git a/frontend/src/index.ts b/frontend/src/index.ts
index 36aed4c4b..37e780919 100644
--- a/frontend/src/index.ts
+++ b/frontend/src/index.ts
@@ -14,6 +14,32 @@ window.Lidarr = await response.json();
__webpack_public_path__ = `${window.Lidarr.urlBase}/`;
/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */
+const error = console.error;
+
+// Monkey patch console.error to filter out some warnings from React
+// TODO: Remove this after the great TypeScript migration
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function logError(...parameters: any[]) {
+ const filter = parameters.find((parameter) => {
+ return (
+ typeof parameter === 'string' &&
+ (parameter.includes(
+ 'Support for defaultProps will be removed from function components in a future major release'
+ ) ||
+ parameter.includes(
+ 'findDOMNode is deprecated and will be removed in the next major release'
+ ))
+ );
+ });
+
+ if (!filter) {
+ error(...parameters);
+ }
+}
+
+console.error = logError;
+
const { bootstrap } = await import('./bootstrap');
await bootstrap();
diff --git a/frontend/src/login.html b/frontend/src/login.html
index 113dc547b..24d086959 100644
--- a/frontend/src/login.html
+++ b/frontend/src/login.html
@@ -11,8 +11,11 @@
-
-
+
+
@@ -33,7 +36,11 @@
sizes="16x16"
href="/Content/Images/Icons/favicon-16x16.png"
/>
-
+
-
+
@@ -59,7 +63,7 @@
body {
background-color: var(--pageBackground);
color: var(--textColor);
- font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
+ font-family: 'Roboto', 'open sans', 'Helvetica Neue', Helvetica, Arial,
sans-serif;
}
@@ -209,9 +213,7 @@