Update to .NET 8
This commit is contained in:
parent
de9899c60e
commit
803cda9d22
|
@ -0,0 +1,19 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
|
{
|
||||||
|
"name": "Sonarr",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"version": "16",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [8989],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@
|
||||||
outputFolder=_output
|
outputFolder=_output
|
||||||
artifactsFolder=_artifacts
|
artifactsFolder=_artifacts
|
||||||
uiFolder="$outputFolder/UI"
|
uiFolder="$outputFolder/UI"
|
||||||
framework="${FRAMEWORK:=net6.0}"
|
framework="${FRAMEWORK:=net8.0}"
|
||||||
|
|
||||||
rm -rf $artifactsFolder
|
rm -rf $artifactsFolder
|
||||||
mkdir $artifactsFolder
|
mkdir $artifactsFolder
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
|
@ -19,10 +19,10 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FRAMEWORK: net6.0
|
FRAMEWORK: net8.0
|
||||||
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
SONARR_MAJOR_VERSION: 4
|
SONARR_MAJOR_VERSION: 4
|
||||||
VERSION: 4.0.2
|
VERSION: 4.0.4
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backend:
|
backend:
|
||||||
|
@ -121,7 +121,7 @@ jobs:
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
|
||||||
- name: Stylelint
|
- name: Stylelint
|
||||||
run: yarn stylelint
|
run: yarn stylelint -f github
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build --env production
|
run: yarn build --env production
|
||||||
|
@ -217,7 +217,7 @@ jobs:
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
|
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
|
||||||
needs: [backend, unit_test, unit_test_postgres, integration_test]
|
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test]
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
uses: ./.github/workflows/deploy.yml
|
uses: ./.github/workflows/deploy.yml
|
||||||
with:
|
with:
|
||||||
|
@ -225,3 +225,25 @@ jobs:
|
||||||
branch: ${{ github.ref_name }}
|
branch: ${{ github.ref_name }}
|
||||||
major_version: ${{ needs.backend.outputs.major_version }}
|
major_version: ${{ needs.backend.outputs.major_version }}
|
||||||
version: ${{ needs.backend.outputs.version }}
|
version: ${{ needs.backend.outputs.version }}
|
||||||
|
|
||||||
|
notify:
|
||||||
|
name: Discord Notification
|
||||||
|
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy]
|
||||||
|
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
|
||||||
|
env:
|
||||||
|
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify
|
||||||
|
uses: tsickert/discord-webhook@v5.3.0
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
username: 'GitHub Actions'
|
||||||
|
avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
|
||||||
|
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
|
||||||
|
embed-url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
||||||
|
embed-description: |
|
||||||
|
**Branch** ${{ github.ref }}
|
||||||
|
**Build** ${{ needs.backend.outputs.version }}
|
||||||
|
embed-color: ${{ env.STATUS == 'success' && '3066993' || '15158332' }}
|
||||||
|
|
|
@ -69,12 +69,38 @@ jobs:
|
||||||
pattern: release_*
|
pattern: release_*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Get Previous Release
|
||||||
|
id: previous-release
|
||||||
|
uses: cardinalby/git-get-release-action@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
with:
|
||||||
|
latest: true
|
||||||
|
prerelease: ${{ inputs.branch != 'main' }}
|
||||||
|
|
||||||
|
- name: Generate Release Notes
|
||||||
|
id: generate-release-notes
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
result-encoding: string
|
||||||
|
script: |
|
||||||
|
const { data } = await github.rest.repos.generateReleaseNotes({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
tag_name: 'v${{ inputs.version }}',
|
||||||
|
target_commitish: '${{ github.sha }}',
|
||||||
|
previous_tag_name: '${{ steps.previous-release.outputs.tag_name }}',
|
||||||
|
})
|
||||||
|
return data.body
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
artifacts: _artifacts/Sonarr.*
|
artifacts: _artifacts/Sonarr.*
|
||||||
commit: ${{ github.sha }}
|
commit: ${{ github.sha }}
|
||||||
generateReleaseNotes: true
|
generateReleaseNotes: false
|
||||||
|
body: ${{ steps.generate-release-notes.outputs.result }}
|
||||||
name: ${{ inputs.version }}
|
name: ${{ inputs.version }}
|
||||||
prerelease: ${{ inputs.branch != 'main' }}
|
prerelease: ${{ inputs.branch != 'main' }}
|
||||||
skipIfReleaseExists: true
|
skipIfReleaseExists: true
|
||||||
|
|
|
@ -127,6 +127,7 @@ coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
#VS outout folders
|
#VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||||
|
"name": "Run Sonarr",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build dotnet",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/_output/net6.0/Sonarr",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build dotnet",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"-restore",
|
||||||
|
"${workspaceFolder}/src/Sonarr.sln",
|
||||||
|
"-p:GenerateFullPaths=true",
|
||||||
|
"-p:Configuration=Debug",
|
||||||
|
"-p:Platform=Posix",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/src/Sonarr.sln",
|
||||||
|
"-property:GenerateFullPaths=true",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/src/Sonarr.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2
build.sh
2
build.sh
|
@ -4,7 +4,7 @@ set -e
|
||||||
outputFolder='_output'
|
outputFolder='_output'
|
||||||
testPackageFolder='_tests'
|
testPackageFolder='_tests'
|
||||||
artifactsFolder="_artifacts";
|
artifactsFolder="_artifacts";
|
||||||
framework="${FRAMEWORK:=net6.0}"
|
framework="${FRAMEWORK:=net8.0}"
|
||||||
|
|
||||||
ProgressStart()
|
ProgressStart()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
### Description: Sonarr .NET Debian install
|
### Description: Sonarr .NET Debian install
|
||||||
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
|
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
|
||||||
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
|
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@REM SET SONARR_MAJOR_VERSION=4
|
@REM SET SONARR_MAJOR_VERSION=4
|
||||||
@REM SET SONARR_VERSION=4.0.0.5
|
@REM SET SONARR_VERSION=4.0.0.5
|
||||||
@REM SET BRANCH=develop
|
@REM SET BRANCH=develop
|
||||||
@REM SET FRAMEWORK=net6.0
|
@REM SET FRAMEWORK=net8.0
|
||||||
@REM SET RUNTIME=win-x64
|
@REM SET RUNTIME=win-x64
|
||||||
|
|
||||||
inno\ISCC.exe sonarr.iss
|
inno\ISCC.exe sonarr.iss
|
||||||
|
|
2
docs.sh
2
docs.sh
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
FRAMEWORK="net6.0"
|
FRAMEWORK="net8.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
|
|
@ -19,6 +19,7 @@ export interface AppSectionSaveState {
|
||||||
|
|
||||||
export interface PagedAppSectionState {
|
export interface PagedAppSectionState {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
totalRecords?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionFilterState<T> {
|
export interface AppSectionFilterState<T> {
|
||||||
|
@ -38,6 +39,7 @@ export interface AppSectionItemState<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,12 @@ import AppSectionState, {
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
|
PagedAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
|
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
import IndexerFlag from 'typings/IndexerFlag';
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
|
@ -41,6 +43,14 @@ export interface ImportListOptionsSettingsAppState
|
||||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface ImportListExclusionsSettingsAppState
|
||||||
|
extends AppSectionState<ImportListExclusion>,
|
||||||
|
AppSectionSaveState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
AppSectionDeleteState {
|
||||||
|
pendingChanges: Partial<ImportListExclusion>;
|
||||||
|
}
|
||||||
|
|
||||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
@ -48,6 +58,7 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
advancedSettings: boolean;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||||
importListOptions: ImportListOptionsSettingsAppState;
|
importListOptions: ImportListOptionsSettingsAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
|
|
|
@ -13,6 +13,8 @@ export interface CommandBody {
|
||||||
trigger: string;
|
trigger: string;
|
||||||
suppressMessages: boolean;
|
suppressMessages: boolean;
|
||||||
seriesId?: number;
|
seriesId?: number;
|
||||||
|
seriesIds?: number[];
|
||||||
|
seasonNumber?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Command extends ModelBase {
|
interface Command extends ModelBase {
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
.isDisabled {
|
.isDisabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownArrowContainer {
|
.dropdownArrowContainer {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import PasswordInput from './PasswordInput';
|
||||||
import PathInputConnector from './PathInputConnector';
|
import PathInputConnector from './PathInputConnector';
|
||||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||||
|
import SeriesTagInput from './SeriesTagInput';
|
||||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||||
import TagInputConnector from './TagInputConnector';
|
import TagInputConnector from './TagInputConnector';
|
||||||
import TagSelectInputConnector from './TagSelectInputConnector';
|
import TagSelectInputConnector from './TagSelectInputConnector';
|
||||||
|
@ -87,6 +88,9 @@ function getComponent(type) {
|
||||||
case inputTypes.DYNAMIC_SELECT:
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
return EnhancedSelectInputConnector;
|
return EnhancedSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.SERIES_TAG:
|
||||||
|
return SeriesTagInput;
|
||||||
|
|
||||||
case inputTypes.SERIES_TYPE_SELECT:
|
case inputTypes.SERIES_TYPE_SELECT:
|
||||||
return SeriesTypeSelectInput;
|
return SeriesTypeSelectInput;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import monitorOptions from 'Utilities/Series/monitorOptions';
|
import monitorOptions from 'Utilities/Series/monitorOptions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MonitorEpisodesSelectInput(props) {
|
function MonitorEpisodesSelectInput(props) {
|
||||||
const {
|
const {
|
||||||
|
@ -19,7 +19,7 @@ function MonitorEpisodesSelectInput(props) {
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,12 +29,12 @@ function MonitorEpisodesSelectInput(props) {
|
||||||
get value() {
|
get value() {
|
||||||
return `(${translate('Mixed')})`;
|
return `(${translate('Mixed')})`;
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<EnhancedSelectInput
|
||||||
values={values}
|
values={values}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
||||||
import SelectInput from './SelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MonitorNewItemsSelectInput(props) {
|
function MonitorNewItemsSelectInput(props) {
|
||||||
const {
|
const {
|
||||||
|
@ -16,7 +16,7 @@ function MonitorNewItemsSelectInput(props) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: 'No Change',
|
value: 'No Change',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,12 +24,12 @@ function MonitorNewItemsSelectInput(props) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<EnhancedSelectInput
|
||||||
values={values}
|
values={values}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -27,6 +27,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||||
return inputTypes.DYNAMIC_SELECT;
|
return inputTypes.DYNAMIC_SELECT;
|
||||||
}
|
}
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
|
case 'seriesTag':
|
||||||
|
return inputTypes.SERIES_TAG;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
return inputTypes.TEXT_TAG;
|
return inputTypes.TEXT_TAG;
|
||||||
case 'tagSelect':
|
case 'tagSelect':
|
||||||
|
|
|
@ -28,7 +28,7 @@ function createMapStateToProps() {
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: includeNoChangeDisabled
|
isDisabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ function createMapStateToProps() {
|
||||||
get value() {
|
get value() {
|
||||||
return `(${translate('Mixed')})`;
|
return `(${translate('Mixed')})`;
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import TagInputConnector from './TagInputConnector';
|
||||||
|
|
||||||
|
interface SeriesTageInputProps {
|
||||||
|
name: string;
|
||||||
|
value: number | number[];
|
||||||
|
onChange: ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
value: number | number[];
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SeriesTagInput(props: SeriesTageInputProps) {
|
||||||
|
const { value, onChange, ...otherProps } = props;
|
||||||
|
const isArray = Array.isArray(value);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||||
|
if (isArray) {
|
||||||
|
onChange({ name, value: newValue });
|
||||||
|
} else {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isArray, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
let finalValue: number[] = [];
|
||||||
|
|
||||||
|
if (isArray) {
|
||||||
|
finalValue = value;
|
||||||
|
} else if (value === 0) {
|
||||||
|
finalValue = [];
|
||||||
|
} else {
|
||||||
|
finalValue = [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||||
|
<TagInputConnector
|
||||||
|
{...otherProps}
|
||||||
|
value={finalValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ interface ISeriesTypeOption {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
format?: string;
|
format?: string;
|
||||||
disabled?: boolean;
|
isDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesTypeOptions: ISeriesTypeOption[] = [
|
const seriesTypeOptions: ISeriesTypeOption[] = [
|
||||||
|
@ -55,7 +55,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: includeNoChangeDisabled,
|
isDisabled: includeNoChangeDisabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ function SeriesTypeSelectInput(props: SeriesTypeSelectInputProps) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: `(${translate('Mixed')})`,
|
value: `(${translate('Mixed')})`,
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,13 @@
|
||||||
width: 1280px;
|
width: 1280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.extraExtraLarge {
|
||||||
|
composes: modal;
|
||||||
|
|
||||||
|
width: 1600px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||||
.modal.extraLarge {
|
.modal.extraLarge {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
@ -90,7 +97,8 @@
|
||||||
.modal.small,
|
.modal.small,
|
||||||
.modal.medium,
|
.modal.medium,
|
||||||
.modal.large,
|
.modal.large,
|
||||||
.modal.extraLarge {
|
.modal.extraLarge,
|
||||||
|
.modal.extraExtraLarge {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// 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 {
|
||||||
|
'extraExtraLarge': string;
|
||||||
'extraLarge': string;
|
'extraLarge': string;
|
||||||
'large': string;
|
'large': string;
|
||||||
'medium': string;
|
'medium': string;
|
||||||
|
|
|
@ -15,5 +15,5 @@
|
||||||
"start_url": "../../../../",
|
"start_url": "../../../../",
|
||||||
"theme_color": "#3a3f51",
|
"theme_color": "#3a3f51",
|
||||||
"background_color": "#3a3f51",
|
"background_color": "#3a3f51",
|
||||||
"display": "minimal-ui"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ class EpisodeDetailsModal extends Component {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_LARGE}
|
size={sizes.EXTRA_EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={this.state.closeOnBackgroundClick}
|
closeOnBackgroundClick={this.state.closeOnBackgroundClick}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
export default function getReleaseTypeName(
|
||||||
|
releaseType?: ReleaseType
|
||||||
|
): string | null {
|
||||||
|
switch (releaseType) {
|
||||||
|
case 'singleEpisode':
|
||||||
|
return translate('SingleEpisode');
|
||||||
|
case 'multiEpisode':
|
||||||
|
return translate('MultiEpisode');
|
||||||
|
case 'seasonPack':
|
||||||
|
return translate('SeasonPack');
|
||||||
|
default:
|
||||||
|
return translate('Unknown');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import { QualityModel } from 'Quality/Quality';
|
import { QualityModel } from 'Quality/Quality';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
@ -17,6 +18,7 @@ export interface EpisodeFile extends ModelBase {
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
customFormats: CustomFormat[];
|
customFormats: CustomFormat[];
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
|
releaseType: ReleaseType;
|
||||||
mediaInfo: MediaInfo;
|
mediaInfo: MediaInfo;
|
||||||
qualityCutoffNotMet: boolean;
|
qualityCutoffNotMet: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
export default function useModalOpenState(
|
||||||
|
initialState: boolean
|
||||||
|
): [boolean, () => void, () => void] {
|
||||||
|
const [isOpen, setOpen] = useState(initialState);
|
||||||
|
|
||||||
|
const setModalOpen = useCallback(() => {
|
||||||
|
setOpen(true);
|
||||||
|
}, [setOpen]);
|
||||||
|
|
||||||
|
const setModalClosed = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, [setOpen]);
|
||||||
|
|
||||||
|
return [isOpen, setModalOpen, setModalClosed];
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ export const LANGUAGE_SELECT = 'languageSelect';
|
||||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||||
export const SELECT = 'select';
|
export const SELECT = 'select';
|
||||||
|
export const SERIES_TAG = 'seriesTag';
|
||||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||||
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
|
@ -45,6 +46,7 @@ export const all = [
|
||||||
ROOT_FOLDER_SELECT,
|
ROOT_FOLDER_SELECT,
|
||||||
LANGUAGE_SELECT,
|
LANGUAGE_SELECT,
|
||||||
SELECT,
|
SELECT,
|
||||||
|
SERIES_TAG,
|
||||||
DYNAMIC_SELECT,
|
DYNAMIC_SELECT,
|
||||||
SERIES_TYPE_SELECT,
|
SERIES_TYPE_SELECT,
|
||||||
TAG,
|
TAG,
|
||||||
|
|
|
@ -3,5 +3,5 @@ export const SMALL = 'small';
|
||||||
export const MEDIUM = 'medium';
|
export const MEDIUM = 'medium';
|
||||||
export const LARGE = 'large';
|
export const LARGE = 'large';
|
||||||
export const EXTRA_LARGE = 'extraLarge';
|
export const EXTRA_LARGE = 'extraLarge';
|
||||||
|
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
|
||||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];
|
||||||
|
|
|
@ -36,6 +36,7 @@ import InteractiveImport, {
|
||||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||||
|
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
|
||||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
|
@ -73,7 +74,8 @@ type SelectType =
|
||||||
| 'releaseGroup'
|
| 'releaseGroup'
|
||||||
| 'quality'
|
| 'quality'
|
||||||
| 'language'
|
| 'language'
|
||||||
| 'indexerFlags';
|
| 'indexerFlags'
|
||||||
|
| 'releaseType';
|
||||||
|
|
||||||
type FilterExistingFiles = 'all' | 'new';
|
type FilterExistingFiles = 'all' | 'new';
|
||||||
|
|
||||||
|
@ -128,6 +130,12 @@ const COLUMNS = [
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'releaseType',
|
||||||
|
label: () => translate('ReleaseType'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'customFormats',
|
name: 'customFormats',
|
||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
|
@ -369,6 +377,10 @@ function InteractiveImportModalContent(
|
||||||
key: 'indexerFlags',
|
key: 'indexerFlags',
|
||||||
value: translate('SelectIndexerFlags'),
|
value: translate('SelectIndexerFlags'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'releaseType',
|
||||||
|
value: translate('SelectReleaseType'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (allowSeriesChange) {
|
if (allowSeriesChange) {
|
||||||
|
@ -511,6 +523,7 @@ function InteractiveImportModalContent(
|
||||||
languages,
|
languages,
|
||||||
indexerFlags,
|
indexerFlags,
|
||||||
episodeFileId,
|
episodeFileId,
|
||||||
|
releaseType,
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
if (!series) {
|
if (!series) {
|
||||||
|
@ -560,6 +573,7 @@ function InteractiveImportModalContent(
|
||||||
quality,
|
quality,
|
||||||
languages,
|
languages,
|
||||||
indexerFlags,
|
indexerFlags,
|
||||||
|
releaseType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -575,6 +589,7 @@ function InteractiveImportModalContent(
|
||||||
quality,
|
quality,
|
||||||
languages,
|
languages,
|
||||||
indexerFlags,
|
indexerFlags,
|
||||||
|
releaseType,
|
||||||
downloadId,
|
downloadId,
|
||||||
episodeFileId,
|
episodeFileId,
|
||||||
});
|
});
|
||||||
|
@ -787,6 +802,22 @@ function InteractiveImportModalContent(
|
||||||
[selectedIds, dispatch]
|
[selectedIds, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onReleaseTypeSelect = useCallback(
|
||||||
|
(releaseType: string) => {
|
||||||
|
dispatch(
|
||||||
|
updateInteractiveImportItems({
|
||||||
|
ids: selectedIds,
|
||||||
|
releaseType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||||
|
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const orderedSelectedIds = items.reduce((acc: number[], file) => {
|
const orderedSelectedIds = items.reduce((acc: number[], file) => {
|
||||||
if (selectedIds.includes(file.id)) {
|
if (selectedIds.includes(file.id)) {
|
||||||
acc.push(file.id);
|
acc.push(file.id);
|
||||||
|
@ -1000,6 +1031,14 @@ function InteractiveImportModalContent(
|
||||||
onModalClose={onSelectModalClose}
|
onModalClose={onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectReleaseTypeModal
|
||||||
|
isOpen={selectModalOpen === 'releaseType'}
|
||||||
|
releaseType="unknown"
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onReleaseTypeSelect={onReleaseTypeSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isConfirmDeleteModalOpen}
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import Episode from 'Episode/Episode';
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import getReleaseTypeName from 'Episode/getReleaseTypeName';
|
||||||
import IndexerFlags from 'Episode/IndexerFlags';
|
import IndexerFlags from 'Episode/IndexerFlags';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||||
|
@ -20,6 +21,8 @@ import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexe
|
||||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||||
|
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||||
|
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
|
||||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
|
@ -44,7 +47,8 @@ type SelectType =
|
||||||
| 'releaseGroup'
|
| 'releaseGroup'
|
||||||
| 'quality'
|
| 'quality'
|
||||||
| 'language'
|
| 'language'
|
||||||
| 'indexerFlags';
|
| 'indexerFlags'
|
||||||
|
| 'releaseType';
|
||||||
|
|
||||||
type SelectedChangeProps = SelectStateInputProps & {
|
type SelectedChangeProps = SelectStateInputProps & {
|
||||||
hasEpisodeFileId: boolean;
|
hasEpisodeFileId: boolean;
|
||||||
|
@ -61,6 +65,7 @@ interface InteractiveImportRowProps {
|
||||||
quality?: QualityModel;
|
quality?: QualityModel;
|
||||||
languages?: Language[];
|
languages?: Language[];
|
||||||
size: number;
|
size: number;
|
||||||
|
releaseType: ReleaseType;
|
||||||
customFormats?: object[];
|
customFormats?: object[];
|
||||||
customFormatScore?: number;
|
customFormatScore?: number;
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
|
@ -86,6 +91,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
languages,
|
languages,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
size,
|
size,
|
||||||
|
releaseType,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
indexerFlags,
|
indexerFlags,
|
||||||
|
@ -315,6 +321,27 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSelectReleaseTypePress = useCallback(() => {
|
||||||
|
setSelectModalOpen('releaseType');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onReleaseTypeSelect = useCallback(
|
||||||
|
(releaseType: ReleaseType) => {
|
||||||
|
dispatch(
|
||||||
|
updateInteractiveImportItem({
|
||||||
|
id,
|
||||||
|
releaseType,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
|
||||||
|
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
selectRowAfterChange();
|
||||||
|
},
|
||||||
|
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||||
|
);
|
||||||
|
|
||||||
const onSelectIndexerFlagsPress = useCallback(() => {
|
const onSelectIndexerFlagsPress = useCallback(() => {
|
||||||
setSelectModalOpen('indexerFlags');
|
setSelectModalOpen('indexerFlags');
|
||||||
}, [setSelectModalOpen]);
|
}, [setSelectModalOpen]);
|
||||||
|
@ -461,6 +488,13 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
|
|
||||||
<TableRowCell>{formatBytes(size)}</TableRowCell>
|
<TableRowCell>{formatBytes(size)}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCellButton
|
||||||
|
title={translate('ClickToChangeReleaseType')}
|
||||||
|
onPress={onSelectReleaseTypePress}
|
||||||
|
>
|
||||||
|
{getReleaseTypeName(releaseType)}
|
||||||
|
</TableRowCellButton>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
{customFormats?.length ? (
|
{customFormats?.length ? (
|
||||||
<Popover
|
<Popover
|
||||||
|
@ -572,6 +606,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
onModalClose={onSelectModalClose}
|
onModalClose={onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectReleaseTypeModal
|
||||||
|
isOpen={selectModalOpen === 'releaseType'}
|
||||||
|
releaseType={releaseType ?? 'unknown'}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onReleaseTypeSelect={onReleaseTypeSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<SelectIndexerFlagsModal
|
<SelectIndexerFlagsModal
|
||||||
isOpen={selectModalOpen === 'indexerFlags'}
|
isOpen={selectModalOpen === 'indexerFlags'}
|
||||||
indexerFlags={indexerFlags ?? 0}
|
indexerFlags={indexerFlags ?? 0}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
import Episode from 'Episode/Episode';
|
import Episode from 'Episode/Episode';
|
||||||
|
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import { QualityModel } from 'Quality/Quality';
|
import { QualityModel } from 'Quality/Quality';
|
||||||
import Series from 'Series/Series';
|
import Series from 'Series/Series';
|
||||||
|
@ -14,6 +15,7 @@ export interface InteractiveImportCommandOptions {
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
languages: Language[];
|
languages: Language[];
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
|
releaseType: ReleaseType;
|
||||||
downloadId?: string;
|
downloadId?: string;
|
||||||
episodeFileId?: number;
|
episodeFileId?: number;
|
||||||
}
|
}
|
||||||
|
@ -33,6 +35,7 @@ interface InteractiveImport extends ModelBase {
|
||||||
qualityWeight: number;
|
qualityWeight: number;
|
||||||
customFormats: object[];
|
customFormats: object[];
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
|
releaseType: ReleaseType;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
episodeFileId?: number;
|
episodeFileId?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_LARGE}
|
size={sizes.EXTRA_EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
type ReleaseType = 'unknown' | 'singleEpisode' | 'multiEpisode' | 'seasonPack';
|
||||||
|
|
||||||
|
export default ReleaseType;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||||
|
import SelectReleaseTypeModalContent from './SelectReleaseTypeModalContent';
|
||||||
|
|
||||||
|
interface SelectQualityModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
releaseType: ReleaseType;
|
||||||
|
modalTitle: string;
|
||||||
|
onReleaseTypeSelect(releaseType: ReleaseType): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectReleaseTypeModal(props: SelectQualityModalProps) {
|
||||||
|
const { isOpen, releaseType, modalTitle, onReleaseTypeSelect, onModalClose } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<SelectReleaseTypeModalContent
|
||||||
|
releaseType={releaseType}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onReleaseTypeSelect={onReleaseTypeSelect}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectReleaseTypeModal;
|
|
@ -0,0 +1,99 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
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, kinds } from 'Helpers/Props';
|
||||||
|
import ReleaseType from 'InteractiveImport/ReleaseType';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
key: 'unknown',
|
||||||
|
get value() {
|
||||||
|
return translate('Unknown');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'singleEpisode',
|
||||||
|
get value() {
|
||||||
|
return translate('SingleEpisode');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'multiEpisode',
|
||||||
|
get value() {
|
||||||
|
return translate('MultiEpisode');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'seasonPack',
|
||||||
|
get value() {
|
||||||
|
return translate('SeasonPack');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SelectReleaseTypeModalContentProps {
|
||||||
|
releaseType: ReleaseType;
|
||||||
|
modalTitle: string;
|
||||||
|
onReleaseTypeSelect(releaseType: ReleaseType): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectReleaseTypeModalContent(
|
||||||
|
props: SelectReleaseTypeModalContentProps
|
||||||
|
) {
|
||||||
|
const { modalTitle, onReleaseTypeSelect, onModalClose } = props;
|
||||||
|
const [releaseType, setReleaseType] = useState(props.releaseType);
|
||||||
|
|
||||||
|
const handleReleaseTypeChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
setReleaseType(value as ReleaseType);
|
||||||
|
},
|
||||||
|
[setReleaseType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleReleaseTypeSelect = useCallback(() => {
|
||||||
|
onReleaseTypeSelect(releaseType);
|
||||||
|
}, [releaseType, onReleaseTypeSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{modalTitle} - {translate('SelectReleaseType')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ReleaseType')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="releaseType"
|
||||||
|
value={releaseType}
|
||||||
|
values={options}
|
||||||
|
onChange={handleReleaseTypeChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.SUCCESS} onPress={handleReleaseTypeSelect}>
|
||||||
|
{translate('SelectReleaseType')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectReleaseTypeModalContent;
|
|
@ -36,7 +36,7 @@ const monitoredOptions = [
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'monitored',
|
key: 'monitored',
|
||||||
|
@ -58,7 +58,7 @@ const seasonFolderOptions = [
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'yes',
|
key: 'yes',
|
||||||
|
|
|
@ -15,7 +15,7 @@ function SeasonInteractiveSearchModal(props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_LARGE}
|
size={sizes.EXTRA_EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
|
|
|
@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<FieldSet legend={translate('Conditions')}>
|
<FieldSet legend={translate('Conditions')}>
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
<div>
|
||||||
|
{translate('CustomFormatsSettingsTriggerInfo')}
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
<div className={styles.customFormats}>
|
<div className={styles.customFormats}>
|
||||||
{
|
{
|
||||||
specifications.map((tag) => {
|
specifications.map((tag) => {
|
||||||
|
|
|
@ -15,6 +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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditDownloadClientModalContent.css';
|
import styles from './EditDownloadClientModalContent.css';
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteDownloadClientPress,
|
onDeleteDownloadClientPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -199,6 +201,12 @@ class EditDownloadClientModalContent extends Component {
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
|
@ -239,6 +247,7 @@ EditDownloadClientModalContent.propTypes = {
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteDownloadClientPress: PropTypes.func
|
onDeleteDownloadClientPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,13 @@ 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 { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveDownloadClient,
|
||||||
|
setDownloadClientFieldValue,
|
||||||
|
setDownloadClientValue,
|
||||||
|
testDownloadClient,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||||
|
|
||||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||||
setDownloadClientValue,
|
setDownloadClientValue,
|
||||||
setDownloadClientFieldValue,
|
setDownloadClientFieldValue,
|
||||||
saveDownloadClient,
|
saveDownloadClient,
|
||||||
testDownloadClient
|
testDownloadClient,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditDownloadClientModalContentConnector extends Component {
|
class EditDownloadClientModalContentConnector extends Component {
|
||||||
|
@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||||
this.props.testDownloadClient({ id: this.props.id });
|
this.props.testDownloadClient({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
|
@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
||||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||||
saveDownloadClient: PropTypes.func.isRequired,
|
saveDownloadClient: PropTypes.func.isRequired,
|
||||||
testDownloadClient: PropTypes.func.isRequired,
|
testDownloadClient: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ const enableOptions = [
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector';
|
|
||||||
|
|
||||||
function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<EditImportListExclusionModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditImportListExclusionModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditImportListExclusionModal;
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
|
||||||
|
|
||||||
|
interface EditImportListExclusionModalProps {
|
||||||
|
id?: number;
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
onDeleteImportListExclusionPress?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditImportListExclusionModal(
|
||||||
|
props: EditImportListExclusionModalProps
|
||||||
|
) {
|
||||||
|
const { isOpen, onModalClose, ...otherProps } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onModalClosePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
clearPendingChanges({
|
||||||
|
section: 'settings.importListExclusions',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
|
||||||
|
<EditImportListExclusionModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditImportListExclusionModal;
|
|
@ -1,43 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import EditImportListExclusionModal from './EditImportListExclusionModal';
|
|
||||||
|
|
||||||
function mapStateToProps() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
clearPendingChanges
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditImportListExclusionModalConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.clearPendingChanges({ section: 'settings.importListExclusions' });
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditImportListExclusionModal
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditImportListExclusionModalConnector.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector);
|
|
|
@ -1,139 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
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 SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
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, kinds } from 'Helpers/Props';
|
|
||||||
import { numberSettingShape, stringSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './EditImportListExclusionModalContent.css';
|
|
||||||
|
|
||||||
function EditImportListExclusionModalContent(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item,
|
|
||||||
onInputChange,
|
|
||||||
onSavePress,
|
|
||||||
onModalClose,
|
|
||||||
onDeleteImportListExclusionPress,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
tvdbId
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{id ? translate('EditImportListExclusion') : translate('AddImportListExclusion')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody className={styles.body}>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('AddImportListExclusionError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !error &&
|
|
||||||
<Form
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Title')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="title"
|
|
||||||
helpText={translate('SeriesTitleToExcludeHelpText')}
|
|
||||||
{...title}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('TvdbId')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="tvdbId"
|
|
||||||
helpText={translate('TvdbIdExcludeHelpText')}
|
|
||||||
{...tvdbId}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
id &&
|
|
||||||
<Button
|
|
||||||
className={styles.deleteButton}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onDeleteImportListExclusionPress}
|
|
||||||
>
|
|
||||||
{translate('Delete')}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
error={saveError}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImportListExclusionShape = {
|
|
||||||
title: PropTypes.shape(stringSettingShape).isRequired,
|
|
||||||
tvdbId: PropTypes.shape(numberSettingShape).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
EditImportListExclusionModalContent.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
item: PropTypes.shape(ImportListExclusionShape).isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onSavePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onDeleteImportListExclusionPress: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditImportListExclusionModalContent;
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
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 SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
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 usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
saveImportListExclusion,
|
||||||
|
setImportListExclusionValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||||
|
import { PendingSection } from 'typings/pending';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditImportListExclusionModalContent.css';
|
||||||
|
|
||||||
|
const newImportListExclusion = {
|
||||||
|
title: '',
|
||||||
|
tvdbId: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EditImportListExclusionModalContentProps {
|
||||||
|
id?: number;
|
||||||
|
onModalClose: () => void;
|
||||||
|
onDeleteImportListExclusionPress?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createImportListExclusionSelector(id?: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.importListExclusions,
|
||||||
|
(importListExclusions) => {
|
||||||
|
const { isFetching, error, isSaving, saveError, pendingChanges, items } =
|
||||||
|
importListExclusions;
|
||||||
|
|
||||||
|
const mapping = id
|
||||||
|
? items.find((i) => i.id === id)
|
||||||
|
: newImportListExclusion;
|
||||||
|
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item: settings.settings as PendingSection<ImportListExclusion>,
|
||||||
|
...settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditImportListExclusionModalContent(
|
||||||
|
props: EditImportListExclusionModalContentProps
|
||||||
|
) {
|
||||||
|
const { id, onModalClose, onDeleteImportListExclusionPress } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetImportListExclusionValue = (payload: {
|
||||||
|
name: string;
|
||||||
|
value: string | number;
|
||||||
|
}) => {
|
||||||
|
// @ts-expect-error 'setImportListExclusionValue' isn't typed yet
|
||||||
|
dispatch(setImportListExclusionValue(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
|
||||||
|
useSelector(createImportListExclusionSelector(props.id));
|
||||||
|
const previousIsSaving = usePrevious(isSaving);
|
||||||
|
|
||||||
|
const { title, tvdbId } = item;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id) {
|
||||||
|
Object.keys(newImportListExclusion).forEach((name) => {
|
||||||
|
dispatchSetImportListExclusionValue({
|
||||||
|
name,
|
||||||
|
value:
|
||||||
|
newImportListExclusion[name as keyof typeof newImportListExclusion],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (previousIsSaving && !isSaving && !saveError) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSavePress = useCallback(() => {
|
||||||
|
dispatch(saveImportListExclusion({ id }));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
(payload: { name: string; value: string | number }) => {
|
||||||
|
// @ts-expect-error 'setImportListExclusionValue' isn't typed yet
|
||||||
|
dispatch(setImportListExclusionValue(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id
|
||||||
|
? translate('EditImportListExclusion')
|
||||||
|
: translate('AddImportListExclusion')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody className={styles.body}>
|
||||||
|
{isFetching && <LoadingIndicator />}
|
||||||
|
|
||||||
|
{!isFetching && !!error && (
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('AddImportListExclusionError')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isFetching && !error && (
|
||||||
|
<Form {...otherProps}>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Title')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="title"
|
||||||
|
helpText={translate('SeriesTitleToExcludeHelpText')}
|
||||||
|
{...title}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('TvdbId')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="tvdbId"
|
||||||
|
helpText={translate('TvdbIdExcludeHelpText')}
|
||||||
|
{...tvdbId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{id && (
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteImportListExclusionPress}
|
||||||
|
>
|
||||||
|
{translate('Delete')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditImportListExclusionModalContent;
|
|
@ -1,117 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { saveImportListExclusion, setImportListExclusionValue } from 'Store/Actions/settingsActions';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
|
|
||||||
|
|
||||||
const newImportListExclusion = {
|
|
||||||
title: '',
|
|
||||||
tvdbId: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
function createImportListExclusionSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.settings.importListExclusions,
|
|
||||||
(id, importListExclusions) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
pendingChanges,
|
|
||||||
items
|
|
||||||
} = importListExclusions;
|
|
||||||
|
|
||||||
const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion;
|
|
||||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item: settings.settings,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createImportListExclusionSelector(),
|
|
||||||
(importListExclusion) => {
|
|
||||||
return {
|
|
||||||
...importListExclusion
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setImportListExclusionValue,
|
|
||||||
saveImportListExclusion
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditImportListExclusionModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.id) {
|
|
||||||
Object.keys(newImportListExclusion).forEach((name) => {
|
|
||||||
this.props.setImportListExclusionValue({
|
|
||||||
name,
|
|
||||||
value: newImportListExclusion[name]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
|
||||||
this.props.onModalClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setImportListExclusionValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
this.props.saveImportListExclusion({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditImportListExclusionModalContent
|
|
||||||
{...this.props}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditImportListExclusionModalContentConnector.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
setImportListExclusionValue: PropTypes.func.isRequired,
|
|
||||||
saveImportListExclusion: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListExclusionModalContentConnector);
|
|
|
@ -1,25 +0,0 @@
|
||||||
.importListExclusion {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
height: 30px;
|
|
||||||
border-bottom: 1px solid var(--borderColor);
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
@add-mixin truncate;
|
|
||||||
|
|
||||||
flex: 0 1 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tvdbId {
|
|
||||||
flex: 0 0 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
|
@ -2,9 +2,6 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'importListExclusion': string;
|
|
||||||
'title': string;
|
|
||||||
'tvdbId': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
|
||||||
import styles from './ImportListExclusion.css';
|
|
||||||
|
|
||||||
class ImportListExclusion extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditImportListExclusionModalOpen: false,
|
|
||||||
isDeleteImportListExclusionModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditImportListExclusionPress = () => {
|
|
||||||
this.setState({ isEditImportListExclusionModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditImportListExclusionModalClose = () => {
|
|
||||||
this.setState({ isEditImportListExclusionModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteImportListExclusionPress = () => {
|
|
||||||
this.setState({
|
|
||||||
isEditImportListExclusionModalOpen: false,
|
|
||||||
isDeleteImportListExclusionModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onDeleteImportListExclusionModalClose = () => {
|
|
||||||
this.setState({ isDeleteImportListExclusionModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmDeleteImportListExclusion = () => {
|
|
||||||
this.props.onConfirmDeleteImportListExclusion(this.props.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
tvdbId
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.importListExclusion
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={styles.title}>{title}</div>
|
|
||||||
<div className={styles.tvdbId}>{tvdbId}</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<Link
|
|
||||||
onPress={this.onEditImportListExclusionPress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.EDIT} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditImportListExclusionModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={this.state.isEditImportListExclusionModalOpen}
|
|
||||||
onModalClose={this.onEditImportListExclusionModalClose}
|
|
||||||
onDeleteImportListExclusionPress={this.onDeleteImportListExclusionPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={this.state.isDeleteImportListExclusionModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('DeleteImportListExclusion')}
|
|
||||||
message={translate('DeleteImportListExclusionMessageText')}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
onConfirm={this.onConfirmDeleteImportListExclusion}
|
|
||||||
onCancel={this.onDeleteImportListExclusionModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportListExclusion.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
tvdbId: PropTypes.number.isRequired,
|
|
||||||
onConfirmDeleteImportListExclusion: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ImportListExclusion.defaultProps = {
|
|
||||||
// The drag preview will not connect the drag handle.
|
|
||||||
connectDragSource: (node) => node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportListExclusion;
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
.actions {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 35px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
7
frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts
vendored
Normal file
7
frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.css.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'actions': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditImportListExclusionModal from './EditImportListExclusionModal';
|
||||||
|
import styles from './ImportListExclusionRow.css';
|
||||||
|
|
||||||
|
interface ImportListExclusionRowProps extends ImportListExclusion {
|
||||||
|
onConfirmDeleteImportListExclusion: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportListExclusionRow(props: ImportListExclusionRowProps) {
|
||||||
|
const { id, title, tvdbId, onConfirmDeleteImportListExclusion } = props;
|
||||||
|
|
||||||
|
const [
|
||||||
|
isEditImportListExclusionModalOpen,
|
||||||
|
setEditImportListExclusionModalOpen,
|
||||||
|
setEditImportListExclusionModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isDeleteImportListExclusionModalOpen,
|
||||||
|
setDeleteImportListExclusionModalOpen,
|
||||||
|
setDeleteImportListExclusionModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const onConfirmDeleteImportListExclusionPress = useCallback(() => {
|
||||||
|
onConfirmDeleteImportListExclusion(id);
|
||||||
|
}, [id, onConfirmDeleteImportListExclusion]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell>{title}</TableRowCell>
|
||||||
|
<TableRowCell>{tvdbId}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
onPress={setEditImportListExclusionModalOpen}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<EditImportListExclusionModal
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditImportListExclusionModalOpen}
|
||||||
|
onModalClose={setEditImportListExclusionModalClosed}
|
||||||
|
onDeleteImportListExclusionPress={setDeleteImportListExclusionModalOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteImportListExclusionModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteImportListExclusion')}
|
||||||
|
message={translate('DeleteImportListExclusionMessageText')}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={onConfirmDeleteImportListExclusionPress}
|
||||||
|
onCancel={setDeleteImportListExclusionModalClosed}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportListExclusionRow;
|
|
@ -1,23 +0,0 @@
|
||||||
.importListExclusionsHeader {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
flex: 0 1 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tvdbId {
|
|
||||||
flex: 0 0 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addImportListExclusion {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -3,9 +3,6 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'addButton': string;
|
'addButton': string;
|
||||||
'addImportListExclusion': string;
|
'addImportListExclusion': string;
|
||||||
'importListExclusionsHeader': string;
|
|
||||||
'title': string;
|
|
||||||
'tvdbId': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
|
||||||
import ImportListExclusion from './ImportListExclusion';
|
|
||||||
import styles from './ImportListExclusions.css';
|
|
||||||
|
|
||||||
class ImportListExclusions extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddImportListExclusionModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onAddImportListExclusionPress = () => {
|
|
||||||
this.setState({ isAddImportListExclusionModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.setState({ isAddImportListExclusionModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
onConfirmDeleteImportListExclusion,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('ImportListExclusions')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('ImportListExclusionsLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.importListExclusionsHeader}>
|
|
||||||
<div className={styles.title}>
|
|
||||||
{translate('Title')}
|
|
||||||
</div>
|
|
||||||
<div className={styles.tvdbId}>
|
|
||||||
{translate('TvdbId')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
items.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<ImportListExclusion
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
{...otherProps}
|
|
||||||
index={index}
|
|
||||||
onConfirmDeleteImportListExclusion={onConfirmDeleteImportListExclusion}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.addImportListExclusion}>
|
|
||||||
<Link
|
|
||||||
className={styles.addButton}
|
|
||||||
onPress={this.onAddImportListExclusionPress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.ADD} />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditImportListExclusionModalConnector
|
|
||||||
isOpen={this.state.isAddImportListExclusionModalOpen}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportListExclusions.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onConfirmDeleteImportListExclusion: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportListExclusions;
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import * as importListExclusionActions from 'Store/Actions/Settings/importListExclusions';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditImportListExclusionModal from './EditImportListExclusionModal';
|
||||||
|
import ImportListExclusionRow from './ImportListExclusionRow';
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: () => translate('Title'),
|
||||||
|
isVisible: true,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tvdbid',
|
||||||
|
label: () => translate('TvdbId'),
|
||||||
|
isVisible: true,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
isVisible: true,
|
||||||
|
isSortable: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function createImportListExlucionsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.importListExclusions,
|
||||||
|
(importListExclusions) => {
|
||||||
|
return {
|
||||||
|
...importListExclusions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImportListExclusions() {
|
||||||
|
const history = useHistory();
|
||||||
|
const useCurrentPage = history.action === 'POP';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const fetchImportListExclusions = useCallback(() => {
|
||||||
|
dispatch(importListExclusionActions.fetchImportListExclusions());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const deleteImportListExclusion = useCallback(
|
||||||
|
(payload: { id: number }) => {
|
||||||
|
dispatch(importListExclusionActions.deleteImportListExclusion(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const gotoImportListExclusionFirstPage = useCallback(() => {
|
||||||
|
dispatch(importListExclusionActions.gotoImportListExclusionFirstPage());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const gotoImportListExclusionPreviousPage = useCallback(() => {
|
||||||
|
dispatch(importListExclusionActions.gotoImportListExclusionPreviousPage());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const gotoImportListExclusionNextPage = useCallback(() => {
|
||||||
|
dispatch(importListExclusionActions.gotoImportListExclusionNextPage());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const gotoImportListExclusionLastPage = useCallback(() => {
|
||||||
|
dispatch(importListExclusionActions.gotoImportListExclusionLastPage());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const gotoImportListExclusionPage = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
dispatch(
|
||||||
|
importListExclusionActions.gotoImportListExclusionPage({ page })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setImportListExclusionSort = useCallback(
|
||||||
|
(sortKey: { sortKey: string }) => {
|
||||||
|
dispatch(
|
||||||
|
importListExclusionActions.setImportListExclusionSort({ sortKey })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setImportListTableOption = useCallback(
|
||||||
|
(payload: { pageSize: number }) => {
|
||||||
|
dispatch(
|
||||||
|
importListExclusionActions.setImportListExclusionTableOption(payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(importListExclusionActions.gotoImportListExclusionFirstPage());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const repopulate = useCallback(() => {
|
||||||
|
gotoImportListExclusionFirstPage();
|
||||||
|
}, [gotoImportListExclusionFirstPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchImportListExclusions();
|
||||||
|
} else {
|
||||||
|
gotoImportListExclusionFirstPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => unregisterPagePopulator(repopulate);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onConfirmDeleteImportListExclusion = useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
deleteImportListExclusion({ id });
|
||||||
|
repopulate();
|
||||||
|
},
|
||||||
|
[deleteImportListExclusion, repopulate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selected = useSelector(createImportListExlucionsSelector());
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
items,
|
||||||
|
pageSize,
|
||||||
|
sortKey,
|
||||||
|
error,
|
||||||
|
sortDirection,
|
||||||
|
totalRecords,
|
||||||
|
...otherProps
|
||||||
|
} = selected;
|
||||||
|
|
||||||
|
const [
|
||||||
|
isAddImportListExclusionModalOpen,
|
||||||
|
setAddImportListExclusionModalOpen,
|
||||||
|
setAddImportListExclusionModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const isFetchingForFirstTime = isFetching && !isPopulated;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('ImportListExclusions')}>
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage={translate('ImportListExclusionsLoadError')}
|
||||||
|
isFetching={isFetchingForFirstTime}
|
||||||
|
isPopulated={isPopulated}
|
||||||
|
error={error}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
columns={COLUMNS}
|
||||||
|
canModifyColumns={false}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={setImportListExclusionSort}
|
||||||
|
onTableOptionChange={setImportListTableOption}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<ImportListExclusionRow
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
onConfirmDeleteImportListExclusion={
|
||||||
|
onConfirmDeleteImportListExclusion
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell />
|
||||||
|
<TableRowCell />
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
<IconButton
|
||||||
|
name={icons.ADD}
|
||||||
|
onPress={setAddImportListExclusionModalOpen}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
pageSize={pageSize}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={gotoImportListExclusionFirstPage}
|
||||||
|
onPreviousPagePress={gotoImportListExclusionPreviousPage}
|
||||||
|
onNextPagePress={gotoImportListExclusionNextPage}
|
||||||
|
onLastPagePress={gotoImportListExclusionLastPage}
|
||||||
|
onPageSelect={gotoImportListExclusionPage}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditImportListExclusionModal
|
||||||
|
isOpen={isAddImportListExclusionModalOpen}
|
||||||
|
onModalClose={setAddImportListExclusionModalClosed}
|
||||||
|
/>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportListExclusions;
|
|
@ -1,59 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { deleteImportListExclusion, fetchImportListExclusions } from 'Store/Actions/settingsActions';
|
|
||||||
import ImportListExclusions from './ImportListExclusions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.importListExclusions,
|
|
||||||
(importListExclusions) => {
|
|
||||||
return {
|
|
||||||
...importListExclusions
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchImportListExclusions,
|
|
||||||
deleteImportListExclusion
|
|
||||||
};
|
|
||||||
|
|
||||||
class ImportListExclusionsConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchImportListExclusions();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onConfirmDeleteImportListExclusion = (id) => {
|
|
||||||
this.props.deleteImportListExclusion({ id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ImportListExclusions
|
|
||||||
{...this.state}
|
|
||||||
{...this.props}
|
|
||||||
onConfirmDeleteImportListExclusion={this.onConfirmDeleteImportListExclusion}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportListExclusionsConnector.propTypes = {
|
|
||||||
fetchImportListExclusions: PropTypes.func.isRequired,
|
|
||||||
deleteImportListExclusion: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListExclusionsConnector);
|
|
|
@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
|
import ImportListsExclusions from './ImportListExclusions/ImportListExclusions';
|
||||||
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||||
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
||||||
import ImportListOptions from './Options/ImportListOptions';
|
import ImportListOptions from './Options/ImportListOptions';
|
||||||
|
@ -113,7 +113,7 @@ class ImportListSettings extends Component {
|
||||||
onChildStateChange={this.onChildStateChange}
|
onChildStateChange={this.onChildStateChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ImportListsExclusionsConnector />
|
<ImportListsExclusions />
|
||||||
<ManageImportListsModal
|
<ManageImportListsModal
|
||||||
isOpen={isManageImportListsOpen}
|
isOpen={isManageImportListsOpen}
|
||||||
onModalClose={this.onManageImportListsModalClose}
|
onModalClose={this.onManageImportListsModalClose}
|
||||||
|
|
|
@ -19,6 +19,7 @@ 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, tooltipPositions } from 'Helpers/Props';
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditImportListModalContent.css';
|
import styles from './EditImportListModalContent.css';
|
||||||
|
@ -38,6 +39,7 @@ function EditImportListModalContent(props) {
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteImportListPress,
|
onDeleteImportListPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -288,6 +290,12 @@ function EditImportListModalContent(props) {
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
|
@ -327,6 +335,7 @@ EditImportListModalContent.propTypes = {
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteImportListPress: PropTypes.func
|
onDeleteImportListPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,13 @@ 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 { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveImportList,
|
||||||
|
setImportListFieldValue,
|
||||||
|
setImportListValue,
|
||||||
|
testImportList,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditImportListModalContent from './EditImportListModalContent';
|
import EditImportListModalContent from './EditImportListModalContent';
|
||||||
|
|
||||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||||
setImportListValue,
|
setImportListValue,
|
||||||
setImportListFieldValue,
|
setImportListFieldValue,
|
||||||
saveImportList,
|
saveImportList,
|
||||||
testImportList
|
testImportList,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditImportListModalContentConnector extends Component {
|
class EditImportListModalContentConnector extends Component {
|
||||||
|
@ -56,6 +63,10 @@ class EditImportListModalContentConnector extends Component {
|
||||||
this.props.testImportList({ id: this.props.id });
|
this.props.testImportList({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -65,6 +76,7 @@ class EditImportListModalContentConnector extends Component {
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
|
@ -82,6 +94,7 @@ EditImportListModalContentConnector.propTypes = {
|
||||||
setImportListFieldValue: PropTypes.func.isRequired,
|
setImportListFieldValue: PropTypes.func.isRequired,
|
||||||
saveImportList: PropTypes.func.isRequired,
|
saveImportList: PropTypes.func.isRequired,
|
||||||
testImportList: PropTypes.func.isRequired,
|
testImportList: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ const autoAddOptions = [
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|
|
@ -32,7 +32,7 @@ const enableOptions = [
|
||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|
|
@ -80,19 +80,19 @@ const fileNameTokens = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const seriesTokens = [
|
const seriesTokens = [
|
||||||
{ token: '{Series Title}', example: 'The Series Title\'s!' },
|
{ token: '{Series Title}', example: 'The Series Title\'s!', footNote: 1 },
|
||||||
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!' },
|
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!', footNote: 1 },
|
||||||
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' },
|
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)', footNote: 1 },
|
||||||
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010' },
|
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010', footNote: 1 },
|
||||||
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!' },
|
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
|
||||||
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!' },
|
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!', footNote: 1 },
|
||||||
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The' },
|
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||||
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The' },
|
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||||
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)' },
|
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)', footNote: 1 },
|
||||||
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010' },
|
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010', footNote: 1 },
|
||||||
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' },
|
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||||
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The' },
|
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The', footNote: 1 },
|
||||||
{ token: '{Series TitleFirstCharacter}', example: 'S' },
|
{ token: '{Series TitleFirstCharacter}', example: 'S', footNote: 1 },
|
||||||
{ token: '{Series Year}', example: '2010' }
|
{ token: '{Series Year}', example: '2010' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -124,8 +124,8 @@ const absoluteTokens = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const episodeTitleTokens = [
|
const episodeTitleTokens = [
|
||||||
{ token: '{Episode Title}', example: 'Episode\'s Title' },
|
{ token: '{Episode Title}', example: 'Episode\'s Title', footNote: 1 },
|
||||||
{ token: '{Episode CleanTitle}', example: 'Episodes Title' }
|
{ token: '{Episode CleanTitle}', example: 'Episodes Title', footNote: 1 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const qualityTokens = [
|
const qualityTokens = [
|
||||||
|
@ -149,8 +149,13 @@ const mediaInfoTokens = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const otherTokens = [
|
const otherTokens = [
|
||||||
{ token: '{Release Group}', example: 'Rls Grp' },
|
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 },
|
||||||
{ token: '{Custom Formats}', example: 'iNTERNAL' }
|
{ token: '{Custom Formats}', example: 'iNTERNAL' },
|
||||||
|
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const otherAnimeTokens = [
|
||||||
|
{ token: '{Release Hash}', example: 'ABCDEFGH' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const originalTokens = [
|
const originalTokens = [
|
||||||
|
@ -300,7 +305,7 @@ class NamingModal extends Component {
|
||||||
<FieldSet legend={translate('Series')}>
|
<FieldSet legend={translate('Series')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
seriesTokens.map(({ token, example }) => {
|
seriesTokens.map(({ token, example, footNote }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -308,6 +313,7 @@ class NamingModal extends Component {
|
||||||
value={value}
|
value={value}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={this.onOptionPress}
|
onPress={this.onOptionPress}
|
||||||
|
@ -317,6 +323,11 @@ class NamingModal extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('SeriesFootNote')} />
|
||||||
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('SeriesID')}>
|
<FieldSet legend={translate('SeriesID')}>
|
||||||
|
@ -446,7 +457,7 @@ class NamingModal extends Component {
|
||||||
<FieldSet legend={translate('EpisodeTitle')}>
|
<FieldSet legend={translate('EpisodeTitle')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
episodeTitleTokens.map(({ token, example }) => {
|
episodeTitleTokens.map(({ token, example, footNote }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -454,6 +465,7 @@ class NamingModal extends Component {
|
||||||
value={value}
|
value={value}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={this.onOptionPress}
|
onPress={this.onOptionPress}
|
||||||
|
@ -463,6 +475,10 @@ class NamingModal extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('EpisodeTitleFootNote')} />
|
||||||
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Quality')}>
|
<FieldSet legend={translate('Quality')}>
|
||||||
|
@ -518,7 +534,26 @@ class NamingModal extends Component {
|
||||||
<FieldSet legend={translate('Other')}>
|
<FieldSet legend={translate('Other')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
otherTokens.map(({ token, example }) => {
|
otherTokens.map(({ token, example, footNote }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
|
tokenSeparator={tokenSeparator}
|
||||||
|
tokenCase={tokenCase}
|
||||||
|
onPress={this.onOptionPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
anime && otherAnimeTokens.map(({ token, example }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -535,6 +570,11 @@ class NamingModal extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
||||||
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Original')}>
|
<FieldSet legend={translate('Original')}>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
.token {
|
.token {
|
||||||
flex: 0 0 50%;
|
flex: 0 0 50%;
|
||||||
padding: 6px 6px;
|
padding: 6px;
|
||||||
background-color: var(--popoverTitleBackgroundColor);
|
background-color: var(--popoverTitleBackgroundColor);
|
||||||
font-family: $monoSpaceFontFamily;
|
font-family: $monoSpaceFontFamily;
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex: 0 0 50%;
|
flex: 0 0 50%;
|
||||||
padding: 6px 6px;
|
padding: 6px;
|
||||||
background-color: var(--popoverBodyBackgroundColor);
|
background-color: var(--popoverBodyBackgroundColor);
|
||||||
|
|
||||||
.footNote {
|
.footNote {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import translate from 'Utilities/String/translate';
|
||||||
import styles from './TheTvdb.css';
|
import styles from './TheTvdb.css';
|
||||||
|
|
||||||
function TheTvdb(props) {
|
function TheTvdb(props) {
|
||||||
debugger;
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<img
|
<img
|
||||||
|
|
|
@ -14,6 +14,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 { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import NotificationEventItems from './NotificationEventItems';
|
import NotificationEventItems from './NotificationEventItems';
|
||||||
import styles from './EditNotificationModalContent.css';
|
import styles from './EditNotificationModalContent.css';
|
||||||
|
@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteNotificationPress,
|
onDeleteNotificationPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
|
@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteNotificationPress: PropTypes.func
|
onDeleteNotificationPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,13 @@ 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 { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveNotification,
|
||||||
|
setNotificationFieldValue,
|
||||||
|
setNotificationValue,
|
||||||
|
testNotification,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||||
|
|
||||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
||||||
setNotificationValue,
|
setNotificationValue,
|
||||||
setNotificationFieldValue,
|
setNotificationFieldValue,
|
||||||
saveNotification,
|
saveNotification,
|
||||||
testNotification
|
testNotification,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditNotificationModalContentConnector extends Component {
|
class EditNotificationModalContentConnector extends Component {
|
||||||
|
@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
||||||
this.props.testNotification({ id: this.props.id });
|
this.props.testNotification({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
|
@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
||||||
setNotificationFieldValue: PropTypes.func.isRequired,
|
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||||
saveNotification: PropTypes.func.isRequired,
|
saveNotification: PropTypes.func.isRequired,
|
||||||
testNotification: PropTypes.func.isRequired,
|
testNotification: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
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 createServerSideCollectionHandlers from 'Store/Actions/Creators/createServerSideCollectionHandlers';
|
||||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { createThunk } from 'Store/thunks';
|
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Variables
|
// Variables
|
||||||
|
@ -14,6 +16,13 @@ const section = 'settings.importListExclusions';
|
||||||
// Actions Types
|
// Actions Types
|
||||||
|
|
||||||
export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions';
|
export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions';
|
||||||
|
export const GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionFirstPage';
|
||||||
|
export const GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPreviousPage';
|
||||||
|
export const GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionNextPage';
|
||||||
|
export const GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionLastPage';
|
||||||
|
export const GOTO_IMPORT_LIST_EXCLUSION_PAGE = 'settings/importListExclusions/gotoImportListExclusionPage';
|
||||||
|
export const SET_IMPORT_LIST_EXCLUSION_SORT = 'settings/importListExclusions/setImportListExclusionSort';
|
||||||
|
export const SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION = 'settings/importListExclusions/setImportListExclusionTableOption';
|
||||||
export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion';
|
export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion';
|
||||||
export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion';
|
export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion';
|
||||||
export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue';
|
export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue';
|
||||||
|
@ -22,9 +31,16 @@ export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/se
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS);
|
export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS);
|
||||||
|
export const gotoImportListExclusionFirstPage = createThunk(GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE);
|
||||||
|
export const gotoImportListExclusionPreviousPage = createThunk(GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE);
|
||||||
|
export const gotoImportListExclusionNextPage = createThunk(GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE);
|
||||||
|
export const gotoImportListExclusionLastPage = createThunk(GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE);
|
||||||
|
export const gotoImportListExclusionPage = createThunk(GOTO_IMPORT_LIST_EXCLUSION_PAGE);
|
||||||
|
export const setImportListExclusionSort = createThunk(SET_IMPORT_LIST_EXCLUSION_SORT);
|
||||||
export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION);
|
export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION);
|
||||||
export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION);
|
export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION);
|
||||||
|
|
||||||
|
export const setImportListExclusionTableOption = createAction(SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION);
|
||||||
export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => {
|
export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
section,
|
section,
|
||||||
|
@ -44,6 +60,7 @@ export default {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
pageSize: 20,
|
||||||
items: [],
|
items: [],
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
saveError: null,
|
saveError: null,
|
||||||
|
@ -53,17 +70,31 @@ export default {
|
||||||
//
|
//
|
||||||
// Action Handlers
|
// Action Handlers
|
||||||
|
|
||||||
actionHandlers: {
|
actionHandlers: handleThunks({
|
||||||
[FETCH_IMPORT_LIST_EXCLUSIONS]: createFetchHandler(section, '/importlistexclusion'),
|
...createServerSideCollectionHandlers(
|
||||||
|
section,
|
||||||
|
'/importlistexclusion/paged',
|
||||||
|
fetchImportListExclusions,
|
||||||
|
{
|
||||||
|
[serverSideCollectionHandlers.FETCH]: FETCH_IMPORT_LIST_EXCLUSIONS,
|
||||||
|
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_IMPORT_LIST_EXCLUSION_PAGE,
|
||||||
|
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_IMPORT_LIST_EXCLUSION_PAGE,
|
||||||
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_IMPORT_LIST_EXCLUSION_PAGE,
|
||||||
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_IMPORT_LIST_EXCLUSION_PAGE,
|
||||||
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_IMPORT_LIST_EXCLUSION_PAGE,
|
||||||
|
[serverSideCollectionHandlers.SORT]: SET_IMPORT_LIST_EXCLUSION_SORT
|
||||||
|
}
|
||||||
|
),
|
||||||
[SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'),
|
[SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'),
|
||||||
[DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion')
|
[DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion')
|
||||||
},
|
}),
|
||||||
|
|
||||||
//
|
//
|
||||||
// Reducers
|
// Reducers
|
||||||
|
|
||||||
reducers: {
|
reducers: {
|
||||||
[SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section)
|
[SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section),
|
||||||
|
[SET_IMPORT_LIST_EXCLUSION_TABLE_OPTION]: createSetTableOptionReducer(section)
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -163,6 +163,7 @@ export const actionHandlers = handleThunks({
|
||||||
languages: item.languages,
|
languages: item.languages,
|
||||||
releaseGroup: item.releaseGroup,
|
releaseGroup: item.releaseGroup,
|
||||||
indexerFlags: item.indexerFlags,
|
indexerFlags: item.indexerFlags,
|
||||||
|
releaseType: item.releaseType,
|
||||||
downloadId: item.downloadId
|
downloadId: item.downloadId
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -250,6 +250,12 @@ export const defaultState = {
|
||||||
label: () => translate('SeasonPack'),
|
label: () => translate('SeasonPack'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.EXACT,
|
||||||
valueType: filterBuilderValueTypes.BOOL
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'episodeRequested',
|
||||||
|
label: () => translate('EpisodeRequested'),
|
||||||
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import indexerFlags from 'Store/Actions/Settings/indexerFlags';
|
|
||||||
import { handleThunks } from 'Store/thunks';
|
import { handleThunks } from 'Store/thunks';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
import autoTaggings from './Settings/autoTaggings';
|
import autoTaggings from './Settings/autoTaggings';
|
||||||
|
@ -13,6 +12,7 @@ import general from './Settings/general';
|
||||||
import importListExclusions from './Settings/importListExclusions';
|
import importListExclusions from './Settings/importListExclusions';
|
||||||
import importListOptions from './Settings/importListOptions';
|
import importListOptions from './Settings/importListOptions';
|
||||||
import importLists from './Settings/importLists';
|
import importLists from './Settings/importLists';
|
||||||
|
import indexerFlags from './Settings/indexerFlags';
|
||||||
import indexerOptions from './Settings/indexerOptions';
|
import indexerOptions from './Settings/indexerOptions';
|
||||||
import indexers from './Settings/indexers';
|
import indexers from './Settings/indexers';
|
||||||
import languages from './Settings/languages';
|
import languages from './Settings/languages';
|
||||||
|
@ -91,7 +91,8 @@ export const defaultState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
'settings.advancedSettings'
|
'settings.advancedSettings',
|
||||||
|
'settings.importListExclusions.pageSize'
|
||||||
];
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Series from 'Series/Series';
|
||||||
|
|
||||||
|
function createMultiSeriesSelector(seriesIds: number[]) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.series.itemMap,
|
||||||
|
(state: AppState) => state.series.items,
|
||||||
|
(itemMap, allSeries) => {
|
||||||
|
return seriesIds.reduce((acc: Series[], seriesId) => {
|
||||||
|
const series = allSeries[itemMap[seriesId]];
|
||||||
|
|
||||||
|
if (series) {
|
||||||
|
acc.push(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createMultiSeriesSelector;
|
|
@ -1,45 +1,44 @@
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppSectionState, {
|
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||||
AppSectionItemState,
|
|
||||||
} from 'App/State/AppSectionState';
|
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
|
import SettingsAppState from 'App/State/SettingsAppState';
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import { PendingSection } from 'typings/pending';
|
import { PendingSection } from 'typings/pending';
|
||||||
|
|
||||||
type SettingNames = keyof Omit<AppState['settings'], 'advancedSettings'>;
|
type SectionsWithItemNames = {
|
||||||
type GetSectionState<Name extends SettingNames> = AppState['settings'][Name];
|
[K in keyof SettingsAppState]: SettingsAppState[K] extends AppSectionItemState<unknown>
|
||||||
type GetSettingsSectionItemType<Name extends SettingNames> =
|
? K
|
||||||
GetSectionState<Name> extends AppSectionItemState<infer R>
|
|
||||||
? R
|
|
||||||
: GetSectionState<Name> extends AppSectionState<infer R>
|
|
||||||
? R
|
|
||||||
: never;
|
: never;
|
||||||
|
}[keyof SettingsAppState];
|
||||||
|
|
||||||
type AppStateWithPending<Name extends SettingNames> = {
|
type GetSectionState<Name extends SectionsWithItemNames> =
|
||||||
item?: GetSettingsSectionItemType<Name>;
|
SettingsAppState[Name];
|
||||||
pendingChanges?: Partial<GetSettingsSectionItemType<Name>>;
|
type GetSettingsSectionItemType<Name extends SectionsWithItemNames> =
|
||||||
saveError?: Error;
|
GetSectionState<Name> extends AppSectionItemState<infer R> ? R : never;
|
||||||
} & GetSectionState<Name>;
|
|
||||||
|
|
||||||
function createSettingsSectionSelector<Name extends SettingNames>(
|
function createSettingsSectionSelector<
|
||||||
section: Name
|
Name extends SectionsWithItemNames,
|
||||||
) {
|
T extends GetSettingsSectionItemType<Name>
|
||||||
|
>(section: Name) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings[section],
|
(state: AppState) => state.settings[section],
|
||||||
(sectionSettings) => {
|
(sectionSettings) => {
|
||||||
const { item, pendingChanges, saveError, ...other } =
|
const { item, pendingChanges, ...other } = sectionSettings;
|
||||||
sectionSettings as AppStateWithPending<Name>;
|
|
||||||
|
|
||||||
const { settings, ...rest } = selectSettings(
|
const saveError =
|
||||||
item,
|
'saveError' in sectionSettings ? sectionSettings.saveError : undefined;
|
||||||
pendingChanges,
|
|
||||||
saveError
|
const {
|
||||||
);
|
settings,
|
||||||
|
pendingChanges: selectedPendingChanges,
|
||||||
|
...rest
|
||||||
|
} = selectSettings(item, pendingChanges, saveError);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...other,
|
...other,
|
||||||
saveError,
|
saveError,
|
||||||
settings: settings as PendingSection<GetSettingsSectionItemType<Name>>,
|
settings: settings as PendingSection<T>,
|
||||||
|
pendingChanges: selectedPendingChanges as Partial<T>,
|
||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandName {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userAgent {
|
|
||||||
color: #b0b0b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queued,
|
.queued,
|
||||||
.started,
|
.started,
|
||||||
.ended {
|
.ended {
|
||||||
|
|
|
@ -2,14 +2,12 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'commandName': string;
|
|
||||||
'duration': string;
|
'duration': string;
|
||||||
'ended': string;
|
'ended': string;
|
||||||
'queued': string;
|
'queued': string;
|
||||||
'started': string;
|
'started': string;
|
||||||
'trigger': string;
|
'trigger': string;
|
||||||
'triggerContent': string;
|
'triggerContent': string;
|
||||||
'userAgent': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -1,279 +0,0 @@
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import formatDate from 'Utilities/Date/formatDate';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './QueuedTaskRow.css';
|
|
||||||
|
|
||||||
function getStatusIconProps(status, message) {
|
|
||||||
const title = titleCase(status);
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case 'queued':
|
|
||||||
return {
|
|
||||||
name: icons.PENDING,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'started':
|
|
||||||
return {
|
|
||||||
name: icons.REFRESH,
|
|
||||||
isSpinning: true,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'completed':
|
|
||||||
return {
|
|
||||||
name: icons.CHECK,
|
|
||||||
kind: kinds.SUCCESS,
|
|
||||||
title: message === 'Completed' ? title : `${title}: ${message}`
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'failed':
|
|
||||||
return {
|
|
||||||
name: icons.FATAL,
|
|
||||||
kind: kinds.DANGER,
|
|
||||||
title: `${title}: ${message}`
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
name: icons.UNKNOWN,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormattedDates(props) {
|
|
||||||
const {
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (showRelativeDates) {
|
|
||||||
return {
|
|
||||||
queuedAt: moment(queued).fromNow(),
|
|
||||||
startedAt: started ? moment(started).fromNow() : '-',
|
|
||||||
endedAt: ended ? moment(ended).fromNow() : '-'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
queuedAt: formatDate(queued, shortDateFormat),
|
|
||||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
|
||||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueuedTaskRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
...getFormattedDates(props),
|
|
||||||
isCancelConfirmModalOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this._updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setUpdateTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
queued !== prevProps.queued ||
|
|
||||||
started !== prevProps.started ||
|
|
||||||
ended !== prevProps.ended
|
|
||||||
) {
|
|
||||||
this.setState(getFormattedDates(this.props));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._updateTimeoutId) {
|
|
||||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setUpdateTimer() {
|
|
||||||
this._updateTimeoutId = setTimeout(() => {
|
|
||||||
this.setState(getFormattedDates(this.props));
|
|
||||||
this.setUpdateTimer();
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onCancelPress = () => {
|
|
||||||
this.setState({
|
|
||||||
isCancelConfirmModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onAbortCancel = () => {
|
|
||||||
this.setState({
|
|
||||||
isCancelConfirmModalOpen: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
trigger,
|
|
||||||
commandName,
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended,
|
|
||||||
status,
|
|
||||||
duration,
|
|
||||||
message,
|
|
||||||
clientUserAgent,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onCancelPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
queuedAt,
|
|
||||||
startedAt,
|
|
||||||
endedAt,
|
|
||||||
isCancelConfirmModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
let triggerIcon = icons.QUICK;
|
|
||||||
|
|
||||||
if (trigger === 'manual') {
|
|
||||||
triggerIcon = icons.INTERACTIVE;
|
|
||||||
} else if (trigger === 'scheduled') {
|
|
||||||
triggerIcon = icons.SCHEDULED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableRowCell className={styles.trigger}>
|
|
||||||
<span className={styles.triggerContent}>
|
|
||||||
<Icon
|
|
||||||
name={triggerIcon}
|
|
||||||
title={titleCase(trigger)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
{...getStatusIconProps(status, message)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>
|
|
||||||
<span className={styles.commandName}>
|
|
||||||
{commandName}
|
|
||||||
</span>
|
|
||||||
{
|
|
||||||
clientUserAgent ?
|
|
||||||
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
|
|
||||||
{translate('From')}: {clientUserAgent}
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.queued}
|
|
||||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{queuedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.started}
|
|
||||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{startedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.ended}
|
|
||||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{endedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.duration}>
|
|
||||||
{formatTimeSpan(duration)}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
status === 'queued' &&
|
|
||||||
<IconButton
|
|
||||||
title={translate('RemovedFromTaskQueue')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
onPress={this.onCancelPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isCancelConfirmModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('Cancel')}
|
|
||||||
message={translate('CancelPendingTask')}
|
|
||||||
confirmLabel={translate('YesCancel')}
|
|
||||||
cancelLabel={translate('NoLeaveIt')}
|
|
||||||
onConfirm={onCancelPress}
|
|
||||||
onCancel={this.onAbortCancel}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTaskRow.propTypes = {
|
|
||||||
trigger: PropTypes.string.isRequired,
|
|
||||||
commandName: PropTypes.string.isRequired,
|
|
||||||
queued: PropTypes.string.isRequired,
|
|
||||||
started: PropTypes.string,
|
|
||||||
ended: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
duration: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
clientUserAgent: PropTypes.string,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onCancelPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueuedTaskRow;
|
|
|
@ -0,0 +1,238 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { CommandBody } from 'Commands/Command';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import formatDate from 'Utilities/Date/formatDate';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
|
||||||
|
import styles from './QueuedTaskRow.css';
|
||||||
|
|
||||||
|
function getStatusIconProps(status: string, message: string | undefined) {
|
||||||
|
const title = titleCase(status);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'queued':
|
||||||
|
return {
|
||||||
|
name: icons.PENDING,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'started':
|
||||||
|
return {
|
||||||
|
name: icons.REFRESH,
|
||||||
|
isSpinning: true,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'completed':
|
||||||
|
return {
|
||||||
|
name: icons.CHECK,
|
||||||
|
kind: kinds.SUCCESS,
|
||||||
|
title: message === 'Completed' ? title : `${title}: ${message}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'failed':
|
||||||
|
return {
|
||||||
|
name: icons.FATAL,
|
||||||
|
kind: kinds.DANGER,
|
||||||
|
title: `${title}: ${message}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
name: icons.UNKNOWN,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedDates(
|
||||||
|
queued: string,
|
||||||
|
started: string | undefined,
|
||||||
|
ended: string | undefined,
|
||||||
|
showRelativeDates: boolean,
|
||||||
|
shortDateFormat: string
|
||||||
|
) {
|
||||||
|
if (showRelativeDates) {
|
||||||
|
return {
|
||||||
|
queuedAt: moment(queued).fromNow(),
|
||||||
|
startedAt: started ? moment(started).fromNow() : '-',
|
||||||
|
endedAt: ended ? moment(ended).fromNow() : '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queuedAt: formatDate(queued, shortDateFormat),
|
||||||
|
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||||
|
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueuedTimes {
|
||||||
|
queuedAt: string;
|
||||||
|
startedAt: string;
|
||||||
|
endedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueuedTaskRowProps {
|
||||||
|
id: number;
|
||||||
|
trigger: string;
|
||||||
|
commandName: string;
|
||||||
|
queued: string;
|
||||||
|
started?: string;
|
||||||
|
ended?: string;
|
||||||
|
status: string;
|
||||||
|
duration?: string;
|
||||||
|
message?: string;
|
||||||
|
body: CommandBody;
|
||||||
|
clientUserAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
trigger,
|
||||||
|
commandName,
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
status,
|
||||||
|
duration,
|
||||||
|
message,
|
||||||
|
body,
|
||||||
|
clientUserAgent,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
|
||||||
|
useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [times, setTimes] = useState<QueuedTimes>(
|
||||||
|
getFormattedDates(
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isCancelConfirmModalOpen,
|
||||||
|
openCancelConfirmModal,
|
||||||
|
closeCancelConfirmModal,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const handleCancelPress = useCallback(() => {
|
||||||
|
dispatch(cancelCommand({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTimeTimeoutId.current = setTimeout(() => {
|
||||||
|
setTimes(
|
||||||
|
getFormattedDates(
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (updateTimeTimeoutId.current) {
|
||||||
|
clearTimeout(updateTimeTimeoutId.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
|
||||||
|
|
||||||
|
const { queuedAt, startedAt, endedAt } = times;
|
||||||
|
|
||||||
|
let triggerIcon = icons.QUICK;
|
||||||
|
|
||||||
|
if (trigger === 'manual') {
|
||||||
|
triggerIcon = icons.INTERACTIVE;
|
||||||
|
} else if (trigger === 'scheduled') {
|
||||||
|
triggerIcon = icons.SCHEDULED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell className={styles.trigger}>
|
||||||
|
<span className={styles.triggerContent}>
|
||||||
|
<Icon name={triggerIcon} title={titleCase(trigger)} />
|
||||||
|
|
||||||
|
<Icon {...getStatusIconProps(status, message)} />
|
||||||
|
</span>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<QueuedTaskRowNameCell
|
||||||
|
commandName={commandName}
|
||||||
|
body={body}
|
||||||
|
clientUserAgent={clientUserAgent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.queued}
|
||||||
|
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{queuedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.started}
|
||||||
|
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{startedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.ended}
|
||||||
|
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{endedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.duration}>
|
||||||
|
{formatTimeSpan(duration)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
{status === 'queued' && (
|
||||||
|
<IconButton
|
||||||
|
title={translate('RemovedFromTaskQueue')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={openCancelConfirmModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isCancelConfirmModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('Cancel')}
|
||||||
|
message={translate('CancelPendingTask')}
|
||||||
|
confirmLabel={translate('YesCancel')}
|
||||||
|
cancelLabel={translate('NoLeaveIt')}
|
||||||
|
onConfirm={handleCancelPress}
|
||||||
|
onCancel={closeCancelConfirmModal}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,31 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import QueuedTaskRow from './QueuedTaskRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(uiSettings) => {
|
|
||||||
return {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onCancelPress() {
|
|
||||||
dispatch(cancelCommand({
|
|
||||||
id: props.id
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
.commandName {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAgent {
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'commandName': string;
|
||||||
|
'userAgent': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { CommandBody } from 'Commands/Command';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import createMultiSeriesSelector from 'Store/Selectors/createMultiSeriesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './QueuedTaskRowNameCell.css';
|
||||||
|
|
||||||
|
export interface QueuedTaskRowNameCellProps {
|
||||||
|
commandName: string;
|
||||||
|
body: CommandBody;
|
||||||
|
clientUserAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueuedTaskRowNameCell(
|
||||||
|
props: QueuedTaskRowNameCellProps
|
||||||
|
) {
|
||||||
|
const { commandName, body, clientUserAgent } = props;
|
||||||
|
const seriesIds = [...(body.seriesIds ?? [])];
|
||||||
|
|
||||||
|
if (body.seriesId) {
|
||||||
|
seriesIds.push(body.seriesId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = useSelector(createMultiSeriesSelector(seriesIds));
|
||||||
|
const sortedSeries = series.sort((a, b) =>
|
||||||
|
a.sortTitle.localeCompare(b.sortTitle)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell>
|
||||||
|
<span className={styles.commandName}>
|
||||||
|
{commandName}
|
||||||
|
{sortedSeries.length ? (
|
||||||
|
<span> - {sortedSeries.map((s) => s.title).join(', ')}</span>
|
||||||
|
) : null}
|
||||||
|
{body.seasonNumber ? (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
{translate('SeasonNumberToken', {
|
||||||
|
seasonNumber: body.seasonNumber,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{clientUserAgent ? (
|
||||||
|
<span
|
||||||
|
className={styles.userAgent}
|
||||||
|
title={translate('TaskUserAgentTooltip')}
|
||||||
|
>
|
||||||
|
{translate('From')}: {clientUserAgent}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,90 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'trigger',
|
|
||||||
label: '',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'commandName',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'queued',
|
|
||||||
label: () => translate('Queued'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'started',
|
|
||||||
label: () => translate('Started'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ended',
|
|
||||||
label: () => translate('Ended'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'duration',
|
|
||||||
label: () => translate('Duration'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function QueuedTasks(props) {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
items
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('Queue')}>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated &&
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QueuedTaskRowConnector
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
}
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTasks.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.array.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueuedTasks;
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueuedTaskRow from './QueuedTaskRow';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'trigger',
|
||||||
|
label: '',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'commandName',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'queued',
|
||||||
|
label: () => translate('Queued'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'started',
|
||||||
|
label: () => translate('Started'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ended',
|
||||||
|
label: () => translate('Ended'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duration',
|
||||||
|
label: () => translate('Duration'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function QueuedTasks() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, isPopulated, items } = useSelector(
|
||||||
|
(state: AppState) => state.commands
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchCommands());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('Queue')}>
|
||||||
|
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||||
|
|
||||||
|
{isPopulated && (
|
||||||
|
<Table columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <QueuedTaskRow key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
|
||||||
import QueuedTasks from './QueuedTasks';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.commands,
|
|
||||||
(commands) => {
|
|
||||||
return commands;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchCommands: fetchCommands
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueuedTasksConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QueuedTasks
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTasksConnector.propTypes = {
|
|
||||||
dispatchFetchCommands: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
import QueuedTasks from './Queued/QueuedTasks';
|
||||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||||
|
|
||||||
function Tasks() {
|
function Tasks() {
|
||||||
|
@ -10,7 +10,7 @@ function Tasks() {
|
||||||
<PageContent title={translate('Tasks')}>
|
<PageContent title={translate('Tasks')}>
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<ScheduledTasksConnector />
|
<ScheduledTasksConnector />
|
||||||
<QueuedTasksConnector />
|
<QueuedTasks />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,13 +3,16 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
|
|
||||||
<!-- Chrome, Opera, and Firefox OS -->
|
<!-- Chrome, Opera, and Firefox OS -->
|
||||||
<meta name="theme-color" content="#3a3f51" />
|
<meta name="theme-color" content="#3a3f51" />
|
||||||
<!-- Windows Phone -->
|
<!-- Windows Phone -->
|
||||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||||
|
<!-- Android/Apple Phone -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|
||||||
<meta name="description" content="Sonarr" />
|
<meta name="description" content="Sonarr" />
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,16 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
|
|
||||||
<!-- Chrome, Opera, and Firefox OS -->
|
<!-- Chrome, Opera, and Firefox OS -->
|
||||||
<meta name="theme-color" content="#3a3f51" />
|
<meta name="theme-color" content="#3a3f51" />
|
||||||
<!-- Windows Phone -->
|
<!-- Windows Phone -->
|
||||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
||||||
|
<!-- Android/Apple Phone -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|
||||||
<meta name="description" content="Sonarr" />
|
<meta name="description" content="Sonarr" />
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export default interface ImportListExclusion extends ModelBase {
|
||||||
|
tvdbId: number;
|
||||||
|
title: string;
|
||||||
|
}
|
|
@ -1,7 +1,21 @@
|
||||||
|
export interface ValidationFailure {
|
||||||
|
propertyName: string;
|
||||||
|
errorMessage: string;
|
||||||
|
severity: 'error' | 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationError extends ValidationFailure {
|
||||||
|
isWarning: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationWarning extends ValidationFailure {
|
||||||
|
isWarning: true;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Pending<T> {
|
export interface Pending<T> {
|
||||||
value: T;
|
value: T;
|
||||||
errors: any[];
|
errors: ValidationError[];
|
||||||
warnings: any[];
|
warnings: ValidationWarning[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PendingSection<T> = {
|
export type PendingSection<T> = {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "6.0.405"
|
"version": "8.0.201"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
"watch": "webpack --watch --config ./frontend/build/webpack.config.js",
|
||||||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||||
"lint-fix": "yarn lint --fix",
|
"lint-fix": "yarn lint --fix",
|
||||||
"stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
"stylelint": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/Sonarr/Sonarr",
|
"repository": "https://github.com/Sonarr/Sonarr",
|
||||||
"author": "Team Sonarr",
|
"author": "Team Sonarr",
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@juggle/resize-observer": "3.4.0",
|
"@juggle/resize-observer": "3.4.0",
|
||||||
"@microsoft/signalr": "6.0.21",
|
"@microsoft/signalr": "8.0.0",
|
||||||
"@sentry/browser": "7.100.0",
|
"@sentry/browser": "7.100.0",
|
||||||
"@types/node": "18.16.8",
|
"@types/node": "18.16.8",
|
||||||
"@types/react": "18.2.6",
|
"@types/react": "18.2.6",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project>
|
<Project>
|
||||||
<!-- Common to all Sonarr Projects -->
|
<!-- Common to all Sonarr Projects -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AnalysisLevel>6.0-all</AnalysisLevel>
|
<AnalysisLevel>8.0-all</AnalysisLevel>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||||
|
@ -98,13 +98,6 @@
|
||||||
<RootNamespace Condition="'$(SonarrProject)'=='true'">$(MSBuildProjectName.Replace('Sonarr','NzbDrone'))</RootNamespace>
|
<RootNamespace Condition="'$(SonarrProject)'=='true'">$(MSBuildProjectName.Replace('Sonarr','NzbDrone'))</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TestProject)'!='true'">
|
|
||||||
<!-- Annotates .NET assemblies with repository information including SHA -->
|
|
||||||
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
|
||||||
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Sentry specific configuration: Only in Release mode -->
|
<!-- Sentry specific configuration: Only in Release mode -->
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
||||||
|
@ -129,14 +122,14 @@
|
||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="4.1.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
<PackageReference Include="NunitXml.TestLogger" Version="3.1.20" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
|
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net8.0'">
|
||||||
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
|
<PackageReference Include="coverlet.collector" Version="6.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(SonarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
<PropertyGroup Condition="'$(SonarrProject)'=='true' and '$(EnableAnalyzers)'=='false'">
|
||||||
|
@ -175,16 +168,46 @@
|
||||||
</Otherwise>
|
</Otherwise>
|
||||||
</Choose>
|
</Choose>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Set architecture to RuntimeInformation.ProcessArchitecture if not specified -->
|
||||||
|
<Choose>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>x64</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>x86</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>arm64</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>arm</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<Otherwise>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture></Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Otherwise>
|
||||||
|
</Choose>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
||||||
'$(RuntimeIdentifier)' == ''">
|
'$(RuntimeIdentifier)' == ''">
|
||||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
||||||
'$(RuntimeIdentifier)' == ''">
|
'$(RuntimeIdentifier)' == ''">
|
||||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" />
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" />
|
||||||
<PackageReference Include="Selenium.Support" Version="3.141.0" />
|
<PackageReference Include="Selenium.Support" Version="4.18.1" />
|
||||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="111.0.5563.6400" />
|
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="122.0.6261.9400" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" />
|
<ProjectReference Include="..\NzbDrone.Test.Common\Sonarr.Test.Common.csproj" />
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue