Compare commits

..

No commits in common. "develop" and "v2.5.0.4277" have entirely different histories.

444 changed files with 5804 additions and 11056 deletions

View file

@ -6,7 +6,7 @@
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true, "nodeGypDependencies": true,
"version": "20", "version": "16",
"nvmVersion": "latest" "nvmVersion": "latest"
} }
}, },

View file

@ -60,7 +60,6 @@ body:
- Master - Master
- Develop - Develop
- Nightly - Nightly
- Plugins (experimental)
- Other (This issue will be closed) - Other (This issue will be closed)
validations: validations:
required: true required: true

View file

@ -12,6 +12,6 @@ jobs:
action: action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/label-actions@v4 - uses: dessant/label-actions@v3
with: with:
process-only: 'issues' process-only: 'issues'

View file

@ -9,7 +9,7 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v5 - uses: dessant/lock-threads@v4
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: '90' issue-inactive-days: '90'

35
.gitignore vendored
View file

@ -121,7 +121,6 @@ _artifacts
_rawPackage/ _rawPackage/
_dotTrace* _dotTrace*
_tests/ _tests/
_temp*
*.Result.xml *.Result.xml
coverage*.xml coverage*.xml
coverage*.json coverage*.json
@ -140,6 +139,12 @@ project.fragment.lock.json
artifacts/ artifacts/
**/Properties/launchSettings.json **/Properties/launchSettings.json
#VS outout folders
bin
obj
output/*
# macOS metadata files # macOS metadata files
._* ._*
.DS_Store .DS_Store
@ -158,12 +163,34 @@ Thumbs.db
/tools/Addins/* /tools/Addins/*
packages.config.md5sum packages.config.md5sum
# Common IntelliJ Platform excludes
# User specific
**/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/.idea.Radarr.Posix
**/.idea/.idea.Radarr.Windows
# Sensitive or high-churn files
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
# ignore node_modules symlink # ignore node_modules symlink
node_modules node_modules
node_modules.nosync node_modules.nosync
# API doc generation # API doc generation
.config/ .config/
# Ignore Jetbrains IntelliJ Workspace Directories
.idea/

View file

@ -1,7 +1,6 @@
# Lidarr # Lidarr
[![Build Status](https://dev.azure.com/Lidarr/Lidarr/_apis/build/status/lidarr.Lidarr?branchName=develop)](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop) [![Build Status](https://dev.azure.com/Lidarr/Lidarr/_apis/build/status/lidarr.Lidarr?branchName=develop)](https://dev.azure.com/Lidarr/Lidarr/_build/latest?definitionId=1&branchName=develop)
[![Translation status](https://translate.servarr.com/widget/servarr/lidarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/lidarr.svg)](https://wiki.servarr.com/lidarr/installation#docker) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/lidarr.svg)](https://wiki.servarr.com/lidarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/lidarr/lidarr/total.svg) ![Github Downloads](https://img.shields.io/github/downloads/lidarr/lidarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/lidarr/backers/badge.svg)](#backers) [![Backers on Open Collective](https://opencollective.com/lidarr/backers/badge.svg)](#backers)

View file

@ -9,18 +9,18 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '2.13.1' majorVersion: '2.5.0'
minorVersion: $[counter('minorVersion', 1076)] minorVersion: $[counter('minorVersion', 1076)]
lidarrVersion: '$(majorVersion).$(minorVersion)' lidarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(lidarrVersion)' buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.427' dotnetVersion: '6.0.424'
nodeVersion: '20.X' nodeVersion: '20.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-22.04' linuxImage: 'ubuntu-20.04'
macImage: 'macOS-13' macImage: 'macOS-12'
trigger: trigger:
branches: branches:
@ -1120,19 +1120,19 @@ stages:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.windowsImage }}
steps: steps:
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@3 - task: SonarCloudPrepare@2
env: env:
SONAR_SCANNER_OPTS: '' SONAR_SCANNER_OPTS: ''
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'lidarr' organization: 'lidarr'
scannerMode: 'cli' scannerMode: 'CLI'
configMode: 'manual' configMode: 'manual'
cliProjectKey: 'lidarr_Lidarr.UI' cliProjectKey: 'lidarr_Lidarr.UI'
cliProjectName: 'LidarrUI' cliProjectName: 'LidarrUI'
cliProjectVersion: '$(lidarrVersion)' cliProjectVersion: '$(lidarrVersion)'
cliSources: './frontend' cliSources: './frontend'
- task: SonarCloudAnalyze@3 - task: SonarCloudAnalyze@2
- job: Api_Docs - job: Api_Docs
displayName: API Docs displayName: API Docs
@ -1208,12 +1208,12 @@ stages:
submodules: true submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
- task: SonarCloudPrepare@3 - task: SonarCloudPrepare@2
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs: inputs:
SonarCloud: 'SonarCloud' SonarCloud: 'SonarCloud'
organization: 'lidarr' organization: 'lidarr'
scannerMode: 'dotnet' scannerMode: 'MSBuild'
projectKey: 'lidarr_Lidarr' projectKey: 'lidarr_Lidarr'
projectName: 'Lidarr' projectName: 'Lidarr'
projectVersion: '$(lidarrVersion)' projectVersion: '$(lidarrVersion)'
@ -1226,10 +1226,10 @@ stages:
./build.sh --backend -f net6.0 -r win-x64 ./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@3 - task: SonarCloudAnalyze@2
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results displayName: Publish SonarCloud Results
- task: reportgenerator@5.3.11 - task: reportgenerator@5
displayName: Generate Coverage Report displayName: Generate Coverage Report
inputs: inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'

View file

@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
app_guid=${app_guid:-media} app_guid=${app_guid:-media}
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory" echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories" echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
# Create User / Group as needed # Create User / Group as needed
@ -114,7 +114,7 @@ case "$ARCH" in
esac esac
echo "" echo ""
echo "Removing previous tarballs" echo "Removing previous tarballs"
# -f to Force so we fail if it doesn't exist # -f to Force so we fail if it doesnt exist
rm -f "${app^}".*.tar.gz rm -f "${app^}".*.tar.gz
echo "" echo ""
echo "Downloading..." echo "Downloading..."

15
docs.sh
View file

@ -1,18 +1,13 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1 PLATFORM=$1
ARCHITECTURE="${2:-x64}"
if [ "$PLATFORM" = "Windows" ]; then if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-$ARCHITECTURE" RUNTIME="win-x64"
elif [ "$PLATFORM" = "Linux" ]; then elif [ "$PLATFORM" = "Linux" ]; then
RUNTIME="linux-$ARCHITECTURE" RUNTIME="linux-x64"
elif [ "$PLATFORM" = "Mac" ]; then elif [ "$PLATFORM" = "Mac" ]; then
RUNTIME="osx-$ARCHITECTURE" RUNTIME="osx-x64"
else else
echo "Platform must be provided as first argument: Windows, Linux or Mac" echo "Platform must be provided as first arguement: Windows, Linux or Mac"
exit 1 exit 1
fi fi
@ -40,7 +35,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
dotnet new tool-manifest dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 & dotnet tool run swagger tofile --output ./src/Lidarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v1 &
sleep 45 sleep 45

View file

@ -9,7 +9,7 @@
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll": true
}, },
"typescript.preferences.quoteStyle": "single", "typescript.preferences.quoteStyle": "single",

View file

@ -26,7 +26,6 @@ module.exports = (env) => {
const config = { const config = {
mode: isProduction ? 'production' : 'development', mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map', devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: { stats: {
children: false children: false
@ -68,7 +67,7 @@ module.exports = (env) => {
output: { output: {
path: distFolder, path: distFolder,
publicPath: '/', publicPath: '/',
filename: isProduction ? '[name]-[contenthash].js' : '[name].js', filename: '[name]-[contenthash].js',
sourceMapFilename: '[file].map' sourceMapFilename: '[file].map'
}, },
@ -93,7 +92,7 @@ module.exports = (env) => {
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: 'Content/styles.css', filename: 'Content/styles.css',
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css' chunkFilename: 'Content/[id]-[chunkhash].css'
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
@ -135,12 +134,6 @@ module.exports = (env) => {
{ {
source: 'frontend/src/Content/robots.txt', source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt') destination: path.join(distFolder, 'Content/robots.txt')
},
// manifest.json and browserconfig.xml
{
source: 'frontend/src/Content/*.(json|xml)',
destination: path.join(distFolder, 'Content')
} }
] ]
} }
@ -188,7 +181,7 @@ module.exports = (env) => {
loose: true, loose: true,
debug: false, debug: false,
useBuiltIns: 'entry', useBuiltIns: 'entry',
corejs: '3.41' corejs: 3
} }
] ]
] ]
@ -209,7 +202,7 @@ module.exports = (env) => {
options: { options: {
importLoaders: 1, importLoaders: 1,
modules: { modules: {
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]' localIdentName: '[name]/[local]/[hash:base64:5]'
} }
} }
}, },

View file

@ -16,7 +16,6 @@ const mixinsFiles = [
module.exports = { module.exports = {
plugins: [ plugins: [
'autoprefixer',
['postcss-mixins', { ['postcss-mixins', {
mixinsFiles mixinsFiles
}], }],

View file

@ -172,8 +172,7 @@ function HistoryDetails(props) {
if (eventType === 'downloadFailed') { if (eventType === 'downloadFailed') {
const { const {
message, message
indexer
} = data; } = data;
return ( return (
@ -193,14 +192,6 @@ function HistoryDetails(props) {
null null
} }
{
indexer ? (
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
) : null}
{ {
message ? message ?
<DescriptionListItem <DescriptionListItem

View file

@ -26,5 +26,4 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 90px; width: 90px;
text-align: right;
} }

View file

@ -57,40 +57,30 @@ function QueueStatusCell(props) {
if (status === 'paused') { if (status === 'paused') {
iconName = icons.PAUSED; iconName = icons.PAUSED;
title = translate('Paused'); title = 'Paused';
} }
if (status === 'queued') { if (status === 'queued') {
iconName = icons.QUEUED; iconName = icons.QUEUED;
title = translate('Queued'); title = 'Queued';
} }
if (status === 'completed') { if (status === 'completed') {
iconName = icons.DOWNLOADED; iconName = icons.DOWNLOADED;
title = translate('Downloaded'); title = 'Downloaded';
if (trackedDownloadState === 'importBlocked') {
title += ` - ${translate('UnableToImportAutomatically')}`;
iconKind = kinds.WARNING;
}
if (trackedDownloadState === 'importFailed') {
title += ` - ${translate('ImportFailed', { sourceTitle })}`;
iconKind = kinds.WARNING;
}
if (trackedDownloadState === 'importPending') { if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`; title += ' - Waiting to Import';
iconKind = kinds.PURPLE; iconKind = kinds.PURPLE;
} }
if (trackedDownloadState === 'importing') { if (trackedDownloadState === 'importing') {
title += ` - ${translate('Importing')}`; title += ' - Importing';
iconKind = kinds.PURPLE; iconKind = kinds.PURPLE;
} }
if (trackedDownloadState === 'failedPending') { if (trackedDownloadState === 'failedPending') {
title += ` - ${translate('WaitingToProcess')}`; title += ' - Waiting to Process';
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
} }
} }
@ -101,38 +91,36 @@ function QueueStatusCell(props) {
if (status === 'delay') { if (status === 'delay') {
iconName = icons.PENDING; iconName = icons.PENDING;
title = translate('Pending'); title = 'Pending';
} }
if (status === 'downloadClientUnavailable') { if (status === 'downloadClientUnavailable') {
iconName = icons.PENDING; iconName = icons.PENDING;
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
title = translate('PendingDownloadClientUnavailable'); title = 'Pending - Download client is unavailable';
} }
if (status === 'failed') { if (status === 'failed') {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = translate('DownloadFailed'); title = 'Download failed';
} }
if (status === 'warning') { if (status === 'warning') {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
const warningMessage = title = `Download warning: ${errorMessage || 'check download client for more details'}`;
errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', { warningMessage });
} }
if (hasError) { if (hasError) {
if (status === 'completed') { if (status === 'completed') {
iconName = icons.DOWNLOAD; iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = translate('ImportFailed', { sourceTitle }); title = `Import failed: ${sourceTitle}`;
} else { } else {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = translate('DownloadFailed'); title = 'Download failed';
} }
} }

View file

@ -20,7 +20,6 @@ interface Album extends ModelBase {
monitored: boolean; monitored: boolean;
releaseDate: string; releaseDate: string;
statistics: Statistics; statistics: Statistics;
lastSearchTime?: string;
isSaving?: boolean; isSaving?: boolean;
} }

View file

@ -121,8 +121,6 @@
.releaseDate, .releaseDate,
.sizeOnDisk, .sizeOnDisk,
.albumType,
.secondaryTypes,
.qualityProfileName, .qualityProfileName,
.links, .links,
.tags { .tags {

View file

@ -3,7 +3,6 @@
interface CssExports { interface CssExports {
'albumNavigationButton': string; 'albumNavigationButton': string;
'albumNavigationButtons': string; 'albumNavigationButtons': string;
'albumType': string;
'alternateTitlesIconContainer': string; 'alternateTitlesIconContainer': string;
'backdrop': string; 'backdrop': string;
'backdropOverlay': string; 'backdropOverlay': string;
@ -21,7 +20,6 @@ interface CssExports {
'overview': string; 'overview': string;
'qualityProfileName': string; 'qualityProfileName': string;
'releaseDate': string; 'releaseDate': string;
'secondaryTypes': string;
'sizeOnDisk': string; 'sizeOnDisk': string;
'tags': string; 'tags': string;
'title': string; 'title': string;

View file

@ -192,7 +192,6 @@ class AlbumDetails extends Component {
duration, duration,
overview, overview,
albumType, albumType,
secondaryTypes,
statistics = {}, statistics = {},
monitored, monitored,
releaseDate, releaseDate,
@ -205,7 +204,6 @@ class AlbumDetails extends Component {
isFetching, isFetching,
isPopulated, isPopulated,
albumsError, albumsError,
tracksError,
trackFilesError, trackFilesError,
hasTrackFiles, hasTrackFiles,
shortDateFormat, shortDateFormat,
@ -398,11 +396,10 @@ class AlbumDetails extends Component {
<div className={styles.details}> <div className={styles.details}>
<div> <div>
{ {
duration ? !!duration &&
<span className={styles.duration}> <span className={styles.duration}>
{formatDuration(duration)} {formatDuration(duration)}
</span> : </span>
null
} }
<HeartRating <HeartRating
@ -421,15 +418,14 @@ class AlbumDetails extends Component {
title={translate('ReleaseDate')} title={translate('ReleaseDate')}
size={sizes.LARGE} size={sizes.LARGE}
> >
<div>
<Icon <Icon
name={icons.CALENDAR} name={icons.CALENDAR}
size={17} size={17}
/> />
<span className={styles.releaseDate}> <span className={styles.releaseDate}>
{moment(releaseDate).format(shortDateFormat)} {moment(releaseDate).format(shortDateFormat)}
</span> </span>
</div>
</Label> </Label>
<Tooltip <Tooltip
@ -438,15 +434,16 @@ class AlbumDetails extends Component {
className={styles.detailsLabel} className={styles.detailsLabel}
size={sizes.LARGE} size={sizes.LARGE}
> >
<div>
<Icon <Icon
name={icons.DRIVE} name={icons.DRIVE}
size={17} size={17}
/> />
<span className={styles.sizeOnDisk}> <span className={styles.sizeOnDisk}>
{formatBytes(sizeOnDisk)} {
formatBytes(sizeOnDisk || 0)
}
</span> </span>
</div>
</Label> </Label>
} }
tooltip={ tooltip={
@ -462,55 +459,32 @@ class AlbumDetails extends Component {
className={styles.detailsLabel} className={styles.detailsLabel}
size={sizes.LARGE} size={sizes.LARGE}
> >
<div>
<Icon <Icon
name={monitored ? icons.MONITORED : icons.UNMONITORED} name={monitored ? icons.MONITORED : icons.UNMONITORED}
size={17} size={17}
/> />
<span className={styles.qualityProfileName}> <span className={styles.qualityProfileName}>
{monitored ? translate('Monitored') : translate('Unmonitored')} {monitored ? translate('Monitored') : translate('Unmonitored')}
</span> </span>
</div>
</Label> </Label>
{ {
albumType ? !!albumType &&
<Label <Label
className={styles.detailsLabel} className={styles.detailsLabel}
title={translate('Type')} title={translate('Type')}
size={sizes.LARGE} size={sizes.LARGE}
> >
<div>
<Icon <Icon
name={icons.INFO} name={icons.INFO}
size={17} size={17}
/> />
<span className={styles.albumType}>
<span className={styles.qualityProfileName}>
{albumType} {albumType}
</span> </span>
</div> </Label>
</Label> :
null
}
{
secondaryTypes.length ?
<Label
className={styles.detailsLabel}
title={translate('SecondaryTypes')}
size={sizes.LARGE}
>
<div>
<Icon
name={icons.INFO}
size={17}
/>
<span className={styles.secondaryTypes}>
{secondaryTypes.join(', ')}
</span>
</div>
</Label> :
null
} }
<Tooltip <Tooltip
@ -519,15 +493,14 @@ class AlbumDetails extends Component {
className={styles.detailsLabel} className={styles.detailsLabel}
size={sizes.LARGE} size={sizes.LARGE}
> >
<div>
<Icon <Icon
name={icons.EXTERNAL_LINK} name={icons.EXTERNAL_LINK}
size={17} size={17}
/> />
<span className={styles.links}> <span className={styles.links}>
{translate('Links')} {translate('Links')}
</span> </span>
</div>
</Label> </Label>
} }
tooltip={ tooltip={
@ -553,9 +526,8 @@ class AlbumDetails extends Component {
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
{ {
!isPopulated && !albumsError && !tracksError && !trackFilesError ? !isPopulated && !albumsError && !trackFilesError &&
<LoadingIndicator /> : <LoadingIndicator />
null
} }
{ {
@ -566,14 +538,6 @@ class AlbumDetails extends Component {
null null
} }
{
!isFetching && tracksError ?
<Alert kind={kinds.DANGER}>
{translate('TracksLoadError')}
</Alert> :
null
}
{ {
!isFetching && trackFilesError ? !isFetching && trackFilesError ?
<Alert kind={kinds.DANGER}> <Alert kind={kinds.DANGER}>
@ -602,14 +566,6 @@ class AlbumDetails extends Component {
</div> </div>
} }
{
isPopulated && !media.length ?
<Alert kind={kinds.WARNING}>
{translate('NoMediumInformation')}
</Alert> :
null
}
</div> </div>
<OrganizePreviewModalConnector <OrganizePreviewModalConnector
@ -676,7 +632,6 @@ AlbumDetails.propTypes = {
duration: PropTypes.number, duration: PropTypes.number,
overview: PropTypes.string, overview: PropTypes.string,
albumType: PropTypes.string.isRequired, albumType: PropTypes.string.isRequired,
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
statistics: PropTypes.object.isRequired, statistics: PropTypes.object.isRequired,
releaseDate: PropTypes.string.isRequired, releaseDate: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
@ -703,8 +658,6 @@ AlbumDetails.propTypes = {
}; };
AlbumDetails.defaultProps = { AlbumDetails.defaultProps = {
secondaryTypes: [],
statistics: {},
isSaving: false isSaving: false
}; };

View file

@ -29,7 +29,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs'; import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status'; import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks'; import Tasks from 'System/Tasks/Tasks';
import Updates from 'System/Updates/Updates'; import UpdatesConnector from 'System/Updates/UpdatesConnector';
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector'; import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
@ -248,7 +248,7 @@ function AppRoutes(props) {
<Route <Route
path="/system/updates" path="/system/updates"
component={Updates} component={UpdatesConnector}
/> />
<Route <Route

View file

@ -44,7 +44,6 @@ export interface CustomFilter {
} }
export interface AppSectionState { export interface AppSectionState {
version: string;
dimensions: { dimensions: {
isSmallScreen: boolean; isSmallScreen: boolean;
width: number; width: number;

View file

@ -4,7 +4,6 @@ import AppSectionState, {
AppSectionSaveState, AppSectionSaveState,
AppSectionSchemaState, AppSectionSchemaState,
} from 'App/State/AppSectionState'; } from 'App/State/AppSectionState';
import CustomFormat from 'typings/CustomFormat';
import DownloadClient from 'typings/DownloadClient'; import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList'; import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer'; import Indexer from 'typings/Indexer';
@ -13,16 +12,13 @@ import MetadataProfile from 'typings/MetadataProfile';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile'; import QualityProfile from 'typings/QualityProfile';
import RootFolder from 'typings/RootFolder'; import RootFolder from 'typings/RootFolder';
import General from 'typings/Settings/General'; import { UiSettings } from 'typings/UiSettings';
import UiSettings from 'typings/Settings/UiSettings';
export interface DownloadClientAppState export interface DownloadClientAppState
extends AppSectionState<DownloadClient>, extends AppSectionState<DownloadClient>,
AppSectionDeleteState, AppSectionDeleteState,
AppSectionSaveState {} AppSectionSaveState {}
export type GeneralAppState = AppSectionItemState<General>;
export interface ImportListAppState export interface ImportListAppState
extends AppSectionState<ImportList>, extends AppSectionState<ImportList>,
AppSectionDeleteState, AppSectionDeleteState,
@ -45,11 +41,6 @@ export interface MetadataProfilesAppState
extends AppSectionState<MetadataProfile>, extends AppSectionState<MetadataProfile>,
AppSectionSchemaState<MetadataProfile> {} AppSectionSchemaState<MetadataProfile> {}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface RootFolderAppState export interface RootFolderAppState
extends AppSectionState<RootFolder>, extends AppSectionState<RootFolder>,
AppSectionDeleteState, AppSectionDeleteState,
@ -59,10 +50,7 @@ export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>; export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState { interface SettingsAppState {
advancedSettings: boolean;
customFormats: CustomFormatAppState;
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
general: GeneralAppState;
importLists: ImportListAppState; importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState; indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState; indexers: IndexerAppState;

View file

@ -1,12 +1,9 @@
import SystemStatus from 'typings/SystemStatus'; import SystemStatus from 'typings/SystemStatus';
import Update from 'typings/Update'; import { AppSectionItemState } from './AppSectionState';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>; export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState { interface SystemAppState {
updates: UpdateAppState;
status: SystemStatusAppState; status: SystemStatusAppState;
} }

View file

@ -149,7 +149,9 @@ class AlbumRow extends Component {
if (name === 'secondaryTypes') { if (name === 'secondaryTypes') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
{secondaryTypes.join(', ')} {
secondaryTypes
}
</TableRowCell> </TableRowCell>
); );
} }

View file

@ -15,7 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditArtistModalContent.css'; import styles from './EditArtistModalContent.css';
@ -93,7 +93,7 @@ class EditArtistModalContent extends Component {
<ModalBody> <ModalBody>
<Form {...otherProps}> <Form {...otherProps}>
<FormGroup size={sizes.MEDIUM}> <FormGroup>
<FormLabel> <FormLabel>
{translate('Monitored')} {translate('Monitored')}
</FormLabel> </FormLabel>
@ -107,10 +107,9 @@ class EditArtistModalContent extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup size={sizes.MEDIUM}> <FormGroup>
<FormLabel> <FormLabel>
{translate('MonitorNewItems')} {translate('MonitorNewItems')}
<Popover <Popover
anchor={ anchor={
<Icon <Icon
@ -133,7 +132,7 @@ class EditArtistModalContent extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup size={sizes.MEDIUM}> <FormGroup>
<FormLabel> <FormLabel>
{translate('QualityProfile')} {translate('QualityProfile')}
</FormLabel> </FormLabel>
@ -147,10 +146,10 @@ class EditArtistModalContent extends Component {
</FormGroup> </FormGroup>
{ {
showMetadataProfile ? showMetadataProfile &&
<FormGroup size={sizes.MEDIUM}> <FormGroup>
<FormLabel> <FormLabel>
{translate('MetadataProfile')} Metadata Profile
<Popover <Popover
anchor={ anchor={
@ -174,11 +173,10 @@ class EditArtistModalContent extends Component {
{...metadataProfileId} {...metadataProfileId}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> : </FormGroup>
null
} }
<FormGroup size={sizes.MEDIUM}> <FormGroup>
<FormLabel> <FormLabel>
{translate('Path')} {translate('Path')}
</FormLabel> </FormLabel>
@ -191,7 +189,7 @@ class EditArtistModalContent extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup size={sizes.MEDIUM}> <FormGroup>
<FormLabel> <FormLabel>
{translate('Tags')} {translate('Tags')}
</FormLabel> </FormLabel>
@ -211,7 +209,7 @@ class EditArtistModalContent extends Component {
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onDeleteArtistPress} onPress={onDeleteArtistPress}
> >
{translate('Delete')} Delete
</Button> </Button>
<Button <Button

View file

@ -79,7 +79,7 @@ function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('LastAlbum')} {translate('Last Album')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem

View file

@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import QualityProfile from 'typings/QualityProfile'; import QualityProfile from 'typings/QualityProfile';
import UiSettings from 'typings/Settings/UiSettings'; import { UiSettings } from 'typings/UiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';

View file

@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
value={tags} value={tags}
helpText={translate('ICalTagsArtistHelpText')} helpText={translate('TagsHelpText')}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
</FormGroup> </FormGroup>

View file

@ -3,8 +3,8 @@ import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import PathInput from 'Components/Form/PathInput'; import PathInput from 'Components/Form/PathInput';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning} className={styles.mappedDrivesWarning}
kind={kinds.WARNING} kind={kinds.WARNING}
> >
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/lidarr/faq#why-cant-lidarr-see-my-files-on-a-remote-server' })} /> Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/lidarr/faq">FAQ</Link> for more information.
</Alert> </Alert>
} }

View file

@ -11,11 +11,11 @@ import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue'; import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MetadataProfileFilterBuilderRowValue from './MetadataProfileFilterBuilderRowValue'; import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue'; import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css'; import styles from './FilterBuilderRow.css';
@ -68,7 +68,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
return IndexerFilterBuilderRowValueConnector; return IndexerFilterBuilderRowValueConnector;
case filterBuilderValueTypes.METADATA_PROFILE: case filterBuilderValueTypes.METADATA_PROFILE:
return MetadataProfileFilterBuilderRowValue; return MetadataProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.MONITOR_NEW_ITEMS: case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
return MonitorNewItemsFilterBuilderRowValue; return MonitorNewItemsFilterBuilderRowValue;
@ -80,7 +80,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
return QualityFilterBuilderRowValueConnector; return QualityFilterBuilderRowValueConnector;
case filterBuilderValueTypes.QUALITY_PROFILE: case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValue; return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.ARTIST: case filterBuilderValueTypes.ARTIST:
return ArtistFilterBuilderRowValue; return ArtistFilterBuilderRowValue;

View file

@ -25,7 +25,7 @@ const EVENT_TYPE_OPTIONS = [
{ {
id: 7, id: 7,
get name() { get name() {
return translate('ImportCompleteFailed'); return translate('ImportFailed');
}, },
}, },
{ {

View file

@ -1,30 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMetadataProfilesSelector() {
return createSelector(
(state: AppState) => state.settings.metadataProfiles.items,
(metadataProfiles) => {
return metadataProfiles;
}
);
}
function MetadataProfileFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
const metadataProfiles = useSelector(createMetadataProfilesSelector());
const tagList = metadataProfiles
.map(({ id, name }) => ({ id, name }))
.sort(sortByProp('name'));
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}
export default MetadataProfileFilterBuilderRowValue;

View file

@ -0,0 +1,28 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.metadataProfiles,
(metadataProfiles) => {
const tagList = metadataProfiles.items.map((metadataProfile) => {
const {
id,
name
} = metadataProfile;
return {
id,
name
};
});
return {
tagList
};
}
);
}
export default connect(createMapStateToProps)(FilterBuilderRowValue);

View file

@ -1,30 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createQualityProfilesSelector() {
return createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfiles) => {
return qualityProfiles;
}
);
}
function QualityProfileFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
const qualityProfiles = useSelector(createQualityProfilesSelector());
const tagList = qualityProfiles
.map(({ id, name }) => ({ id, name }))
.sort(sortByProp('name'));
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}
export default QualityProfileFilterBuilderRowValue;

View file

@ -0,0 +1,28 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles,
(qualityProfiles) => {
const tagList = qualityProfiles.items.map((qualityProfile) => {
const {
id,
name
} = qualityProfile;
return {
id,
name
};
});
return {
tagList
};
}
);
}
export default connect(createMapStateToProps)(FilterBuilderRowValue);

View file

@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput'; import TextInput from './TextInput';
import styles from './EnhancedSelectInput.css'; import styles from './EnhancedSelectInput.css';
const MINIMUM_DISTANCE_FROM_EDGE = 10;
function isArrowKey(keyCode) { function isArrowKey(keyCode) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
} }
@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
// Listeners // Listeners
onComputeMaxHeight = (data) => { onComputeMaxHeight = (data) => {
const {
top,
bottom
} = data.offsets.reference;
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; if ((/^botton/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
return data; return data;
}; };
@ -450,10 +457,6 @@ class EnhancedSelectInput extends Component {
order: 851, order: 851,
enabled: true, enabled: true,
fn: this.onComputeMaxHeight fn: this.onComputeMaxHeight
},
preventOverflow: {
enabled: true,
boundariesElement: 'viewport'
} }
}} }}
> >

View file

@ -49,12 +49,12 @@ function getComponent(type) {
case inputTypes.DEVICE: case inputTypes.DEVICE:
return DeviceInputConnector; return DeviceInputConnector;
case inputTypes.KEY_VALUE_LIST:
return KeyValueListInput;
case inputTypes.PLAYLIST: case inputTypes.PLAYLIST:
return PlaylistInputConnector; return PlaylistInputConnector;
case inputTypes.KEY_VALUE_LIST:
return KeyValueListInput;
case inputTypes.MONITOR_ALBUMS_SELECT: case inputTypes.MONITOR_ALBUMS_SELECT:
return MonitorAlbumsSelectInput; return MonitorAlbumsSelectInput;

View file

@ -0,0 +1,156 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import KeyValueListInputItem from './KeyValueListInputItem';
import styles from './KeyValueListInput.css';
class KeyValueListInput extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isFocused: false
};
}
//
// Listeners
onItemChange = (index, itemValue) => {
const {
name,
value,
onChange
} = this.props;
const newValue = [...value];
if (index == null) {
newValue.push(itemValue);
} else {
newValue.splice(index, 1, itemValue);
}
onChange({
name,
value: newValue
});
};
onRemoveItem = (index) => {
const {
name,
value,
onChange
} = this.props;
const newValue = [...value];
newValue.splice(index, 1);
onChange({
name,
value: newValue
});
};
onFocus = () => {
this.setState({
isFocused: true
});
};
onBlur = () => {
this.setState({
isFocused: false
});
const {
name,
value,
onChange
} = this.props;
const newValue = value.reduce((acc, v) => {
if (v.key || v.value) {
acc.push(v);
}
return acc;
}, []);
if (newValue.length !== value.length) {
onChange({
name,
value: newValue
});
}
};
//
// Render
render() {
const {
className,
value,
keyPlaceholder,
valuePlaceholder,
hasError,
hasWarning
} = this.props;
const { isFocused } = this.state;
return (
<div className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{
[...value, { key: '', value: '' }].map((v, index) => {
return (
<KeyValueListInputItem
key={index}
index={index}
keyValue={v.key}
value={v.value}
keyPlaceholder={keyPlaceholder}
valuePlaceholder={valuePlaceholder}
isNew={index === value.length}
onChange={this.onItemChange}
onRemove={this.onRemoveItem}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
);
})
}
</div>
);
}
}
KeyValueListInput.propTypes = {
className: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.object).isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
keyPlaceholder: PropTypes.string,
valuePlaceholder: PropTypes.string,
onChange: PropTypes.func.isRequired
};
KeyValueListInput.defaultProps = {
className: styles.inputContainer,
value: []
};
export default KeyValueListInput;

View file

@ -1,104 +0,0 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { InputOnChange } from 'typings/inputs';
import KeyValueListInputItem from './KeyValueListInputItem';
import styles from './KeyValueListInput.css';
interface KeyValue {
key: string;
value: string;
}
export interface KeyValueListInputProps {
className?: string;
name: string;
value: KeyValue[];
hasError?: boolean;
hasWarning?: boolean;
keyPlaceholder?: string;
valuePlaceholder?: string;
onChange: InputOnChange<KeyValue[]>;
}
function KeyValueListInput({
className = styles.inputContainer,
name,
value = [],
hasError = false,
hasWarning = false,
keyPlaceholder,
valuePlaceholder,
onChange,
}: KeyValueListInputProps): JSX.Element {
const [isFocused, setIsFocused] = useState(false);
const handleItemChange = useCallback(
(index: number | null, itemValue: KeyValue) => {
const newValue = [...value];
if (index === null) {
newValue.push(itemValue);
} else {
newValue.splice(index, 1, itemValue);
}
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const handleRemoveItem = useCallback(
(index: number) => {
const newValue = [...value];
newValue.splice(index, 1);
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const onFocus = useCallback(() => setIsFocused(true), []);
const onBlur = useCallback(() => {
setIsFocused(false);
const newValue = value.reduce((acc: KeyValue[], v) => {
if (v.key || v.value) {
acc.push(v);
}
return acc;
}, []);
if (newValue.length !== value.length) {
onChange({ name, value: newValue });
}
}, [value, name, onChange]);
return (
<div
className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{[...value, { key: '', value: '' }].map((v, index) => (
<KeyValueListInputItem
key={index}
index={index}
keyValue={v.key}
value={v.value}
keyPlaceholder={keyPlaceholder}
valuePlaceholder={valuePlaceholder}
isNew={index === value.length}
onChange={handleItemChange}
onRemove={handleRemoveItem}
onFocus={onFocus}
onBlur={onBlur}
/>
))}
</div>
);
}
export default KeyValueListInput;

View file

@ -5,19 +5,13 @@
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
border-bottom: 0;
} }
} }
.keyInputWrapper { .inputWrapper {
flex: 1 0 0; flex: 1 0 0;
} }
.valueInputWrapper {
flex: 1 0 0;
min-width: 40px;
}
.buttonWrapper { .buttonWrapper {
flex: 0 0 22px; flex: 0 0 22px;
} }
@ -26,10 +20,6 @@
.valueInput { .valueInput {
width: 100%; width: 100%;
border: none; border: none;
background-color: transparent; background-color: var(--inputBackgroundColor);
color: var(--textColor); color: var(--textColor);
&::placeholder {
color: var(--helpTextColor);
}
} }

View file

@ -2,11 +2,10 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'buttonWrapper': string; 'buttonWrapper': string;
'inputWrapper': string;
'itemContainer': string; 'itemContainer': string;
'keyInput': string; 'keyInput': string;
'keyInputWrapper': string;
'valueInput': string; 'valueInput': string;
'valueInputWrapper': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View file

@ -0,0 +1,124 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import TextInput from './TextInput';
import styles from './KeyValueListInputItem.css';
class KeyValueListInputItem extends Component {
//
// Listeners
onKeyChange = ({ value: keyValue }) => {
const {
index,
value,
onChange
} = this.props;
onChange(index, { key: keyValue, value });
};
onValueChange = ({ value }) => {
// TODO: Validate here or validate at a lower level component
const {
index,
keyValue,
onChange
} = this.props;
onChange(index, { key: keyValue, value });
};
onRemovePress = () => {
const {
index,
onRemove
} = this.props;
onRemove(index);
};
onFocus = () => {
this.props.onFocus();
};
onBlur = () => {
this.props.onBlur();
};
//
// Render
render() {
const {
keyValue,
value,
keyPlaceholder,
valuePlaceholder,
isNew
} = this.props;
return (
<div className={styles.itemContainer}>
<div className={styles.inputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={this.onKeyChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<div className={styles.inputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={this.onValueChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<div className={styles.buttonWrapper}>
{
isNew ?
null :
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={this.onRemovePress}
/>
}
</div>
</div>
);
}
}
KeyValueListInputItem.propTypes = {
index: PropTypes.number,
keyValue: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
keyPlaceholder: PropTypes.string.isRequired,
valuePlaceholder: PropTypes.string.isRequired,
isNew: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired
};
KeyValueListInputItem.defaultProps = {
keyPlaceholder: 'Key',
valuePlaceholder: 'Value'
};
export default KeyValueListInputItem;

View file

@ -1,89 +0,0 @@
import React, { useCallback } from 'react';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import TextInput from './TextInput';
import styles from './KeyValueListInputItem.css';
interface KeyValueListInputItemProps {
index: number;
keyValue: string;
value: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
isNew: boolean;
onChange: (index: number, itemValue: { key: string; value: string }) => void;
onRemove: (index: number) => void;
onFocus: () => void;
onBlur: () => void;
}
function KeyValueListInputItem({
index,
keyValue,
value,
keyPlaceholder = 'Key',
valuePlaceholder = 'Value',
isNew,
onChange,
onRemove,
onFocus,
onBlur,
}: KeyValueListInputItemProps): JSX.Element {
const handleKeyChange = useCallback(
({ value: keyValue }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, value, onChange]
);
const handleValueChange = useCallback(
({ value }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, keyValue, onChange]
);
const handleRemovePress = useCallback(() => {
onRemove(index);
}, [index, onRemove]);
return (
<div className={styles.itemContainer}>
<div className={styles.keyInputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={handleKeyChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.valueInputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={handleValueChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.buttonWrapper}>
{isNew ? null : (
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={handleRemovePress}
/>
)}
</div>
</div>
);
}
export default KeyValueListInputItem;

View file

@ -14,8 +14,6 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.CHECK; return inputTypes.CHECK;
case 'device': case 'device':
return inputTypes.DEVICE; return inputTypes.DEVICE;
case 'keyValueList':
return inputTypes.KEY_VALUE_LIST;
case 'playlist': case 'playlist':
return inputTypes.PLAYLIST; return inputTypes.PLAYLIST;
case 'password': case 'password':

View file

@ -83,6 +83,13 @@
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointMedium) {
.modal.small,
.modal.medium {
width: 90%;
}
}
@media only screen and (max-width: $breakpointSmall) {
.modalContainer { .modalContainer {
position: fixed; position: fixed;
} }

View file

@ -3,9 +3,9 @@
padding: 0; padding: 0;
font-size: inherit; font-size: inherit;
}
&.isDisabled { .isDisabled {
color: var(--disabledColor); color: var(--disabledColor);
cursor: not-allowed; cursor: not-allowed;
}
} }

View file

@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import ArtistSearchInputConnector from './ArtistSearchInputConnector'; import ArtistSearchInputConnector from './ArtistSearchInputConnector';
import KeyboardShortcutsModal from './KeyboardShortcutsModal'; import KeyboardShortcutsModal from './KeyboardShortcutsModal';
import PageHeaderActionsMenu from './PageHeaderActionsMenu'; import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
import styles from './PageHeader.css'; import styles from './PageHeader.css';
class PageHeader extends Component { class PageHeader extends Component {
@ -83,7 +83,6 @@ class PageHeader extends Component {
size={14} size={14}
title={translate('Donate')} title={translate('Donate')}
/> />
<IconButton <IconButton
className={styles.translation} className={styles.translation}
title={translate('SuggestTranslationChange')} title={translate('SuggestTranslationChange')}
@ -91,8 +90,7 @@ class PageHeader extends Component {
to="https://translate.servarr.com/projects/servarr/lidarr/" to="https://translate.servarr.com/projects/servarr/lidarr/"
size={24} size={24}
/> />
<PageHeaderActionsMenuConnector
<PageHeaderActionsMenu
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal} onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
/> />
</div> </div>

View file

@ -0,0 +1,90 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './PageHeaderActionsMenu.css';
function PageHeaderActionsMenu(props) {
const {
formsAuth,
onKeyboardShortcutsPress,
onRestartPress,
onShutdownPress
} = props;
return (
<div>
<Menu alignMenu={align.RIGHT}>
<MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon
name={icons.INTERACTIVE}
title={translate('Menu')}
/>
</MenuButton>
<MenuContent>
<MenuItem onPress={onKeyboardShortcutsPress}>
<Icon
className={styles.itemIcon}
name={icons.KEYBOARD}
/>
{translate('KeyboardShortcuts')}
</MenuItem>
<MenuItemSeparator />
<MenuItem onPress={onRestartPress}>
<Icon
className={styles.itemIcon}
name={icons.RESTART}
/>
{translate('Restart')}
</MenuItem>
<MenuItem onPress={onShutdownPress}>
<Icon
className={styles.itemIcon}
name={icons.SHUTDOWN}
kind={kinds.DANGER}
/>
Shutdown
</MenuItem>
{
formsAuth &&
<div className={styles.separator} />
}
{
formsAuth &&
<MenuItem
to={`${window.Lidarr.urlBase}/logout`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
{translate('Logout')}
</MenuItem>
}
</MenuContent>
</Menu>
</div>
);
}
PageHeaderActionsMenu.propTypes = {
formsAuth: PropTypes.bool.isRequired,
onKeyboardShortcutsPress: PropTypes.func.isRequired,
onRestartPress: PropTypes.func.isRequired,
onShutdownPress: PropTypes.func.isRequired
};
export default PageHeaderActionsMenu;

View file

@ -1,87 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Icon from 'Components/Icon';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
import { align, icons, kinds } from 'Helpers/Props';
import { restart, shutdown } from 'Store/Actions/systemActions';
import translate from 'Utilities/String/translate';
import styles from './PageHeaderActionsMenu.css';
interface PageHeaderActionsMenuProps {
onKeyboardShortcutsPress(): void;
}
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
const { onKeyboardShortcutsPress } = props;
const dispatch = useDispatch();
const { authentication, isDocker } = useSelector(
(state: AppState) => state.system.status.item
);
const formsAuth = authentication === 'forms';
const handleRestartPress = useCallback(() => {
dispatch(restart());
}, [dispatch]);
const handleShutdownPress = useCallback(() => {
dispatch(shutdown());
}, [dispatch]);
return (
<div>
<Menu alignMenu={align.RIGHT}>
<MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
</MenuButton>
<MenuContent>
<MenuItem onPress={onKeyboardShortcutsPress}>
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
{translate('KeyboardShortcuts')}
</MenuItem>
{isDocker ? null : (
<>
<MenuItemSeparator />
<MenuItem onPress={handleRestartPress}>
<Icon className={styles.itemIcon} name={icons.RESTART} />
{translate('Restart')}
</MenuItem>
<MenuItem onPress={handleShutdownPress}>
<Icon
className={styles.itemIcon}
name={icons.SHUTDOWN}
kind={kinds.DANGER}
/>
{translate('Shutdown')}
</MenuItem>
</>
)}
{formsAuth ? (
<>
<MenuItemSeparator />
<MenuItem to={`${window.Lidarr.urlBase}/logout`} noRouter={true}>
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
{translate('Logout')}
</MenuItem>
</>
) : null}
</MenuContent>
</Menu>
</div>
);
}
export default PageHeaderActionsMenu;

View file

@ -0,0 +1,56 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { restart, shutdown } from 'Store/Actions/systemActions';
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
function createMapStateToProps() {
return createSelector(
(state) => state.system.status,
(status) => {
return {
formsAuth: status.item.authentication === 'forms'
};
}
);
}
const mapDispatchToProps = {
restart,
shutdown
};
class PageHeaderActionsMenuConnector extends Component {
//
// Listeners
onRestartPress = () => {
this.props.restart();
};
onShutdownPress = () => {
this.props.shutdown();
};
//
// Render
render() {
return (
<PageHeaderActionsMenu
{...this.props}
onRestartPress={this.onRestartPress}
onShutdownPress={this.onShutdownPress}
/>
);
}
}
PageHeaderActionsMenuConnector.propTypes = {
restart: PropTypes.func.isRequired,
shutdown: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);

View file

@ -172,7 +172,7 @@ class SignalRConnector extends Component {
const status = resource.status; const status = resource.status;
// Both successful and failed commands need to be // Both successful and failed commands need to be
// completed, otherwise they spin until they time out. // completed, otherwise they spin until they timeout.
if (status === 'completed' || status === 'failed') { if (status === 'completed' || status === 'failed') {
this.props.dispatchFinishCommand(resource); this.props.dispatchFinishCommand(resource);
@ -224,58 +224,10 @@ class SignalRConnector extends Component {
repopulatePage('trackFileUpdated'); repopulatePage('trackFileUpdated');
}; };
handleDownloadclient = ({ action, resource }) => {
const section = 'settings.downloadClients';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleHealth = () => { handleHealth = () => {
this.props.dispatchFetchHealth(); this.props.dispatchFetchHealth();
}; };
handleImportlist = ({ action, resource }) => {
const section = 'settings.importLists';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleIndexer = ({ action, resource }) => {
const section = 'settings.indexers';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleMetadata = ({ action, resource }) => {
const section = 'settings.metadata';
if (action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
}
};
handleNotification = ({ action, resource }) => {
const section = 'settings.notifications';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleArtist = (body) => { handleArtist = (body) => {
const action = body.action; const action = body.action;
const section = 'artist'; const section = 'artist';

View file

@ -4,7 +4,7 @@
line-height: 1.52857143; line-height: 1.52857143;
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointSmall) {
.cell { .cell {
white-space: nowrap; white-space: nowrap;
} }

View file

@ -7,7 +7,7 @@
white-space: nowrap; white-space: nowrap;
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointSmall) {
.cell { .cell {
white-space: nowrap; white-space: nowrap;
} }

View file

@ -10,7 +10,7 @@
border-collapse: collapse; border-collapse: collapse;
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointSmall) {
.tableContainer { .tableContainer {
min-width: 100%; min-width: 100%;
width: fit-content; width: fit-content;

View file

@ -9,7 +9,7 @@
margin-left: 10px; margin-left: 10px;
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointSmall) {
.headerCell { .headerCell {
white-space: nowrap; white-space: nowrap;
} }

View file

@ -60,7 +60,7 @@
height: 25px; height: 25px;
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointSmall) {
.pager { .pager {
flex-wrap: wrap; flex-wrap: wrap;
} }

View file

@ -9,7 +9,7 @@
margin-left: 10px; margin-left: 10px;
} }
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointSmall) {
.headerCell { .headerCell {
white-space: nowrap; white-space: nowrap;
} }

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
<TileColor>#00ccff</TileColor>
</tile>
</msapplication>
</browserconfig>

View file

@ -0,0 +1,19 @@
{
"name": "Lidarr",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "../../../../",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "standalone"
}

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="__URL_BASE__/Content/Images/Icons/mstile-150x150.png" />
<TileColor>
#00ccff
</TileColor>
</tile>
</msapplication>
</browserconfig>

View file

@ -1,19 +0,0 @@
{
"name": "__INSTANCE_NAME__",
"icons": [
{
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "__URL_BASE__/",
"theme_color": "#3a3f51",
"background_color": "#3a3f51",
"display": "standalone"
}

View file

@ -2,8 +2,8 @@ export const AUTO_COMPLETE = 'autoComplete';
export const CAPTCHA = 'captcha'; export const CAPTCHA = 'captcha';
export const CHECK = 'check'; export const CHECK = 'check';
export const DEVICE = 'device'; export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList';
export const PLAYLIST = 'playlist'; export const PLAYLIST = 'playlist';
export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect'; export const MONITOR_ALBUMS_SELECT = 'monitorAlbumsSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float'; export const FLOAT = 'float';
@ -34,8 +34,8 @@ export const all = [
CAPTCHA, CAPTCHA,
CHECK, CHECK,
DEVICE, DEVICE,
KEY_VALUE_LIST,
PLAYLIST, PLAYLIST,
KEY_VALUE_LIST,
MONITOR_ALBUMS_SELECT, MONITOR_ALBUMS_SELECT,
MONITOR_NEW_ITEMS_SELECT, MONITOR_NEW_ITEMS_SELECT,
FLOAT, FLOAT,

View file

@ -11,7 +11,6 @@ import Scroller from 'Components/Scroller/Scroller';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import SelectAlbumRow from './SelectAlbumRow'; import SelectAlbumRow from './SelectAlbumRow';
import styles from './SelectAlbumModalContent.css'; import styles from './SelectAlbumModalContent.css';
@ -20,7 +19,6 @@ const columns = [
{ {
name: 'title', name: 'title',
label: () => translate('AlbumTitle'), label: () => translate('AlbumTitle'),
isSortable: true,
isVisible: true isVisible: true
}, },
{ {
@ -31,7 +29,6 @@ const columns = [
{ {
name: 'releaseDate', name: 'releaseDate',
label: () => translate('ReleaseDate'), label: () => translate('ReleaseDate'),
isSortable: true,
isVisible: true isVisible: true
}, },
{ {
@ -66,22 +63,16 @@ class SelectAlbumModalContent extends Component {
render() { render() {
const { const {
isFetching,
isPopulated,
error,
items, items,
sortKey,
sortDirection,
onSortPress,
onAlbumSelect, onAlbumSelect,
onModalClose onModalClose,
isFetching,
...otherProps
} = this.props; } = this.props;
const filter = this.state.filter; const filter = this.state.filter;
const filterLower = filter.toLowerCase(); const filterLower = filter.toLowerCase();
const errorMessage = getErrorMessage(error, 'Unable to load albums');
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
@ -92,14 +83,10 @@ class SelectAlbumModalContent extends Component {
className={styles.modalBody} className={styles.modalBody}
scrollDirection={scrollDirections.NONE} scrollDirection={scrollDirections.NONE}
> >
<Scroller {
className={styles.scroller} isFetching &&
autoFocus={false} <LoadingIndicator />
> }
{isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null}
<TextInput <TextInput
className={styles.filterInput} className={styles.filterInput}
placeholder={translate('FilterAlbumPlaceholder')} placeholder={translate('FilterAlbumPlaceholder')}
@ -109,12 +96,14 @@ class SelectAlbumModalContent extends Component {
onChange={this.onFilterChange} onChange={this.onFilterChange}
/> />
{isPopulated && !!items.length ? ( <Scroller
className={styles.scroller}
autoFocus={false}
>
{
<Table <Table
columns={columns} columns={columns}
sortKey={sortKey} {...otherProps}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{ {
@ -133,7 +122,7 @@ class SelectAlbumModalContent extends Component {
} }
</TableBody> </TableBody>
</Table> </Table>
) : null} }
</Scroller> </Scroller>
</ModalBody> </ModalBody>
@ -148,13 +137,8 @@ class SelectAlbumModalContent extends Component {
} }
SelectAlbumModalContent.propTypes = { SelectAlbumModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, isFetching: PropTypes.bool.isRequired,
sortDirection: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
onAlbumSelect: PropTypes.func.isRequired, onAlbumSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View file

@ -3,14 +3,18 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearAlbums, fetchAlbums, setAlbumsSort } from 'Store/Actions/albumSelectionActions'; import {
import { saveInteractiveImportItem, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; clearInteractiveImportAlbums,
fetchInteractiveImportAlbums,
saveInteractiveImportItem,
setInteractiveImportAlbumsSort,
updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import SelectAlbumModalContent from './SelectAlbumModalContent'; import SelectAlbumModalContent from './SelectAlbumModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createClientSideCollectionSelector('albumSelection'), createClientSideCollectionSelector('interactiveImport.albums'),
(albums) => { (albums) => {
return albums; return albums;
} }
@ -18,9 +22,9 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchAlbums, fetchInteractiveImportAlbums,
setAlbumsSort, setInteractiveImportAlbumsSort,
clearAlbums, clearInteractiveImportAlbums,
updateInteractiveImportItem, updateInteractiveImportItem,
saveInteractiveImportItem saveInteractiveImportItem
}; };
@ -35,20 +39,20 @@ class SelectAlbumModalContentConnector extends Component {
artistId artistId
} = this.props; } = this.props;
this.props.fetchAlbums({ artistId }); this.props.fetchInteractiveImportAlbums({ artistId });
} }
componentWillUnmount() { componentWillUnmount() {
// This clears the albums for the queue and hides the queue // This clears the albums for the queue and hides the queue
// We'll need another place to store albums for manual import // We'll need another place to store albums for manual import
this.props.clearAlbums(); this.props.clearInteractiveImportAlbums();
} }
// //
// Listeners // Listeners
onSortPress = (sortKey, sortDirection) => { onSortPress = (sortKey, sortDirection) => {
this.props.setAlbumsSort({ sortKey, sortDirection }); this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
}; };
onAlbumSelect = (albumId) => { onAlbumSelect = (albumId) => {
@ -78,7 +82,6 @@ class SelectAlbumModalContentConnector extends Component {
return ( return (
<SelectAlbumModalContent <SelectAlbumModalContent
{...this.props} {...this.props}
onSortPress={this.onSortPress}
onAlbumSelect={this.onAlbumSelect} onAlbumSelect={this.onAlbumSelect}
/> />
); );
@ -89,9 +92,9 @@ SelectAlbumModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired, ids: PropTypes.arrayOf(PropTypes.number).isRequired,
artistId: PropTypes.number.isRequired, artistId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchAlbums: PropTypes.func.isRequired, fetchInteractiveImportAlbums: PropTypes.func.isRequired,
setAlbumsSort: PropTypes.func.isRequired, setInteractiveImportAlbumsSort: PropTypes.func.isRequired,
clearAlbums: PropTypes.func.isRequired, clearInteractiveImportAlbums: PropTypes.func.isRequired,
saveInteractiveImportItem: PropTypes.func.isRequired, saveInteractiveImportItem: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired, updateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired

View file

@ -18,17 +18,12 @@
.leftButtons, .leftButtons,
.rightButtons { .rightButtons {
display: flex; display: flex;
flex: 1 0 50%;
flex-wrap: wrap; flex-wrap: wrap;
min-width: 0;
}
.leftButtons {
flex: 0 1 auto;
} }
.rightButtons { .rightButtons {
justify-content: flex-end; justify-content: flex-end;
flex: 1 1 50%;
} }
.importMode, .importMode,
@ -36,7 +31,6 @@
composes: select from '~Components/Form/SelectInput.css'; composes: select from '~Components/Form/SelectInput.css';
margin-right: 10px; margin-right: 10px;
max-width: 100%;
width: auto; width: auto;
} }
@ -49,12 +43,10 @@
.leftButtons, .leftButtons,
.rightButtons { .rightButtons {
flex-direction: column; flex-direction: column;
gap: 3px;
} }
.leftButtons { .leftButtons {
align-items: flex-start; align-items: flex-start;
max-width: fit-content;
} }
.rightButtons { .rightButtons {

View file

@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
@ -131,8 +130,7 @@ class AddNewItem extends Component {
<div className={styles.helpText}> <div className={styles.helpText}>
{translate('FailedLoadingSearchResults')} {translate('FailedLoadingSearchResults')}
</div> </div>
<div>{getErrorMessage(error)}</div>
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
</div> : null </div> : null
} }

View file

@ -1,4 +1,4 @@
import React from 'react'; import React, { Fragment } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@ -8,7 +8,6 @@ import ParseToolbarButton from 'Parse/ParseToolbarButton';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector'; import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
function CustomFormatSettingsPage() { function CustomFormatSettingsPage() {
return ( return (
@ -18,13 +17,11 @@ function CustomFormatSettingsPage() {
// @ts-ignore // @ts-ignore
showSave={false} showSave={false}
additionalButtons={ additionalButtons={
<> <Fragment>
<PageToolbarSeparator /> <PageToolbarSeparator />
<ParseToolbarButton /> <ParseToolbarButton />
</Fragment>
<ManageCustomFormatsToolbarButton />
</>
} }
/> />

View file

@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditCustomFormatModal from './EditCustomFormatModal'; import EditCustomFormatModal from './EditCustomFormatModal';
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
function mapStateToProps() { function mapStateToProps() {
return {}; return {};
@ -37,7 +36,6 @@ class EditCustomFormatModalConnector extends Component {
} }
EditCustomFormatModalConnector.propTypes = { EditCustomFormatModalConnector.propTypes = {
...EditCustomFormatModalContentConnector.propTypes,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired clearPendingChanges: PropTypes.func.isRequired
}; };

View file

@ -1,28 +0,0 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageCustomFormatsEditModalContent from './ManageCustomFormatsEditModalContent';
interface ManageCustomFormatsEditModalProps {
isOpen: boolean;
customFormatIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function ManageCustomFormatsEditModal(
props: ManageCustomFormatsEditModalProps
) {
const { isOpen, customFormatIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageCustomFormatsEditModalContent
customFormatIds={customFormatIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ManageCustomFormatsEditModal;

View file

@ -1,16 +0,0 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}

View file

@ -1,8 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'modalFooter': string;
'selected': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

@ -1,125 +0,0 @@
import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
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 { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ManageCustomFormatsEditModalContent.css';
interface SavePayload {
includeCustomFormatWhenRenaming?: boolean;
}
interface ManageCustomFormatsEditModalContentProps {
customFormatIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
const NO_CHANGE = 'noChange';
const enableOptions = [
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
isDisabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageCustomFormatsEditModalContent(
props: ManageCustomFormatsEditModalContentProps
) {
const { customFormatIds, onSavePress, onModalClose } = props;
const [includeCustomFormatWhenRenaming, setIncludeCustomFormatWhenRenaming] =
useState(NO_CHANGE);
const save = useCallback(() => {
let hasChanges = false;
const payload: SavePayload = {};
if (includeCustomFormatWhenRenaming !== NO_CHANGE) {
hasChanges = true;
payload.includeCustomFormatWhenRenaming =
includeCustomFormatWhenRenaming === 'enabled';
}
if (hasChanges) {
onSavePress(payload);
}
onModalClose();
}, [includeCustomFormatWhenRenaming, onSavePress, onModalClose]);
const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'includeCustomFormatWhenRenaming':
setIncludeCustomFormatWhenRenaming(value);
break;
default:
console.warn(
`EditCustomFormatsModalContent Unknown Input: '${name}'`
);
}
},
[]
);
const selectedCount = customFormatIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditSelectedCustomFormats')}</ModalHeader>
<ModalBody>
<FormGroup>
<FormLabel>{translate('IncludeCustomFormatWhenRenaming')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="includeCustomFormatWhenRenaming"
value={includeCustomFormatWhenRenaming}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountCustomFormatsSelected', {
count: selectedCount,
})}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={save}>{translate('ApplyChanges')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default ManageCustomFormatsEditModalContent;

View file

@ -1,20 +0,0 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageCustomFormatsModalContent from './ManageCustomFormatsModalContent';
interface ManageCustomFormatsModalProps {
isOpen: boolean;
onModalClose(): void;
}
function ManageCustomFormatsModal(props: ManageCustomFormatsModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageCustomFormatsModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default ManageCustomFormatsModal;

View file

@ -1,16 +0,0 @@
.leftButtons,
.rightButtons {
display: flex;
flex: 1 0 50%;
flex-wrap: wrap;
}
.rightButtons {
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}

View file

@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteButton': string;
'leftButtons': string;
'rightButtons': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

@ -1,244 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CustomFormatAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
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 {
bulkDeleteCustomFormats,
bulkEditCustomFormats,
setManageCustomFormatsSort,
} from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import ManageCustomFormatsEditModal from './Edit/ManageCustomFormatsEditModal';
import ManageCustomFormatsModalRow from './ManageCustomFormatsModalRow';
import styles from './ManageCustomFormatsModalContent.css';
// TODO: This feels janky to do, but not sure of a better way currently
type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageCustomFormatsModalRow
>['onSelectedChange'];
const COLUMNS: Column[] = [
{
name: 'name',
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'includeCustomFormatWhenRenaming',
label: () => translate('IncludeCustomFormatWhenRenaming'),
isSortable: true,
isVisible: true,
},
{
name: 'actions',
label: '',
isVisible: true,
},
];
interface ManageCustomFormatsModalContentProps {
onModalClose(): void;
}
function ManageCustomFormatsModalContent(
props: ManageCustomFormatsModalContentProps
) {
const { onModalClose } = props;
const {
isFetching,
isPopulated,
isDeleting,
isSaving,
error,
items,
sortKey,
sortDirection,
}: CustomFormatAppState = useSelector(
createClientSideCollectionSelector('settings.customFormats')
);
const dispatch = useDispatch();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageCustomFormatsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
const onDeleteModalClose = useCallback(() => {
setIsDeleteModalOpen(false);
}, [setIsDeleteModalOpen]);
const onEditPress = useCallback(() => {
setIsEditModalOpen(true);
}, [setIsEditModalOpen]);
const onEditModalClose = useCallback(() => {
setIsEditModalOpen(false);
}, [setIsEditModalOpen]);
const onConfirmDelete = useCallback(() => {
dispatch(bulkDeleteCustomFormats({ ids: selectedIds }));
setIsDeleteModalOpen(false);
}, [selectedIds, dispatch]);
const onSavePress = useCallback(
(payload: object) => {
setIsEditModalOpen(false);
dispatch(
bulkEditCustomFormats({
ids: selectedIds,
...payload,
})
);
},
[selectedIds, dispatch]
);
const onSelectAllChange = useCallback(
({ value }: SelectStateInputProps) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, shiftKey = false }) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const errorMessage = getErrorMessage(error, 'Unable to load custom formats.');
const anySelected = selectedCount > 0;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('ManageCustomFormats')}</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>{translate('NoCustomFormatsFound')}</Alert>
) : null}
{isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table
columns={COLUMNS}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
sortKey={sortKey}
sortDirection={sortDirection}
onSelectAllChange={onSelectAllChange}
onSortPress={onSortPress}
>
<TableBody>
{items.map((item) => {
return (
<ManageCustomFormatsModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
</TableBody>
</Table>
) : null}
</ModalBody>
<ModalFooter>
<div className={styles.leftButtons}>
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!anySelected}
onPress={onDeletePress}
>
{translate('Delete')}
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving}
isDisabled={!anySelected}
onPress={onEditPress}
>
{translate('Edit')}
</SpinnerButton>
</div>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
<ManageCustomFormatsEditModal
isOpen={isEditModalOpen}
customFormatIds={selectedIds}
onModalClose={onEditModalClose}
onSavePress={onSavePress}
/>
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedCustomFormats')}
message={translate('DeleteSelectedCustomFormatsMessageText', {
count: selectedIds.length,
})}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>
</ModalContent>
);
}
export default ManageCustomFormatsModalContent;

View file

@ -1,12 +0,0 @@
.name,
.includeCustomFormatWhenRenaming {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 40px;
}

View file

@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
'includeCustomFormatWhenRenaming': string;
'name': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

@ -1,126 +0,0 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import { deleteCustomFormat } from 'Store/Actions/settingsActions';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector';
import styles from './ManageCustomFormatsModalRow.css';
interface ManageCustomFormatsModalRowProps {
id: number;
name: string;
includeCustomFormatWhenRenaming: boolean;
columns: Column[];
isSelected?: boolean;
onSelectedChange(result: SelectStateInputProps): void;
}
function isDeletingSelector() {
return createSelector(
(state: AppState) => state.settings.customFormats.isDeleting,
(isDeleting) => {
return isDeleting;
}
);
}
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
const {
id,
isSelected,
name,
includeCustomFormatWhenRenaming,
onSelectedChange,
} = props;
const dispatch = useDispatch();
const isDeleting = useSelector(isDeletingSelector());
const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] =
useState(false);
const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] =
useState(false);
const handlelectedChange = useCallback(
(result: SelectStateInputProps) => {
onSelectedChange({
...result,
});
},
[onSelectedChange]
);
const handleEditCustomFormatModalOpen = useCallback(() => {
setIsEditCustomFormatModalOpen(true);
}, [setIsEditCustomFormatModalOpen]);
const handleEditCustomFormatModalClose = useCallback(() => {
setIsEditCustomFormatModalOpen(false);
}, [setIsEditCustomFormatModalOpen]);
const handleDeleteCustomFormatPress = useCallback(() => {
setIsEditCustomFormatModalOpen(false);
setIsDeleteCustomFormatModalOpen(true);
}, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]);
const handleDeleteCustomFormatModalClose = useCallback(() => {
setIsDeleteCustomFormatModalOpen(false);
}, [setIsDeleteCustomFormatModalOpen]);
const handleConfirmDeleteCustomFormat = useCallback(() => {
dispatch(deleteCustomFormat({ id }));
}, [id, dispatch]);
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={handlelectedChange}
/>
<TableRowCell className={styles.name}>{name}</TableRowCell>
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
name={icons.EDIT}
onPress={handleEditCustomFormatModalOpen}
/>
</TableRowCell>
<EditCustomFormatModalConnector
id={id}
isOpen={isEditCustomFormatModalOpen}
onModalClose={handleEditCustomFormatModalClose}
onDeleteCustomFormatPress={handleDeleteCustomFormatPress}
/>
<ConfirmModal
isOpen={isDeleteCustomFormatModalOpen}
kind="danger"
title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={handleConfirmDeleteCustomFormat}
onCancel={handleDeleteCustomFormatModalClose}
/>
</TableRow>
);
}
export default ManageCustomFormatsModalRow;

View file

@ -1,28 +0,0 @@
import React from 'react';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import ManageCustomFormatsModal from './ManageCustomFormatsModal';
function ManageCustomFormatsToolbarButton() {
const [isManageModalOpen, openManageModal, closeManageModal] =
useModalOpenState(false);
return (
<>
<PageToolbarButton
label={translate('ManageFormats')}
iconName={icons.MANAGE}
onPress={openManageModal}
/>
<ManageCustomFormatsModal
isOpen={isManageModalOpen}
onModalClose={closeManageModal}
/>
</>
);
}
export default ManageCustomFormatsToolbarButton;

View file

@ -7,8 +7,8 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
@ -52,13 +52,12 @@ function EditSpecificationModalContent(props) {
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) && fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
<div> <div>
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> <div dangerouslySetInnerHTML={{ __html: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
{'More details'} <Link to="https://www.regular-expressions.info/tutorial.html">{'Here'}</Link>
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} /> {'Regular expressions can be tested '}
</div> <Link to="http://regexstorm.net/tester">Here</Link>
<div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
</div> </div>
</Alert> </Alert>
} }

View file

@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteDownloadClients, bulkDeleteDownloadClients,
bulkEditDownloadClients, bulkEditDownloadClients,
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageDownloadClientsModalRow typeof ManageDownloadClientsModalRow
>['onSelectedChange']; >['onSelectedChange'];
const COLUMNS: Column[] = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: () => translate('Name'),
@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
interface ManageDownloadClientsModalContentProps { interface ManageDownloadClientsModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageDownloadClientsModalContent( function ManageDownloadClientsModalContent(
@ -218,9 +220,9 @@ function ManageDownloadClientsModalContent(
{error ? <div>{errorMessage}</div> : null} {error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length ? ( {isPopulated && !error && !items.length && (
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert> <Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
) : null} )}
{isPopulated && !!items.length && !isFetching && !isFetching ? ( {isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table <Table

View file

@ -156,7 +156,6 @@ class GeneralSettings extends Component {
/> />
<LoggingSettings <LoggingSettings
advancedSettings={advancedSettings}
settings={settings} settings={settings}
onInputChange={onInputChange} onInputChange={onInputChange}
/> />

View file

@ -15,14 +15,12 @@ const logLevelOptions = [
function LoggingSettings(props) { function LoggingSettings(props) {
const { const {
advancedSettings,
settings, settings,
onInputChange onInputChange
} = props; } = props;
const { const {
logLevel, logLevel
logSizeLimit
} = settings; } = settings;
return ( return (
@ -41,30 +39,11 @@ function LoggingSettings(props) {
{...logLevel} {...logLevel}
/> />
</FormGroup> </FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('LogSizeLimit')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="logSizeLimit"
min={1}
max={10}
unit="MB"
helpText={translate('LogSizeLimitHelpText')}
onChange={onInputChange}
{...logSizeLimit}
/>
</FormGroup>
</FieldSet> </FieldSet>
); );
} }
LoggingSettings.propTypes = { LoggingSettings.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired onInputChange: PropTypes.func.isRequired
}; };

View file

@ -292,7 +292,7 @@ function EditImportListModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
helpText={translate('ImportListTagsHelpText')} helpText={translate('TagsHelpText')}
{...tags} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />

View file

@ -198,9 +198,9 @@ function ManageImportListsModalContent(
{error ? <div>{errorMessage}</div> : null} {error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length ? ( {isPopulated && !error && !items.length && (
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert> <Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
) : null} )}
{isPopulated && !!items.length && !isFetching && !isFetching ? ( {isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table <Table

View file

@ -10,11 +10,11 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteIndexers, bulkDeleteIndexers,
bulkEditIndexers, bulkEditIndexers,
@ -35,7 +35,7 @@ type OnSelectedChangeCallback = React.ComponentProps<
typeof ManageIndexersModalRow typeof ManageIndexersModalRow
>['onSelectedChange']; >['onSelectedChange'];
const COLUMNS: Column[] = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: () => translate('Name'), label: () => translate('Name'),
@ -82,6 +82,8 @@ const COLUMNS: Column[] = [
interface ManageIndexersModalContentProps { interface ManageIndexersModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@ -213,9 +215,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
{error ? <div>{errorMessage}</div> : null} {error ? <div>{errorMessage}</div> : null}
{isPopulated && !error && !items.length ? ( {isPopulated && !error && !items.length && (
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert> <Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
) : null} )}
{isPopulated && !!items.length && !isFetching && !isFetching ? ( {isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table <Table

View file

@ -191,21 +191,26 @@ class MediaManagement extends Component {
<FieldSet <FieldSet
legend={translate('Importing')} legend={translate('Importing')}
> >
{
!isWindows &&
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
size={sizes.MEDIUM} size={sizes.MEDIUM}
> >
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel> <FormLabel>
{translate('SkipFreeSpaceCheck')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="skipFreeSpaceCheckWhenImporting" name="skipFreeSpaceCheckWhenImporting"
helpText={translate('SkipFreeSpaceCheckHelpText')} helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
onChange={onInputChange} onChange={onInputChange}
{...settings.skipFreeSpaceCheckWhenImporting} {...settings.skipFreeSpaceCheckWhenImporting}
/> />
</FormGroup> </FormGroup>
}
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}

View file

@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -14,11 +15,11 @@ function createMapStateToProps() {
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
(state) => state.settings.namingExamples, (state) => state.settings.namingExamples,
createSettingsSectionSelector(SECTION), createSettingsSectionSelector(SECTION),
(advancedSettings, namingExamples, sectionSettings) => { (advancedSettings, examples, sectionSettings) => {
return { return {
advancedSettings, advancedSettings,
examples: namingExamples.item, examples: examples.item,
examplesPopulated: namingExamples.isPopulated, examplesPopulated: !_.isEmpty(examples.item),
...sectionSettings ...sectionSettings
}; };
} }

View file

@ -94,9 +94,9 @@ class RootFolder extends Component {
<ConfirmModal <ConfirmModal
isOpen={this.state.isDeleteRootFolderModalOpen} isOpen={this.state.isDeleteRootFolderModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('RemoveRootFolder')} title={translate('DeleteRootFolder')}
message={translate('RemoveRootFolderArtistsMessageText', { name })} message={translate('DeleteRootFolderMessageText', { name })}
confirmLabel={translate('Remove')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRootFolder} onConfirm={this.onConfirmDeleteRootFolder}
onCancel={this.onDeleteRootFolderModalClose} onCancel={this.onDeleteRootFolderModalClose}
/> />

View file

@ -105,7 +105,7 @@ function EditNotificationModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
helpText={translate('NotificationsTagsArtistHelpText')} helpText={translate('TagsHelpText')}
{...tags} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />

View file

@ -87,9 +87,9 @@ function EditDelayProfileModalContent(props) {
{ {
!isFetching && !!error ? !isFetching && !!error ?
<Alert kind={kinds.DANGER}> <div>
{translate('AddDelayProfileError')} {translate('UnableToAddANewQualityProfilePleaseTryAgain')}
</Alert> : </div> :
null null
} }
@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
{ {
id === 1 ? id === 1 ?
<Alert> <Alert>
{translate('DefaultDelayProfileArtist')} {translate('DefaultDelayProfileHelpText')}
</Alert> : </Alert> :
<FormGroup> <FormGroup>
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
{...tags} {...tags}
helpText={translate('DelayProfileArtistTagsHelpText')} helpText={translate('TagsHelpText')}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>

View file

@ -119,7 +119,7 @@ function EditReleaseProfileModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
helpText={translate('ReleaseProfileTagArtistHelpText')} helpText={translate('TagsHelpText')}
{...tags} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />

View file

@ -24,19 +24,19 @@
height: 20px; height: 20px;
} }
.track { .bar {
top: 9px; top: 9px;
margin: 0 5px; margin: 0 5px;
height: 3px; height: 3px;
background-color: var(--sliderAccentColor); background-color: var(--sliderAccentColor);
box-shadow: 0 0 0 #000; box-shadow: 0 0 0 #000;
&:nth-child(3n + 1) { &:nth-child(3n+1) {
background-color: #ddd; background-color: #ddd;
} }
} }
.thumb { .handle {
top: 1px; top: 1px;
z-index: 0 !important; z-index: 0 !important;
width: 18px; width: 18px;

View file

@ -1,6 +1,8 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'bar': string;
'handle': string;
'kilobitsPerSecond': string; 'kilobitsPerSecond': string;
'quality': string; 'quality': string;
'qualityDefinition': string; 'qualityDefinition': string;
@ -8,9 +10,7 @@ interface CssExports {
'sizeLimit': string; 'sizeLimit': string;
'sizes': string; 'sizes': string;
'slider': string; 'slider': string;
'thumb': string;
'title': string; 'title': string;
'track': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View file

@ -55,27 +55,6 @@ class QualityDefinition extends Component {
}; };
} }
//
// Control
trackRenderer(props, state) {
return (
<div
{...props}
className={styles.track}
/>
);
}
thumbRenderer(props, state) {
return (
<div
{...props}
className={styles.thumb}
/>
);
}
// //
// Listeners // Listeners
@ -195,7 +174,6 @@ class QualityDefinition extends Component {
<div className={styles.sizeLimit}> <div className={styles.sizeLimit}>
<ReactSlider <ReactSlider
className={styles.slider}
min={slider.min} min={slider.min}
max={slider.max} max={slider.max}
step={slider.step} step={slider.step}
@ -204,9 +182,9 @@ class QualityDefinition extends Component {
withTracks={true} withTracks={true}
allowCross={false} allowCross={false}
snapDragDisabled={true} snapDragDisabled={true}
pearling={true} className={styles.slider}
renderThumb={this.thumbRenderer} trackClassName={styles.bar}
renderTrack={this.trackRenderer} thumbClassName={styles.handle}
onChange={this.onSliderChange} onChange={this.onSliderChange}
onAfterChange={this.onAfterSliderChange} onAfterChange={this.onAfterSliderChange}
/> />

View file

@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} /> <InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} /> <InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
</div> </div>
</Alert> </Alert>
} }

View file

@ -4,13 +4,11 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import Tags from './Tags'; import Tags from './Tags';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('tags', sortByProp('label')), (state) => state.tags,
(tags) => { (tags) => {
const isFetching = tags.isFetching || tags.details.isFetching; const isFetching = tags.isFetching || tags.details.isFetching;
const error = tags.error || tags.details.error; const error = tags.error || tags.details.error;

View file

@ -7,7 +7,7 @@ function createRemoveItemHandler(section, url) {
return function(getState, payload, dispatch) { return function(getState, payload, dispatch) {
const { const {
id, id,
queryParams ...queryParams
} = payload; } = payload;
dispatch(set({ section, isDeleting: true })); dispatch(set({ section, isDeleting: true }));

View file

@ -1,12 +1,7 @@
import { createAction } from 'redux-actions'; 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 createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler'; import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler'; import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetClientSideCollectionSortReducer
from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState'; 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 DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue'; export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat'; 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 // 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 fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT); export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
export const deleteCustomFormat = createThunk(DELETE_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) => { export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
return { return {
@ -58,30 +47,20 @@ export default {
// State // State
defaultState: { defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSaving: false,
saveError: null,
isDeleting: false,
deleteError: null,
items: [],
pendingChanges: {},
isSchemaFetching: false, isSchemaFetching: false,
isSchemaPopulated: false, isSchemaPopulated: false,
schemaError: null, isFetching: false,
isPopulated: false,
schema: { schema: {
includeCustomFormatWhenRenaming: false includeCustomFormatWhenRenaming: false
}, },
error: null,
sortKey: 'name', isDeleting: false,
sortDirection: sortDirections.ASCENDING, deleteError: null,
sortPredicates: { isSaving: false,
name: ({ name }) => { saveError: null,
return name.toLocaleLowerCase(); items: [],
} pendingChanges: {}
}
}, },
// //
@ -103,10 +82,7 @@ export default {
})); }));
createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch); 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; newState.pendingChanges = pendingChanges;
return updateSectionState(state, section, newState); return updateSectionState(state, section, newState);
}, }
[SET_MANAGE_CUSTOM_FORMATS_SORT]: createSetClientSideCollectionSortReducer(section)
} }
}; };

Some files were not shown because too many files have changed in this diff Show more